package maintenance import ( "bytes" "crypto/ecdh" "encoding/base64" "encoding/hex" "encoding/json" "os" "strings" "testing" "github.com/carlosframework/platform/internal/appconfig" ) type vector struct { RecipientPrivate string `json:"recipient_private_hex"` EphemeralPrivate string `json:"ephemeral_private_hex"` Nonce string `json:"nonce_hex"` Plaintext string `json:"plaintext"` Binding Binding `json:"binding"` AAD string `json:"aad_hex"` Envelope string `json:"envelope"` } func fixture(t *testing.T) (vector, *ecdh.PrivateKey, *ecdh.PrivateKey, []byte) { t.Helper() raw, err := os.ReadFile("testdata/envelope-vector.json") if err != nil { t.Fatal(err) } var v vector if err := json.Unmarshal(raw, &v); err != nil { t.Fatal(err) } decode := func(s string) []byte { b, err := hex.DecodeString(s) if err != nil { t.Fatal(err) } return b } key := func(s string) *ecdh.PrivateKey { k, err := ecdh.P256().NewPrivateKey(decode(s)) if err != nil { t.Fatal(err) } return k } return v, key(v.RecipientPrivate), key(v.EphemeralPrivate), decode(v.Nonce) } func refused(t *testing.T, k *ecdh.PrivateKey, b Binding, envelope string) { t.Helper() got, err := Open(k, b, envelope) if err != ErrEnvelope || got != nil { t.Fatal("invalid envelope returned plaintext or a non-fixed error") } } func TestEnvelopeIndependentVector(t *testing.T) { v, k, eph, nonce := fixture(t) aad, err := bindingAAD(v.Binding, k.PublicKey()) if err != nil { t.Fatal(err) } if hex.EncodeToString(aad) != v.AAD { t.Fatal("authenticated field encoding differs from independent vector") } got, err := sealWith(eph, nonce, k.PublicKey(), v.Binding, []byte(v.Plaintext)) if err != nil { t.Fatal(err) } if got != v.Envelope { t.Fatal("ciphertext differs from independent Node/OpenSSL vector") } plain, err := Open(k, v.Binding, v.Envelope) if err != nil || !bytes.Equal(plain, []byte(v.Plaintext)) { t.Fatal("independent vector did not decrypt") } } func TestEnvelopeEveryBindingField(t *testing.T) { v, k, _, _ := fixture(t) mutations := map[string]func(*Binding){ "version": func(b *Binding) { b.Version++ }, "direction": func(b *Binding) { b.Direction = DirectionResult }, "account": func(b *Binding) { b.Account = "other" }, "app": func(b *Binding) { b.App = "docs" }, "host": func(b *Binding) { b.Host = "other.home.example" }, "request": func(b *Binding) { b.RequestID = "request-0002" }, "attempt": func(b *Binding) { b.AttemptID = "attempt-0002" }, "recipient": func(b *Binding) { b.Recipient = strings.Repeat("0", 64) }, } for name, mutate := range mutations { t.Run(name, func(t *testing.T) { b := v.Binding; mutate(&b); refused(t, k, b, v.Envelope) }) } otherRaw := make([]byte, 32) otherRaw[31] = 3 other, err := ecdh.P256().NewPrivateKey(otherRaw) if err != nil { t.Fatal(err) } b := v.Binding b.Recipient, _ = RecipientFingerprint(other.PublicKey()) refused(t, other, b, v.Envelope) } func TestEnvelopeLengthPrefixesPreventFieldConcatenation(t *testing.T) { v, k, _, _ := fixture(t) b := v.Binding b.Account = "ab" b.App = "c" sealed, err := Seal(k.PublicKey(), b, []byte(v.Plaintext)) if err != nil { t.Fatal(err) } b.Account = "a" b.App = "bc" refused(t, k, b, sealed) } func TestEnvelopeDirectionsAndFreshness(t *testing.T) { v, k, _, _ := fixture(t) for _, direction := range []Direction{DirectionInput, DirectionResult, DirectionEnvironment} { t.Run(string(direction), func(t *testing.T) { b := v.Binding b.Direction = direction first, err := Seal(k.PublicKey(), b, []byte(v.Plaintext)) if err != nil { t.Fatal(err) } second, err := Seal(k.PublicKey(), b, []byte(v.Plaintext)) if err != nil { t.Fatal(err) } if first == second { t.Fatal("production sealing reused its random envelope") } plain, err := Open(k, b, first) if err != nil || string(plain) != v.Plaintext { t.Fatal("round trip failed") } for _, other := range []Direction{DirectionInput, DirectionResult, DirectionEnvironment} { if other != direction { changed := b changed.Direction = other refused(t, k, changed, first) } } }) } } func TestEnvelopeRejectsEveryTamperedByteAndNoncanonicalInput(t *testing.T) { v, k, _, _ := fixture(t) raw, err := base64.StdEncoding.DecodeString(v.Envelope) if err != nil { t.Fatal(err) } for i := range raw { changed := bytes.Clone(raw) changed[i] ^= 1 refused(t, k, v.Binding, base64.StdEncoding.EncodeToString(changed)) } for _, bad := range []string{"", v.Envelope + "\n", " " + v.Envelope, v.Envelope[:len(v.Envelope)-1], v.Envelope + "A", base64.RawStdEncoding.EncodeToString(raw), strings.Repeat("A", base64.StdEncoding.EncodedLen(MaxEnvelopePlaintext+1024))} { refused(t, k, v.Binding, bad) } for i := 0; i < len(raw); i++ { refused(t, k, v.Binding, base64.StdEncoding.EncodeToString(raw[:i])) } } func TestEnvelopeLimitsAndInvalidConfiguration(t *testing.T) { v, k, eph, nonce := fixture(t) for _, size := range []int{0, MaxEnvelopePlaintext} { plain := bytes.Repeat([]byte{0x42}, size) sealed, err := Seal(k.PublicKey(), v.Binding, plain) if err != nil { t.Fatal(err) } got, err := Open(k, v.Binding, sealed) if err != nil || !bytes.Equal(got, plain) { t.Fatal("boundary payload failed") } } if got, err := Seal(k.PublicKey(), v.Binding, make([]byte, MaxEnvelopePlaintext+1)); err != ErrEnvelope || got != "" { t.Fatal("oversized plaintext accepted") } invalid := []Binding{v.Binding, v.Binding, v.Binding, v.Binding, v.Binding, v.Binding, v.Binding, v.Binding} invalid[0].Account = "" invalid[1].App = "space app" invalid[2].Host = strings.Repeat("a", 254) invalid[3].RequestID = "request\x00suffix" invalid[4].AttemptID = strings.Repeat("a", 65) invalid[5].Direction = "unknown" invalid[6].Version = 0 invalid[7].Recipient = "" for _, b := range invalid { if got, err := Seal(k.PublicKey(), b, []byte(v.Plaintext)); err != ErrEnvelope || got != "" { t.Fatal("invalid binding accepted") } refused(t, k, b, v.Envelope) } if _, err := Seal(nil, v.Binding, nil); err != ErrEnvelope { t.Fatal("nil recipient accepted") } refused(t, nil, v.Binding, v.Envelope) x, err := ecdh.X25519().NewPrivateKey(bytes.Repeat([]byte{1}, 32)) if err != nil { t.Fatal(err) } if _, err := RecipientFingerprint(x.PublicKey()); err != ErrEnvelope { t.Fatal("wrong curve accepted") } refused(t, x, v.Binding, v.Envelope) for _, n := range [][]byte{nil, nonce[:len(nonce)-1], append(bytes.Clone(nonce), 0)} { if _, err := sealWith(eph, n, k.PublicKey(), v.Binding, nil); err != ErrEnvelope { t.Fatal("invalid nonce accepted") } } } func TestEnvelopeCannotCrossAppconfigPurpose(t *testing.T) { v, k, _, _ := fixture(t) appBinding := appconfig.Binding{Account: v.Binding.Account, App: v.Binding.App} unbound, err := appconfig.SealUnbound(k.PublicKey(), []byte(v.Plaintext)) if err != nil { t.Fatal(err) } bound, err := appconfig.SealBound(k.PublicKey(), appBinding, []byte(v.Plaintext)) if err != nil { t.Fatal(err) } for _, old := range []string{unbound, bound} { refused(t, k, v.Binding, old) oldRaw, err := base64.StdEncoding.DecodeString(old) if err != nil { t.Fatal(err) } refused(t, k, v.Binding, base64.StdEncoding.EncodeToString(append([]byte(envelopePrefix), oldRaw...))) } newRaw, err := base64.StdEncoding.DecodeString(v.Envelope) if err != nil { t.Fatal(err) } if got, err := appconfig.Open(k, appBinding, base64.StdEncoding.EncodeToString(newRaw[len(envelopePrefix):])); err == nil || got != nil { t.Fatal("maintenance ciphertext opened as appconfig secret") } }