carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package maintenance
2
3import (
4 "bytes"
5 "crypto/ecdh"
6 "encoding/base64"
7 "encoding/hex"
8 "encoding/json"
9 "os"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/appconfig"
14)
15
16type vector struct {
17 RecipientPrivate string `json:"recipient_private_hex"`
18 EphemeralPrivate string `json:"ephemeral_private_hex"`
19 Nonce string `json:"nonce_hex"`
20 Plaintext string `json:"plaintext"`
21 Binding Binding `json:"binding"`
22 AAD string `json:"aad_hex"`
23 Envelope string `json:"envelope"`
24}
25
26func fixture(t *testing.T) (vector, *ecdh.PrivateKey, *ecdh.PrivateKey, []byte) {
27 t.Helper()
28 raw, err := os.ReadFile("testdata/envelope-vector.json")
29 if err != nil {
30 t.Fatal(err)
31 }
32 var v vector
33 if err := json.Unmarshal(raw, &v); err != nil {
34 t.Fatal(err)
35 }
36 decode := func(s string) []byte {
37 b, err := hex.DecodeString(s)
38 if err != nil {
39 t.Fatal(err)
40 }
41 return b
42 }
43 key := func(s string) *ecdh.PrivateKey {
44 k, err := ecdh.P256().NewPrivateKey(decode(s))
45 if err != nil {
46 t.Fatal(err)
47 }
48 return k
49 }
50 return v, key(v.RecipientPrivate), key(v.EphemeralPrivate), decode(v.Nonce)
51}
52func refused(t *testing.T, k *ecdh.PrivateKey, b Binding, envelope string) {
53 t.Helper()
54 got, err := Open(k, b, envelope)
55 if err != ErrEnvelope || got != nil {
56 t.Fatal("invalid envelope returned plaintext or a non-fixed error")
57 }
58}
59func TestEnvelopeIndependentVector(t *testing.T) {
60 v, k, eph, nonce := fixture(t)
61 aad, err := bindingAAD(v.Binding, k.PublicKey())
62 if err != nil {
63 t.Fatal(err)
64 }
65 if hex.EncodeToString(aad) != v.AAD {
66 t.Fatal("authenticated field encoding differs from independent vector")
67 }
68 got, err := sealWith(eph, nonce, k.PublicKey(), v.Binding, []byte(v.Plaintext))
69 if err != nil {
70 t.Fatal(err)
71 }
72 if got != v.Envelope {
73 t.Fatal("ciphertext differs from independent Node/OpenSSL vector")
74 }
75 plain, err := Open(k, v.Binding, v.Envelope)
76 if err != nil || !bytes.Equal(plain, []byte(v.Plaintext)) {
77 t.Fatal("independent vector did not decrypt")
78 }
79}
80func TestEnvelopeEveryBindingField(t *testing.T) {
81 v, k, _, _ := fixture(t)
82 mutations := map[string]func(*Binding){
83 "version": func(b *Binding) { b.Version++ },
84 "direction": func(b *Binding) { b.Direction = DirectionResult },
85 "account": func(b *Binding) { b.Account = "other" },
86 "app": func(b *Binding) { b.App = "docs" },
87 "host": func(b *Binding) { b.Host = "other.home.example" },
88 "request": func(b *Binding) { b.RequestID = "request-0002" },
89 "attempt": func(b *Binding) { b.AttemptID = "attempt-0002" },
90 "recipient": func(b *Binding) { b.Recipient = strings.Repeat("0", 64) },
91 }
92 for name, mutate := range mutations {
93 t.Run(name, func(t *testing.T) { b := v.Binding; mutate(&b); refused(t, k, b, v.Envelope) })
94 }
95 otherRaw := make([]byte, 32)
96 otherRaw[31] = 3
97 other, err := ecdh.P256().NewPrivateKey(otherRaw)
98 if err != nil {
99 t.Fatal(err)
100 }
101 b := v.Binding
102 b.Recipient, _ = RecipientFingerprint(other.PublicKey())
103 refused(t, other, b, v.Envelope)
104}
105func TestEnvelopeLengthPrefixesPreventFieldConcatenation(t *testing.T) {
106 v, k, _, _ := fixture(t)
107 b := v.Binding
108 b.Account = "ab"
109 b.App = "c"
110 sealed, err := Seal(k.PublicKey(), b, []byte(v.Plaintext))
111 if err != nil {
112 t.Fatal(err)
113 }
114 b.Account = "a"
115 b.App = "bc"
116 refused(t, k, b, sealed)
117}
118func TestEnvelopeDirectionsAndFreshness(t *testing.T) {
119 v, k, _, _ := fixture(t)
120 for _, direction := range []Direction{DirectionInput, DirectionResult, DirectionEnvironment} {
121 t.Run(string(direction), func(t *testing.T) {
122 b := v.Binding
123 b.Direction = direction
124 first, err := Seal(k.PublicKey(), b, []byte(v.Plaintext))
125 if err != nil {
126 t.Fatal(err)
127 }
128 second, err := Seal(k.PublicKey(), b, []byte(v.Plaintext))
129 if err != nil {
130 t.Fatal(err)
131 }
132 if first == second {
133 t.Fatal("production sealing reused its random envelope")
134 }
135 plain, err := Open(k, b, first)
136 if err != nil || string(plain) != v.Plaintext {
137 t.Fatal("round trip failed")
138 }
139 for _, other := range []Direction{DirectionInput, DirectionResult, DirectionEnvironment} {
140 if other != direction {
141 changed := b
142 changed.Direction = other
143 refused(t, k, changed, first)
144 }
145 }
146 })
147 }
148}
149func TestEnvelopeRejectsEveryTamperedByteAndNoncanonicalInput(t *testing.T) {
150 v, k, _, _ := fixture(t)
151 raw, err := base64.StdEncoding.DecodeString(v.Envelope)
152 if err != nil {
153 t.Fatal(err)
154 }
155 for i := range raw {
156 changed := bytes.Clone(raw)
157 changed[i] ^= 1
158 refused(t, k, v.Binding, base64.StdEncoding.EncodeToString(changed))
159 }
160 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))} {
161 refused(t, k, v.Binding, bad)
162 }
163 for i := 0; i < len(raw); i++ {
164 refused(t, k, v.Binding, base64.StdEncoding.EncodeToString(raw[:i]))
165 }
166}
167func TestEnvelopeLimitsAndInvalidConfiguration(t *testing.T) {
168 v, k, eph, nonce := fixture(t)
169 for _, size := range []int{0, MaxEnvelopePlaintext} {
170 plain := bytes.Repeat([]byte{0x42}, size)
171 sealed, err := Seal(k.PublicKey(), v.Binding, plain)
172 if err != nil {
173 t.Fatal(err)
174 }
175 got, err := Open(k, v.Binding, sealed)
176 if err != nil || !bytes.Equal(got, plain) {
177 t.Fatal("boundary payload failed")
178 }
179 }
180 if got, err := Seal(k.PublicKey(), v.Binding, make([]byte, MaxEnvelopePlaintext+1)); err != ErrEnvelope || got != "" {
181 t.Fatal("oversized plaintext accepted")
182 }
183 invalid := []Binding{v.Binding, v.Binding, v.Binding, v.Binding, v.Binding, v.Binding, v.Binding, v.Binding}
184 invalid[0].Account = ""
185 invalid[1].App = "space app"
186 invalid[2].Host = strings.Repeat("a", 254)
187 invalid[3].RequestID = "request\x00suffix"
188 invalid[4].AttemptID = strings.Repeat("a", 65)
189 invalid[5].Direction = "unknown"
190 invalid[6].Version = 0
191 invalid[7].Recipient = ""
192 for _, b := range invalid {
193 if got, err := Seal(k.PublicKey(), b, []byte(v.Plaintext)); err != ErrEnvelope || got != "" {
194 t.Fatal("invalid binding accepted")
195 }
196 refused(t, k, b, v.Envelope)
197 }
198 if _, err := Seal(nil, v.Binding, nil); err != ErrEnvelope {
199 t.Fatal("nil recipient accepted")
200 }
201 refused(t, nil, v.Binding, v.Envelope)
202 x, err := ecdh.X25519().NewPrivateKey(bytes.Repeat([]byte{1}, 32))
203 if err != nil {
204 t.Fatal(err)
205 }
206 if _, err := RecipientFingerprint(x.PublicKey()); err != ErrEnvelope {
207 t.Fatal("wrong curve accepted")
208 }
209 refused(t, x, v.Binding, v.Envelope)
210 for _, n := range [][]byte{nil, nonce[:len(nonce)-1], append(bytes.Clone(nonce), 0)} {
211 if _, err := sealWith(eph, n, k.PublicKey(), v.Binding, nil); err != ErrEnvelope {
212 t.Fatal("invalid nonce accepted")
213 }
214 }
215}
216func TestEnvelopeCannotCrossAppconfigPurpose(t *testing.T) {
217 v, k, _, _ := fixture(t)
218 appBinding := appconfig.Binding{Account: v.Binding.Account, App: v.Binding.App}
219 unbound, err := appconfig.SealUnbound(k.PublicKey(), []byte(v.Plaintext))
220 if err != nil {
221 t.Fatal(err)
222 }
223 bound, err := appconfig.SealBound(k.PublicKey(), appBinding, []byte(v.Plaintext))
224 if err != nil {
225 t.Fatal(err)
226 }
227 for _, old := range []string{unbound, bound} {
228 refused(t, k, v.Binding, old)
229 oldRaw, err := base64.StdEncoding.DecodeString(old)
230 if err != nil {
231 t.Fatal(err)
232 }
233 refused(t, k, v.Binding, base64.StdEncoding.EncodeToString(append([]byte(envelopePrefix), oldRaw...)))
234 }
235 newRaw, err := base64.StdEncoding.DecodeString(v.Envelope)
236 if err != nil {
237 t.Fatal(err)
238 }
239 if got, err := appconfig.Open(k, appBinding, base64.StdEncoding.EncodeToString(newRaw[len(envelopePrefix):])); err == nil || got != nil {
240 t.Fatal("maintenance ciphertext opened as appconfig secret")
241 }
242}
243