Files
mesh-controller/internal/secrets/seal_test.go
jschoubben 78c5b653cf A secret the mesh is given, not one it made
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.
2026-08-30 03:47:04 +02:00

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 = &notForYou{}
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)
}
}