The last of the four gaps ADR 0024 names. Everything the mesh handles today it generated itself, sealed to both ends, and discarded. An API key for a hosted service comes from a person, and carrying it needs a verb the mesh did not have. Accept seals it on the way in and keeps no plaintext — the same storage and the same property as a generated one, only a different origin. That is the whole difference from the arrangement being replaced, where an operator-supplied key sits in a column the control plane can read, which makes a copy of the database a copy of every account the mesh touches. The consequence is deliberate: the mesh cannot show it back. Somebody who loses the key gets a new one from wherever it came from. There is no reveal and there cannot be one, because a mesh that can reveal a secret is a mesh that holds it — asserted as a test, because it is a property somebody will eventually ask to break. An empty value is refused. A credential that exists, authenticates nowhere and looks exactly like a working one is the failure this whole mechanism is arranged to prevent.
217 lines
6.8 KiB
Go
217 lines
6.8 KiB
Go
package secrets
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
)
|
|
|
|
// A node's key, as the node would generate it and report the public half.
|
|
func nodeKey(t *testing.T) (public string, open func(string) ([]byte, error)) {
|
|
t.Helper()
|
|
private, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pub, priv [32]byte
|
|
copy(pub[:], private.PublicKey().Bytes())
|
|
copy(priv[:], private.Bytes())
|
|
return base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()),
|
|
func(sealed string) ([]byte, error) {
|
|
blob, err := base64.StdEncoding.DecodeString(sealed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, ok := box.OpenAnonymous(nil, blob, &pub, &priv)
|
|
if !ok {
|
|
return nil, errNotForYou
|
|
}
|
|
return out, nil
|
|
}
|
|
}
|
|
|
|
var errNotForYou = ¬ForYou{}
|
|
|
|
type notForYou struct{}
|
|
|
|
func (*notForYou) Error() string { return "not sealed to this node" }
|
|
|
|
func TestBothEndsGetTheSameSecretAndTheMeshGetsNeither(t *testing.T) {
|
|
// The whole arrangement in one test. The provider must create the credential the consumer was
|
|
// given, or the mesh reports success and nothing can connect — and neither blob is readable
|
|
// by whoever is holding them, which is the part encrypting a column does not achieve.
|
|
consumerPub, openConsumer := nodeKey(t)
|
|
providerPub, openProvider := nodeKey(t)
|
|
|
|
sealed, err := Make(consumerPub, providerPub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
forConsumer, err := openConsumer(sealed.ForConsumer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
forProvider, err := openProvider(sealed.ForProvider)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(forConsumer) != string(forProvider) {
|
|
t.Fatalf("the two ends were given different passwords: %q and %q",
|
|
forConsumer, forProvider)
|
|
}
|
|
if len(forConsumer) < 32 {
|
|
t.Fatalf("the password is %d characters, which is not a password", len(forConsumer))
|
|
}
|
|
// Neither can open the other's, which is what makes two blobs different from one shared key.
|
|
if _, err := openConsumer(sealed.ForProvider); err == nil {
|
|
t.Fatal("the consumer opened the provider's copy")
|
|
}
|
|
}
|
|
|
|
func TestTheSealedFormLooksNothingLikeTheSecret(t *testing.T) {
|
|
consumerPub, openConsumer := nodeKey(t)
|
|
providerPub, _ := nodeKey(t)
|
|
sealed, err := Make(consumerPub, providerPub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
password, err := openConsumer(sealed.ForConsumer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(sealed.ForConsumer, string(password)) {
|
|
t.Fatal("the secret is visible inside what is stored")
|
|
}
|
|
if sealed.ForConsumer == sealed.ForProvider {
|
|
// Sealed boxes are randomised, so an observer cannot tell the two ends hold the same
|
|
// value — nor that a rotation changed nothing.
|
|
t.Fatal("the two blobs are identical, so the storage says they hold the same value")
|
|
}
|
|
}
|
|
|
|
func TestAPasswordIsSafeToPutInAFile(t *testing.T) {
|
|
// It lands in a file something else reads. A newline or a quote in it is a support call.
|
|
consumerPub, openConsumer := nodeKey(t)
|
|
providerPub, _ := nodeKey(t)
|
|
for i := 0; i < 50; i++ {
|
|
sealed, err := Make(consumerPub, providerPub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
password, err := openConsumer(sealed.ForConsumer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.ContainsAny(string(password), "\n\r\t \"'\\$`") {
|
|
t.Fatalf("a password needs quoting: %q", password)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEverySecretIsDifferent(t *testing.T) {
|
|
consumerPub, openConsumer := nodeKey(t)
|
|
providerPub, _ := nodeKey(t)
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 50; i++ {
|
|
sealed, _ := Make(consumerPub, providerPub)
|
|
password, _ := openConsumer(sealed.ForConsumer)
|
|
if seen[string(password)] {
|
|
t.Fatalf("the same password came out twice: %q", password)
|
|
}
|
|
seen[string(password)] = true
|
|
}
|
|
}
|
|
|
|
func TestAnEndWithNoSealingKeyIsRefused(t *testing.T) {
|
|
// Sealing to nothing would produce a blob nobody can open, stored as though it were a working
|
|
// credential — which is the failure this whole design exists to make impossible.
|
|
//
|
|
// Asserted on the message, not merely on failing. Seal refuses an empty key anyway, so a test
|
|
// that only checked for an error passed with this check removed and proved nothing about it.
|
|
// What the check adds is a reason a person can act on: the remedy is on the node, not here.
|
|
public, _ := nodeKey(t)
|
|
for _, pair := range [][2]string{{public, ""}, {"", public}} {
|
|
_, err := Make(pair[0], pair[1])
|
|
if err == nil {
|
|
t.Fatal("a secret was made for a node with no sealing key")
|
|
}
|
|
if !strings.Contains(err.Error(), "both ends need a sealing key") {
|
|
t.Fatalf("the refusal does not say what is missing: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSomethingThatIsNotAKeyIsRefused(t *testing.T) {
|
|
if _, err := Seal("not-a-key", []byte("x")); err == nil {
|
|
t.Fatal("a secret was sealed to nonsense")
|
|
}
|
|
short := base64.StdEncoding.EncodeToString([]byte("too short"))
|
|
if _, err := Seal(short, []byte("x")); err == nil {
|
|
t.Fatal("a secret was sealed to a key of the wrong length")
|
|
}
|
|
}
|
|
|
|
func TestASecretSomebodySuppliedIsKeptTheSameWay(t *testing.T) {
|
|
// An API key comes from a person; the mesh's job is to carry it without being able to read it
|
|
// afterwards. Same storage, same property, different origin.
|
|
consumerPub, openConsumer := nodeKey(t)
|
|
providerPub, openProvider := nodeKey(t)
|
|
|
|
sealed, err := Accept("sk-a-real-looking-key", consumerPub, providerPub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := openConsumer(sealed.ForConsumer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "sk-a-real-looking-key" {
|
|
t.Fatalf("the value did not survive: %q", got)
|
|
}
|
|
if _, err := openProvider(sealed.ForProvider); err != nil {
|
|
t.Fatal("the other end cannot open its copy")
|
|
}
|
|
// And what is stored is not the value, which is the whole point.
|
|
for what, blob := range map[string]string{
|
|
"the consumer's copy": sealed.ForConsumer, "the provider's copy": sealed.ForProvider,
|
|
} {
|
|
if strings.Contains(blob, "sk-a-real-looking-key") {
|
|
t.Fatalf("%s holds the key in the clear", what)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSealingNothingIsRefused(t *testing.T) {
|
|
// An empty key sealed and stored would be a credential that exists, authenticates nowhere,
|
|
// and looks exactly like a working one.
|
|
public, _ := nodeKey(t)
|
|
for _, value := range []string{"", " ", "\n"} {
|
|
if _, err := Accept(value, public, public); err == nil {
|
|
t.Fatalf("%q was accepted as a secret", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnAcceptedSecretCannotBeReadBack(t *testing.T) {
|
|
// Stated as a test because it is a property somebody will ask to break. There is no field
|
|
// holding the value and no function returning it — a mesh that can reveal a secret is a mesh
|
|
// that holds it.
|
|
public, _ := nodeKey(t)
|
|
sealed, err := Accept("sk-something", public, public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
said, err := json.Marshal(sealed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(said), "sk-something") {
|
|
t.Fatalf("what is kept carries the secret: %s", said)
|
|
}
|
|
}
|