// internal/appconfig/crypto_test.go package appconfig import ( "bytes" "crypto/ecdh" "encoding/base64" "strings" "testing" ) // fixedScalar returns a 32-byte P-256 scalar for deterministic test keys — // never used outside tests. Byte 0 is pinned to 0x01 to keep it a valid // scalar well under the P-256 order regardless of the fill byte. func fixedScalar(b byte) []byte { s := make([]byte, 32) for i := range s { s[i] = b } s[0] = 0x01 return s } var testBinding = Binding{Account: "bab", App: "keymail"} // TestSealOpenGoldenVector pins the wire format (ephPub(65) || nonce(12) || // ciphertext, base64) against a fixed ephemeral key and nonce so a future // change to field order, sizes, or the HKDF info string breaks loudly // instead of silently producing envelopes older code can't read. The // expected value was computed by running this exact algorithm once, not // invented — see the plan's Task 2 for how. func TestSealOpenGoldenVector(t *testing.T) { recipientPriv, err := ecdh.P256().NewPrivateKey(fixedScalar(0x11)) if err != nil { t.Fatalf("recipient key: %v", err) } ephPriv, err := ecdh.P256().NewPrivateKey(fixedScalar(0x22)) if err != nil { t.Fatalf("ephemeral key: %v", err) } nonce := make([]byte, 12) for i := range nonce { nonce[i] = byte(i + 1) } plaintext := []byte("s3kr1t-value") const wantEnvelope = "BKtNPVwwBQiJVQNbG6mW7bJMPFZfpNRwU1bRNeKQcV29Pzvpof1ONhvRxqXEJnk/4+qdPPRg1qWtCl0sgEsTaUgBAgMEBQYHCAkKCwwPFyVKJUipEHkmgC9Wpeogyu3CLH/32CzrKb0y" got, err := sealWith(ephPriv, nonce, recipientPriv.PublicKey(), "", hkdfInfo, nil, plaintext) if err != nil { t.Fatalf("sealWith: %v", err) } if got != wantEnvelope { t.Fatalf("envelope drifted from the pinned wire format:\n got: %s\n want: %s", got, wantEnvelope) } // SealUnbound is the same v1 shape; and a v1 envelope — every one in a // production bucket today — opens under ANY binding (see Open). opened, err := Open(recipientPriv, Binding{Account: "zzz", App: "other"}, got) if err != nil { t.Fatalf("Open: %v", err) } if !bytes.Equal(opened, plaintext) { t.Fatalf("Open = %q, want %q", opened, plaintext) } } func TestSealOpenRoundTripRandomKeys(t *testing.T) { priv, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } envelope, err := SealBound(pubKey, testBinding, []byte("hello")) if err != nil { t.Fatalf("Seal: %v", err) } got, err := Open(privKey, testBinding, envelope) if err != nil { t.Fatalf("Open: %v", err) } if string(got) != "hello" { t.Fatalf("Open = %q, want hello", got) } } func TestOpenRejectsTamperedCiphertext(t *testing.T) { priv, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } envelope, err := SealBound(pubKey, testBinding, []byte("hello")) if err != nil { t.Fatalf("Seal: %v", err) } raw, err := base64.StdEncoding.DecodeString(envelope) if err != nil { t.Fatalf("decode: %v", err) } raw[len(raw)-1] ^= 0xFF // flip a ciphertext byte tampered := base64.StdEncoding.EncodeToString(raw) if _, err := Open(privKey, testBinding, tampered); err == nil { t.Fatal("Open accepted tampered ciphertext, want an error") } } // TestCheckEnvelopeAcceptsSealed pins the property the console door relies // on: anything Seal produces passes the structural check. If these two ever // diverge, the door starts refusing envelopes the box could have opened. // // Empty plaintext is the ONE deliberate exception, and it is the next test // rather than a case here. It is not a structural refusal — the envelope is // perfectly well formed and the box could open it and find "" — it is a // refusal of the VALUE, added because an empty secret is nearly always a // substitution that produced nothing (discussion #16). Keeping the two // tests apart keeps that distinction where the next person will see it: the // invariant this test protects is still "Seal's output is structurally // valid", not "the door takes everything". func TestCheckEnvelopeAcceptsSealed(t *testing.T) { _, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } for _, plaintext := range [][]byte{[]byte("s3kr1t"), []byte("x")} { envelope, err := SealBound(pubKey, testBinding, plaintext) if err != nil { t.Fatalf("Seal(%q): %v", plaintext, err) } if err := CheckEnvelope(envelope); err != nil { t.Fatalf("CheckEnvelope rejected a Seal envelope for plaintext %q: %v", plaintext, err) } unbound, err := SealUnbound(pubKey, plaintext) if err != nil { t.Fatalf("SealUnbound(%q): %v", plaintext, err) } if err := CheckEnvelope(unbound); err != nil { t.Fatalf("CheckEnvelope rejected a SealUnbound envelope for plaintext %q: %v", plaintext, err) } } } // TestCheckEnvelopeRefusesEmptyValue pins the length-read the refusal rests // on, because it is the whole mechanism and it is not obvious: AES-GCM is // length-preserving, so an envelope of EXACTLY ephLen+nonceLen+16 sealed an // empty plaintext and one byte longer did not. That is the only fact about a // secret's value this side of the seal can know without a key, and if it // ever stopped being true this refusal would start rejecting real // one-character secrets — hence the boundary is asserted from both sides, // against real Seal output rather than against a hand-built buffer. func TestCheckEnvelopeRefusesEmptyValue(t *testing.T) { _, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } empty, err := SealBound(pubKey, testBinding, []byte{}) if err != nil { t.Fatalf("Seal(empty): %v", err) } unboundEmpty, err := SealUnbound(pubKey, []byte{}) if err != nil { t.Fatalf("SealUnbound(empty): %v", err) } if err := CheckEnvelope(unboundEmpty); err == nil || !strings.Contains(err.Error(), "empty value") { t.Fatalf("CheckEnvelope on an unbound empty value: %v", err) } err = CheckEnvelope(empty) if err == nil { t.Fatal("CheckEnvelope accepted an envelope sealing an empty value") } if !strings.Contains(err.Error(), "empty value") { t.Fatalf("error %q does not name the defect (want it to mention %q)", err, "empty value") } // The refusal points somewhere useful rather than just saying no: the // two things an operator might actually have meant. for _, want := range []string{"secrets unset", "env set"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error %q does not offer %q", err, want) } } // One byte over the floor is a real secret and must survive — the // shortest value this check must never eat. oneByte, err := SealBound(pubKey, testBinding, []byte("x")) if err != nil { t.Fatalf("Seal(x): %v", err) } if err := CheckEnvelope(oneByte); err != nil { t.Fatalf("CheckEnvelope rejected a one-byte secret: %v", err) } } // TestCheckEnvelopeRefusals walks the structural defects the door has to // catch at SET time, and asserts on the one thing every refusal must have in // common besides being an error: it names the DEFECT and never echoes the // envelope. An envelope is ciphertext an operator may have pasted from // anywhere; a refusal that quotes it back puts it into console logs, audit // rows and HTTP responses for nothing. func TestCheckEnvelopeRefusals(t *testing.T) { // 65 bytes that are not a P-256 point (0xAA is not the 0x04 // uncompressed-point tag), padded past the minimum length so the length // check cannot be what refuses it. notAPoint := make([]byte, ephLen+nonceLen+16) for i := range notAPoint { notAPoint[i] = 0xAA } cases := []struct { name string envelope string want string // substring naming the defect }{ {"empty", "", "too short"}, {"not base64", "!!!not base64!!!", "base64"}, {"too short", base64.StdEncoding.EncodeToString(make([]byte, ephLen+nonceLen+15)), "too short"}, {"ephemeral key is not a point", base64.StdEncoding.EncodeToString(notAPoint), "ephemeral key"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := CheckEnvelope(tc.envelope) if err == nil { t.Fatalf("CheckEnvelope accepted %s", tc.name) } if !strings.Contains(err.Error(), tc.want) { t.Fatalf("error %q does not name the defect (want it to mention %q)", err, tc.want) } if tc.envelope != "" && strings.Contains(err.Error(), tc.envelope) { t.Fatalf("error echoes the envelope back: %q", err) } }) } }