carlos / contra Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Tests against vectors/contra-v1.json — the cross-language crypto contract.
2//
3// The rule this file exists to enforce: Go, JavaScript and Swift must produce
4// identical bytes for every derivation in PROTOCOL.md, and the vector file is
5// what pins them. It is the reference; this package is not. When a value here
6// disagrees with the file, this package is wrong until somebody argues
7// otherwise on the record.
8//
9// The vectors were generated by vectors/gen/gen.c against OpenSSL — an
10// implementation nobody in this repo wrote — precisely so that passing means
11// two independent implementations agree, rather than one implementation
12// agreeing with its own output. If you ever find yourself regenerating the
13// file to make a test pass, stop: that is the failure mode the arrangement is
14// built to prevent.
15//
16// The JS and Swift suites must consume this same file. A second
17// implementation that passes its own vectors has proved nothing.
18package crypto
19
20import (
21 "bytes"
22 "crypto/ecdh"
23 "encoding/hex"
24 "encoding/json"
25 "os"
26 "path/filepath"
27 "strings"
28 "testing"
29)
30
31// ecdhReject is the range check on its own, so a test can assert that a
32// candidate really is out of [1, n-1] rather than inferring it.
33func ecdhReject(candidate []byte) (*ecdh.PrivateKey, error) {
34 return ecdh.P256().NewPrivateKey(candidate)
35}
36
37// confuse renders a code the way aged paper and OCR hand it back: lowercase,
38// zeros read as O, ones read as I and L. Normalisation has to undo all of it.
39func confuse(s string) string {
40 r := strings.NewReplacer("0", "O", "1", "l")
41 return strings.ToLower(r.Replace(s))
42}
43
44type vectorFile struct {
45 Format string `json:"format"`
46 Version int `json:"version"`
47 Argon2id struct {
48 Params struct {
49 MemKiB uint32 `json:"m_kib"`
50 Time uint32 `json:"t"`
51 Lanes uint8 `json:"p"`
52 TagLen uint32 `json:"tag_len"`
53 } `json:"params"`
54 Cases []struct {
55 Name string `json:"name"`
56 Passphrase string `json:"passphrase_utf8"`
57 SaltHex string `json:"salt_hex"`
58 OutHex string `json:"out_hex"`
59 } `json:"cases"`
60 } `json:"argon2id"`
61 HKDF struct {
62 Cases []struct {
63 Name string `json:"name"`
64 Info string `json:"info"`
65 IKMHex string `json:"ikm_hex"`
66 SaltHex string `json:"salt_hex"`
67 L int `json:"l"`
68 OKMHex string `json:"okm_hex"`
69 } `json:"cases"`
70 } `json:"hkdf"`
71 KCV struct {
72 Cases []struct {
73 Name string `json:"name"`
74 KEKHex string `json:"kek_hex"`
75 KCVHex string `json:"kcv_hex"`
76 } `json:"cases"`
77 } `json:"kcv"`
78 P256 struct {
79 Cases []struct {
80 Name string `json:"name"`
81 SeedHex string `json:"seed_hex"`
82 Tried int `json:"candidates_tried"`
83 Candidates []string `json:"candidates"`
84 DHex string `json:"d_hex"`
85 PubHex string `json:"public_uncompressed_hex"`
86 } `json:"cases"`
87 } `json:"p256_from_seed"`
88 Crockford struct {
89 Alphabet string `json:"alphabet"`
90 EncodeCases []struct {
91 BytesHex string `json:"bytes_hex"`
92 Symbols string `json:"symbols"`
93 } `json:"encode_cases"`
94 NormalizeCases []struct {
95 Input string `json:"input"`
96 Normalized string `json:"normalized"`
97 Error string `json:"error"`
98 } `json:"normalize_cases"`
99 DecodeCases []struct {
100 Symbols string `json:"symbols"`
101 BytesHex string `json:"bytes_hex"`
102 Error string `json:"error"`
103 } `json:"decode_cases"`
104 } `json:"crockford"`
105 PrintedCode struct {
106 Cases []struct {
107 Name string `json:"name"`
108 Tag string `json:"tag"`
109 PayloadHex string `json:"payload_hex"`
110 Checksum20 uint32 `json:"checksum20"`
111 Normalized string `json:"normalized"`
112 Printed string `json:"printed"`
113 } `json:"cases"`
114 Repair struct {
115 Correct string `json:"correct_normalized"`
116 Typo string `json:"typo_normalized"`
117 Position int `json:"typo_position"`
118 Candidates []string `json:"candidates"`
119 Count int `json:"candidate_count"`
120 } `json:"repair"`
121 } `json:"printed_code"`
122 AEAD struct {
123 AccountIDHex string `json:"account_id_hex"`
124 VaultIDHex string `json:"vault_id_hex"`
125 RKHex string `json:"rk_hex"`
126 Wraps []struct {
127 Name string `json:"name"`
128 ObjectType string `json:"object_type"`
129 KEKHex string `json:"kek_hex"`
130 KCVHex string `json:"kcv_hex"`
131 WrapIDHex string `json:"wrap_id_hex"`
132 Version uint64 `json:"version"`
133 NonceHex string `json:"nonce_hex"`
134 AADHex string `json:"aad_hex"`
135 CTHex string `json:"ciphertext_hex"`
136 TagHex string `json:"tag_hex"`
137 } `json:"wraps"`
138 Item struct {
139 VaultIDHex string `json:"vault_id_hex"`
140 ObjectType string `json:"object_type"`
141 ItemIDHex string `json:"item_id_hex"`
142 Version uint64 `json:"version"`
143 KVaultHex string `json:"k_vault_hex"`
144 NonceHex string `json:"nonce_hex"`
145 AADHex string `json:"aad_hex"`
146 PlainHex string `json:"plaintext_hex"`
147 CTHex string `json:"ciphertext_hex"`
148 TagHex string `json:"tag_hex"`
149 } `json:"item"`
150 } `json:"aead"`
151 Handoff struct {
152 RecipientSeedHex string `json:"recipient_seed_hex"`
153 RecipientScalarHex string `json:"recipient_scalar_hex"`
154 RecipientPubHex string `json:"recipient_public_hex"`
155 EphemeralSeedHex string `json:"ephemeral_seed_hex"`
156 EphemeralScalarHex string `json:"ephemeral_scalar_hex"`
157 EphemeralPubHex string `json:"ephemeral_public_hex"`
158 SharedHex string `json:"shared_secret_hex"`
159 KeyHex string `json:"key_hex"`
160 AccountIDHex string `json:"account_id_hex"`
161 ObjectType string `json:"object_type"`
162 Version uint64 `json:"version"`
163 NonceHex string `json:"nonce_hex"`
164 AADHex string `json:"aad_hex"`
165 PlainHex string `json:"plaintext_hex"`
166 CTHex string `json:"ciphertext_hex"`
167 TagHex string `json:"tag_hex"`
168 } `json:"handoff"`
169 AccountChain struct {
170 Passphrase string `json:"passphrase_utf8"`
171 SaltPWHex string `json:"salt_pw_hex"`
172 AccountKeyHex string `json:"account_key_hex"`
173 RecoveryHex string `json:"recovery_code_hex"`
174 PRFOutHex string `json:"prf_output_hex"`
175 CredSaltHex string `json:"cred_salt_hex"`
176 SPWHex string `json:"s_pw_hex"`
177 SeedLoginHex string `json:"seed_login_hex"`
178 LKPWPubHex string `json:"lk_pw_public_hex"`
179 SeedRCLoginHex string `json:"seed_rc_login_hex"`
180 LKRCPubHex string `json:"lk_rc_public_hex"`
181 KEKPWHex string `json:"kek_pw_hex"`
182 KEKRCHex string `json:"kek_rc_hex"`
183 KEKPRFHex string `json:"kek_prf_hex"`
184 RKHex string `json:"rk_hex"`
185 } `json:"account_chain"`
186}
187
188func unhex(t *testing.T, s string) []byte {
189 t.Helper()
190 b, err := hex.DecodeString(s)
191 if err != nil {
192 t.Fatalf("bad hex %q: %v", s, err)
193 }
194 return b
195}
196
197func eq(t *testing.T, what string, got []byte, wantHex string) {
198 t.Helper()
199 if g := hex.EncodeToString(got); g != wantHex {
200 t.Errorf("%s\n got %s\n want %s", what, g, wantHex)
201 }
202}
203
204func load(t *testing.T) *vectorFile {
205 t.Helper()
206 path := filepath.Join("..", "..", "..", "vectors", "contra-v1.json")
207 raw, err := os.ReadFile(path)
208 if err != nil {
209 t.Fatalf("read %s: %v", path, err)
210 }
211 var v vectorFile
212 if err := json.Unmarshal(raw, &v); err != nil {
213 t.Fatalf("parse %s: %v", path, err)
214 }
215 if v.Format != "contra-golden-vectors" || v.Version != 1 {
216 t.Fatalf("unexpected vector file %q v%d", v.Format, v.Version)
217 }
218 return &v
219}
220
221// The parameters in the file are the protocol's, so the constants have to
222// match them rather than the other way round.
223func TestArgon2Params(t *testing.T) {
224 v := load(t)
225 p := v.Argon2id.Params
226 if p.MemKiB != Argon2MemoryKiB || p.Time != Argon2Time ||
227 p.Lanes != Argon2Lanes || p.TagLen != Argon2TagLen {
228 t.Fatalf("parameters drifted from the vectors: file m=%d t=%d p=%d len=%d, code m=%d t=%d p=%d len=%d",
229 p.MemKiB, p.Time, p.Lanes, p.TagLen,
230 Argon2MemoryKiB, Argon2Time, Argon2Lanes, Argon2TagLen)
231 }
232}
233
234func TestArgon2id(t *testing.T) {
235 v := load(t)
236 for _, c := range v.Argon2id.Cases {
237 t.Run(c.Name, func(t *testing.T) {
238 eq(t, "S_pw", StretchPassphrase(c.Passphrase, unhex(t, c.SaltHex)), c.OutHex)
239 })
240 }
241}
242
243func TestHKDFLabels(t *testing.T) {
244 v := load(t)
245 if len(v.HKDF.Cases) == 0 {
246 t.Fatal("no HKDF cases")
247 }
248 seen := map[string]bool{}
249 for _, c := range v.HKDF.Cases {
250 t.Run(c.Name, func(t *testing.T) {
251 var salt []byte
252 if c.SaltHex != "" {
253 salt = unhex(t, c.SaltHex)
254 }
255 got, err := Derive(unhex(t, c.IKMHex), salt, c.Info, c.L)
256 if err != nil {
257 t.Fatalf("Derive: %v", err)
258 }
259 eq(t, c.Info, got, c.OKMHex)
260 })
261 seen[c.Info] = true
262 }
263 // PROTOCOL.md names five derivation labels. A label that loses its vector
264 // is a label nothing pins.
265 for _, l := range []string{LabelLogin, LabelKEK, LabelRCLogin, LabelRCKEK, LabelPRFKEK} {
266 if !seen[l] {
267 t.Errorf("no vector covers HKDF label %q", l)
268 }
269 }
270}
271
272func TestKCV(t *testing.T) {
273 v := load(t)
274 for _, c := range v.KCV.Cases {
275 t.Run(c.Name, func(t *testing.T) {
276 eq(t, "KCV", KCV(unhex(t, c.KEKHex)), c.KCVHex)
277 })
278 }
279}
280
281func TestP256FromSeed(t *testing.T) {
282 v := load(t)
283 sawRejection := false
284 for _, c := range v.P256.Cases {
285 t.Run(c.Name, func(t *testing.T) {
286 key, err := DeriveP256(unhex(t, c.SeedHex))
287 if err != nil {
288 t.Fatalf("DeriveP256: %v", err)
289 }
290 eq(t, "d", key.Bytes(), c.DHex)
291 eq(t, "public key", key.PublicKey().Bytes(), c.PubHex)
292 })
293 if c.Tried > 1 {
294 sawRejection = true
295 }
296 }
297 // The whole reason a seed was searched for: without a case that rejects,
298 // the loop past i=0 is dead code in every implementation that copies this.
299 if !sawRejection {
300 t.Error("no vector exercises the rejection branch")
301 }
302}
303
304// The rejecting seed is worth its own test, because it is the one input in the
305// file that cost real compute to find and is trivially lost in a regeneration.
306func TestP256RejectionBranch(t *testing.T) {
307 v := load(t)
308 for _, c := range v.P256.Cases {
309 if c.Tried < 2 {
310 continue
311 }
312 seed := unhex(t, c.SeedHex)
313 // Candidate 0 must be out of range: ecdh rejects it.
314 if _, err := DeriveP256(seed); err != nil {
315 t.Fatalf("rejecting seed did not derive: %v", err)
316 }
317 first, err := Derive(seed, nil, LabelECP256+"\x00", 32)
318 if err != nil {
319 t.Fatal(err)
320 }
321 eq(t, "candidate 0", first, c.Candidates[0])
322 if _, err := ecdhReject(first); err == nil {
323 t.Error("candidate 0 was accepted; this seed no longer forces a rejection")
324 }
325 return
326 }
327 t.Skip("no rejecting seed in the vector file")
328}
329
330func TestCrockfordAlphabet(t *testing.T) {
331 v := load(t)
332 if v.Crockford.Alphabet != Alphabet {
333 t.Fatalf("alphabet drift\n got %s\n want %s", Alphabet, v.Crockford.Alphabet)
334 }
335}
336
337func TestCrockfordEncode(t *testing.T) {
338 v := load(t)
339 for _, c := range v.Crockford.EncodeCases {
340 if got := Base32Encode(unhex(t, c.BytesHex)); got != c.Symbols {
341 t.Errorf("encode %s\n got %s\n want %s", c.BytesHex, got, c.Symbols)
342 }
343 }
344}
345
346func TestCrockfordNormalize(t *testing.T) {
347 v := load(t)
348 for _, c := range v.Crockford.NormalizeCases {
349 got, err := Normalize(c.Input)
350 if c.Error != "" {
351 if err == nil {
352 t.Errorf("normalize %q: expected an error (%s), got %q", c.Input, c.Error, got)
353 }
354 continue
355 }
356 if err != nil {
357 t.Errorf("normalize %q: %v", c.Input, err)
358 continue
359 }
360 if got != c.Normalized {
361 t.Errorf("normalize %q\n got %s\n want %s", c.Input, got, c.Normalized)
362 }
363 }
364}
365
366func TestCrockfordDecode(t *testing.T) {
367 v := load(t)
368 for _, c := range v.Crockford.DecodeCases {
369 got, err := Base32Decode(c.Symbols, 16)
370 if c.Error != "" {
371 if err == nil {
372 t.Errorf("decode %q: expected an error (%s)", c.Symbols, c.Error)
373 }
374 continue
375 }
376 if err != nil {
377 t.Errorf("decode %q: %v", c.Symbols, err)
378 continue
379 }
380 eq(t, "decode "+c.Symbols, got, c.BytesHex)
381 }
382}
383
384func TestPrintedCode(t *testing.T) {
385 v := load(t)
386 for _, c := range v.PrintedCode.Cases {
387 t.Run(c.Name, func(t *testing.T) {
388 payload := unhex(t, c.PayloadHex)
389 if got := Checksum20(payload); got != c.Checksum20 {
390 t.Errorf("checksum20 got %d want %d", got, c.Checksum20)
391 }
392 if got := NormalizedCode(c.Tag, payload); got != c.Normalized {
393 t.Errorf("normalized\n got %s\n want %s", got, c.Normalized)
394 }
395 if got := FormatCode(c.Tag, payload); got != c.Printed {
396 t.Errorf("printed\n got %s\n want %s", got, c.Printed)
397 }
398 // Round trip through the printed form, hyphens and all.
399 back, err := ParseCode(c.Tag, c.Printed)
400 if err != nil {
401 t.Fatalf("ParseCode: %v", err)
402 }
403 eq(t, "round trip", back, c.PayloadHex)
404 // And through a lowercase, O/I/L-confused rendering of it, which
405 // is what aged paper and OCR actually hand us.
406 confused := confuse(c.Printed)
407 back, err = ParseCode(c.Tag, confused)
408 if err != nil {
409 t.Fatalf("ParseCode(%q): %v", confused, err)
410 }
411 eq(t, "confused round trip", back, c.PayloadHex)
412 })
413 }
414}
415
416func TestRepair(t *testing.T) {
417 v := load(t)
418 r := v.PrintedCode.Repair
419 if r.Typo == "" {
420 t.Skip("no repair case in the vector file")
421 }
422 if _, err := ParseCode(TagRecovery, r.Typo); err == nil {
423 t.Fatal("the typo case validates, so it is not testing repair")
424 }
425 got, err := RepairCode(TagRecovery, r.Typo)
426 if err != nil {
427 t.Fatalf("RepairCode: %v", err)
428 }
429 if len(got) != r.Count {
430 t.Errorf("candidate count got %d want %d (%v)", len(got), r.Count, got)
431 }
432 for i := range r.Candidates {
433 if i >= len(got) || got[i] != r.Candidates[i] {
434 t.Errorf("candidate %d\n got %v\n want %v", i, got, r.Candidates)
435 break
436 }
437 }
438 found := false
439 for _, c := range got {
440 if c == r.Correct {
441 found = true
442 }
443 }
444 if !found {
445 t.Errorf("repair did not recover the correct code %s", r.Correct)
446 }
447}
448
449func TestAEADWraps(t *testing.T) {
450 v := load(t)
451 accountID := unhex(t, v.AEAD.AccountIDHex)
452 rk := unhex(t, v.AEAD.RKHex)
453
454 for _, w := range v.AEAD.Wraps {
455 t.Run(w.Name, func(t *testing.T) {
456 aad := AAD(accountID, w.ObjectType, unhex(t, w.WrapIDHex), w.Version)
457 eq(t, "AAD", aad, w.AADHex)
458 eq(t, "KCV", KCV(unhex(t, w.KEKHex)), w.KCVHex)
459
460 sealed, err := Seal(unhex(t, w.KEKHex), unhex(t, w.NonceHex), aad, rk)
461 if err != nil {
462 t.Fatalf("Seal: %v", err)
463 }
464 eq(t, "ciphertext", sealed[:len(sealed)-16], w.CTHex)
465 eq(t, "tag", sealed[len(sealed)-16:], w.TagHex)
466
467 opened, err := Open(unhex(t, w.KEKHex), unhex(t, w.NonceHex), aad, sealed)
468 if err != nil {
469 t.Fatalf("Open: %v", err)
470 }
471 if !bytes.Equal(opened, rk) {
472 t.Error("Open did not return RK")
473 }
474 })
475 }
476}
477
478func TestAEADItem(t *testing.T) {
479 v := load(t)
480 it := v.AEAD.Item
481 aad := AAD(unhex(t, it.VaultIDHex), it.ObjectType, unhex(t, it.ItemIDHex), it.Version)
482 eq(t, "item AAD", aad, it.AADHex)
483
484 sealed, err := Seal(unhex(t, it.KVaultHex), unhex(t, it.NonceHex), aad, unhex(t, it.PlainHex))
485 if err != nil {
486 t.Fatalf("Seal: %v", err)
487 }
488 eq(t, "item ciphertext", sealed[:len(sealed)-16], it.CTHex)
489 eq(t, "item tag", sealed[len(sealed)-16:], it.TagHex)
490}
491
492// The handoff is the one place in the protocol where a secret travels through
493// the server, so this walks the whole chain against the generator: ECDH from
494// both sides, the derived key, the AAD, and the sealed bytes.
495func TestHandoffChain(t *testing.T) {
496 v := load(t)
497 h := v.Handoff
498
499 // The keypairs are derived, so the seeds prove the vector's own inputs
500 // were not quietly chosen to fit an answer.
501 for _, c := range []struct{ name, seed, scalar, pub string }{
502 {"recipient", h.RecipientSeedHex, h.RecipientScalarHex, h.RecipientPubHex},
503 {"ephemeral", h.EphemeralSeedHex, h.EphemeralScalarHex, h.EphemeralPubHex},
504 } {
505 key, err := DeriveP256(unhex(t, c.seed))
506 if err != nil {
507 t.Fatalf("%s DeriveP256: %v", c.name, err)
508 }
509 eq(t, c.name+" scalar", key.Bytes(), c.scalar)
510 eq(t, c.name+" public", key.PublicKey().Bytes(), c.pub)
511 }
512
513 // Both directions, because a one-sided check would pass on a primitive
514 // that is not actually Diffie-Hellman.
515 fwd, err := ECDH(unhex(t, h.EphemeralScalarHex), unhex(t, h.RecipientPubHex))
516 if err != nil {
517 t.Fatalf("ECDH sender: %v", err)
518 }
519 eq(t, "shared secret (sender side)", fwd, h.SharedHex)
520
521 back, err := ECDH(unhex(t, h.RecipientScalarHex), unhex(t, h.EphemeralPubHex))
522 if err != nil {
523 t.Fatalf("ECDH recipient: %v", err)
524 }
525 eq(t, "shared secret (recipient side)", back, h.SharedHex)
526
527 key, err := ECIESKey(unhex(t, h.EphemeralPubHex), unhex(t, h.RecipientPubHex), fwd)
528 if err != nil {
529 t.Fatalf("ECIESKey: %v", err)
530 }
531 eq(t, "handoff key", key, h.KeyHex)
532
533 aad := AAD(unhex(t, h.AccountIDHex), h.ObjectType, unhex(t, h.RecipientPubHex), h.Version)
534 eq(t, "handoff AAD", aad, h.AADHex)
535
536 sealed, err := Seal(key, unhex(t, h.NonceHex), aad, unhex(t, h.PlainHex))
537 if err != nil {
538 t.Fatalf("Seal: %v", err)
539 }
540 eq(t, "handoff ciphertext", sealed[:len(sealed)-16], h.CTHex)
541 eq(t, "handoff tag", sealed[len(sealed)-16:], h.TagHex)
542}
543
544// SealTo mints its own ephemeral key and nonce, so it cannot be checked
545// against fixed bytes. What can be checked is the property that matters: the
546// envelope opens for the device it was sealed to, and for nobody else.
547func TestSealToRoundTripsAndIsBoundToOneDevice(t *testing.T) {
548 v := load(t)
549 h := v.Handoff
550 recipientPub := unhex(t, h.RecipientPubHex)
551 recipientScalar := unhex(t, h.RecipientScalarHex)
552 accountKey := unhex(t, h.PlainHex)
553 aad := AAD(unhex(t, h.AccountIDHex), ObjectHandoff, recipientPub, 1)
554
555 envelope, err := SealTo(recipientPub, aad, accountKey)
556 if err != nil {
557 t.Fatalf("SealTo: %v", err)
558 }
559 if len(envelope) != PublicKeyLen+NonceLen+len(accountKey)+16 {
560 t.Fatalf("envelope is %d bytes, want %d",
561 len(envelope), PublicKeyLen+NonceLen+len(accountKey)+16)
562 }
563
564 got, err := OpenFrom(recipientScalar, recipientPub, aad, envelope)
565 if err != nil {
566 t.Fatalf("OpenFrom: %v", err)
567 }
568 eq(t, "handoff round trip", got, h.PlainHex)
569
570 // A second device holds a different scalar. Without this the envelope
571 // would be "sealed" in name only.
572 other, err := DeriveP256(unhex(t, h.EphemeralSeedHex))
573 if err != nil {
574 t.Fatalf("DeriveP256: %v", err)
575 }
576 if _, err := OpenFrom(other.Bytes(), other.PublicKey().Bytes(), aad, envelope); err == nil {
577 t.Error("a different device opened an envelope that was not sealed to it")
578 }
579
580 // And the AAD names the recipient, so relaying the same envelope to a
581 // second device fails even if that device's key were somehow the same.
582 wrong := AAD(unhex(t, h.AccountIDHex), ObjectHandoff, other.PublicKey().Bytes(), 1)
583 if _, err := OpenFrom(recipientScalar, recipientPub, wrong, envelope); err == nil {
584 t.Error("an envelope opened under an AAD naming a different device")
585 }
586}
587
588// The binding is only worth anything if breaking it breaks decryption. This is
589// the transplant a malicious server would attempt: same key, same nonce, same
590// bytes, a different slot.
591func TestAADBindingRejectsTransplant(t *testing.T) {
592 v := load(t)
593 it := v.AEAD.Item
594 key, nonce := unhex(t, it.KVaultHex), unhex(t, it.NonceHex)
595 good := AAD(unhex(t, it.VaultIDHex), it.ObjectType, unhex(t, it.ItemIDHex), it.Version)
596
597 sealed, err := Seal(key, nonce, good, unhex(t, it.PlainHex))
598 if err != nil {
599 t.Fatal(err)
600 }
601
602 otherItem := append([]byte(nil), unhex(t, it.ItemIDHex)...)
603 otherItem[0] ^= 0x01
604 otherVault := append([]byte(nil), unhex(t, it.VaultIDHex)...)
605 otherVault[0] ^= 0x01
606
607 for _, bad := range []struct {
608 name string
609 aad []byte
610 }{
611 {"different item", AAD(unhex(t, it.VaultIDHex), it.ObjectType, otherItem, it.Version)},
612 {"different vault", AAD(otherVault, it.ObjectType, unhex(t, it.ItemIDHex), it.Version)},
613 {"different version", AAD(unhex(t, it.VaultIDHex), it.ObjectType, unhex(t, it.ItemIDHex), it.Version+1)},
614 {"different type", AAD(unhex(t, it.VaultIDHex), ObjectWrapPW, unhex(t, it.ItemIDHex), it.Version)},
615 } {
616 if _, err := Open(key, nonce, bad.aad, sealed); err == nil {
617 t.Errorf("%s: transplant succeeded — the AAD binding is not holding", bad.name)
618 }
619 }
620}
621
622// Length prefixes exist so two different tuples cannot produce one AAD. This
623// is the collision that unprefixed concatenation would allow.
624func TestAADIsInjective(t *testing.T) {
625 id := bytes.Repeat([]byte{0xab}, 16)
626 a := AAD(id, "wrap.p", append(append([]byte(nil), 'w'), id...), 1)
627 b := AAD(id, "wrap.pw", id, 1)
628 if bytes.Equal(a, b) {
629 t.Fatal("two different tuples produced the same AAD")
630 }
631}
632
633// The whole of PROTOCOL.md's "The shape", re-derived from the printed inputs.
634// It is what a new implementation bisects against when it diverges somewhere
635// in the middle.
636func TestAccountChain(t *testing.T) {
637 v := load(t)
638 c := v.AccountChain
639
640 sPW := StretchPassphrase(c.Passphrase, unhex(t, c.SaltPWHex))
641 eq(t, "S_pw", sPW, c.SPWHex)
642
643 seedLogin, err := Derive(sPW, nil, LabelLogin, 32)
644 if err != nil {
645 t.Fatal(err)
646 }
647 eq(t, "seed_login", seedLogin, c.SeedLoginHex)
648
649 lkPW, err := DeriveP256(seedLogin)
650 if err != nil {
651 t.Fatal(err)
652 }
653 eq(t, "LK_pw public", lkPW.PublicKey().Bytes(), c.LKPWPubHex)
654
655 kekPW, err := Derive(sPW, unhex(t, c.AccountKeyHex), LabelKEK, 32)
656 if err != nil {
657 t.Fatal(err)
658 }
659 eq(t, "KEK_pw", kekPW, c.KEKPWHex)
660
661 seedRC, err := Derive(unhex(t, c.RecoveryHex), nil, LabelRCLogin, 32)
662 if err != nil {
663 t.Fatal(err)
664 }
665 eq(t, "seed_rc_login", seedRC, c.SeedRCLoginHex)
666
667 lkRC, err := DeriveP256(seedRC)
668 if err != nil {
669 t.Fatal(err)
670 }
671 eq(t, "LK_rc public", lkRC.PublicKey().Bytes(), c.LKRCPubHex)
672
673 kekRC, err := Derive(unhex(t, c.RecoveryHex), nil, LabelRCKEK, 32)
674 if err != nil {
675 t.Fatal(err)
676 }
677 eq(t, "KEK_rc", kekRC, c.KEKRCHex)
678
679 kekPRF, err := Derive(unhex(t, c.PRFOutHex), unhex(t, c.CredSaltHex), LabelPRFKEK, 32)
680 if err != nil {
681 t.Fatal(err)
682 }
683 eq(t, "KEK_prf", kekPRF, c.KEKPRFHex)
684
685 // The three wraps are peers: each opens RK on its own, and none is
686 // derivable from another.
687 rk := unhex(t, c.RKHex)
688 for _, w := range v.AEAD.Wraps {
689 aad := AAD(unhex(t, v.AEAD.AccountIDHex), w.ObjectType, unhex(t, w.WrapIDHex), w.Version)
690 sealed := append(unhex(t, w.CTHex), unhex(t, w.TagHex)...)
691 got, err := Open(unhex(t, w.KEKHex), unhex(t, w.NonceHex), aad, sealed)
692 if err != nil {
693 t.Errorf("%s did not open: %v", w.Name, err)
694 continue
695 }
696 if !bytes.Equal(got, rk) {
697 t.Errorf("%s opened to something other than RK", w.Name)
698 }
699 }
700}
701
702// The KEK branches must be independent: knowing one says nothing about
703// another. This cannot prove HKDF's PRF property, but it does catch the
704// copy-paste that gives two labels the same output.
705func TestBranchesAreDistinct(t *testing.T) {
706 v := load(t)
707 seen := map[string]string{}
708 for _, c := range v.HKDF.Cases {
709 if prev, ok := seen[c.OKMHex]; ok {
710 t.Errorf("labels %q and %q derive the same key", prev, c.Info)
711 }
712 seen[c.OKMHex] = c.Info
713 }
714}
715