| new file mode 100644 |
| index 0000000..748fb15 |
| --- /dev/null |
| +++ b/internal/crypto/vectors_test.go |
| @@ -0,0 +1,600 @@ |
| +// Tests against vectors/contra-v1.json — the cross-language crypto contract. |
| +// |
| +// The rule this file exists to enforce: Go, JavaScript and Swift must produce |
| +// identical bytes for every derivation in DESIGN.md, and the vector file is |
| +// what pins them. It is the reference; this package is not. When a value here |
| +// disagrees with the file, this package is wrong until somebody argues |
| +// otherwise on the record. |
| +// |
| +// The vectors were generated by vectors/gen/gen.c against OpenSSL — an |
| +// implementation nobody in this repo wrote — precisely so that passing means |
| +// two independent implementations agree, rather than one implementation |
| +// agreeing with its own output. If you ever find yourself regenerating the |
| +// file to make a test pass, stop: that is the failure mode the arrangement is |
| +// built to prevent. |
| +// |
| +// The JS and Swift suites must consume this same file. A second |
| +// implementation that passes its own vectors has proved nothing. |
| +package crypto |
| + |
| +import ( |
| + "bytes" |
| + "crypto/ecdh" |
| + "encoding/hex" |
| + "encoding/json" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// ecdhReject is the range check on its own, so a test can assert that a |
| +// candidate really is out of [1, n-1] rather than inferring it. |
| +func ecdhReject(candidate []byte) (*ecdh.PrivateKey, error) { |
| + return ecdh.P256().NewPrivateKey(candidate) |
| +} |
| + |
| +// confuse renders a code the way aged paper and OCR hand it back: lowercase, |
| +// zeros read as O, ones read as I and L. Normalisation has to undo all of it. |
| +func confuse(s string) string { |
| + r := strings.NewReplacer("0", "O", "1", "l") |
| + return strings.ToLower(r.Replace(s)) |
| +} |
| + |
| +type vectorFile struct { |
| + Format string `json:"format"` |
| + Version int `json:"version"` |
| + Argon2id struct { |
| + Params struct { |
| + MemKiB uint32 `json:"m_kib"` |
| + Time uint32 `json:"t"` |
| + Lanes uint8 `json:"p"` |
| + TagLen uint32 `json:"tag_len"` |
| + } `json:"params"` |
| + Cases []struct { |
| + Name string `json:"name"` |
| + Passphrase string `json:"passphrase_utf8"` |
| + SaltHex string `json:"salt_hex"` |
| + OutHex string `json:"out_hex"` |
| + } `json:"cases"` |
| + } `json:"argon2id"` |
| + HKDF struct { |
| + Cases []struct { |
| + Name string `json:"name"` |
| + Info string `json:"info"` |
| + IKMHex string `json:"ikm_hex"` |
| + SaltHex string `json:"salt_hex"` |
| + L int `json:"l"` |
| + OKMHex string `json:"okm_hex"` |
| + } `json:"cases"` |
| + } `json:"hkdf"` |
| + KCV struct { |
| + Cases []struct { |
| + Name string `json:"name"` |
| + KEKHex string `json:"kek_hex"` |
| + KCVHex string `json:"kcv_hex"` |
| + } `json:"cases"` |
| + } `json:"kcv"` |
| + P256 struct { |
| + Cases []struct { |
| + Name string `json:"name"` |
| + SeedHex string `json:"seed_hex"` |
| + Tried int `json:"candidates_tried"` |
| + Candidates []string `json:"candidates"` |
| + DHex string `json:"d_hex"` |
| + PubHex string `json:"public_uncompressed_hex"` |
| + } `json:"cases"` |
| + } `json:"p256_from_seed"` |
| + Crockford struct { |
| + Alphabet string `json:"alphabet"` |
| + EncodeCases []struct { |
| + BytesHex string `json:"bytes_hex"` |
| + Symbols string `json:"symbols"` |
| + } `json:"encode_cases"` |
| + NormalizeCases []struct { |
| + Input string `json:"input"` |
| + Normalized string `json:"normalized"` |
| + Error string `json:"error"` |
| + } `json:"normalize_cases"` |
| + DecodeCases []struct { |
| + Symbols string `json:"symbols"` |
| + BytesHex string `json:"bytes_hex"` |
| + Error string `json:"error"` |
| + } `json:"decode_cases"` |
| + } `json:"crockford"` |
| + PrintedCode struct { |
| + Cases []struct { |
| + Name string `json:"name"` |
| + Tag string `json:"tag"` |
| + PayloadHex string `json:"payload_hex"` |
| + Checksum20 uint32 `json:"checksum20"` |
| + Normalized string `json:"normalized"` |
| + Printed string `json:"printed"` |
| + } `json:"cases"` |
| + Repair struct { |
| + Correct string `json:"correct_normalized"` |
| + Typo string `json:"typo_normalized"` |
| + Position int `json:"typo_position"` |
| + Candidates []string `json:"candidates"` |
| + Count int `json:"candidate_count"` |
| + } `json:"repair"` |
| + } `json:"printed_code"` |
| + AEAD struct { |
| + AccountIDHex string `json:"account_id_hex"` |
| + VaultIDHex string `json:"vault_id_hex"` |
| + RKHex string `json:"rk_hex"` |
| + Wraps []struct { |
| + Name string `json:"name"` |
| + ObjectType string `json:"object_type"` |
| + KEKHex string `json:"kek_hex"` |
| + KCVHex string `json:"kcv_hex"` |
| + WrapIDHex string `json:"wrap_id_hex"` |
| + Version uint64 `json:"version"` |
| + NonceHex string `json:"nonce_hex"` |
| + AADHex string `json:"aad_hex"` |
| + CTHex string `json:"ciphertext_hex"` |
| + TagHex string `json:"tag_hex"` |
| + } `json:"wraps"` |
| + Item struct { |
| + VaultIDHex string `json:"vault_id_hex"` |
| + ObjectType string `json:"object_type"` |
| + ItemIDHex string `json:"item_id_hex"` |
| + Version uint64 `json:"version"` |
| + KVaultHex string `json:"k_vault_hex"` |
| + NonceHex string `json:"nonce_hex"` |
| + AADHex string `json:"aad_hex"` |
| + PlainHex string `json:"plaintext_hex"` |
| + CTHex string `json:"ciphertext_hex"` |
| + TagHex string `json:"tag_hex"` |
| + } `json:"item"` |
| + } `json:"aead"` |
| + AccountChain struct { |
| + Passphrase string `json:"passphrase_utf8"` |
| + SaltPWHex string `json:"salt_pw_hex"` |
| + AccountKeyHex string `json:"account_key_hex"` |
| + RecoveryHex string `json:"recovery_code_hex"` |
| + PRFOutHex string `json:"prf_output_hex"` |
| + CredSaltHex string `json:"cred_salt_hex"` |
| + SPWHex string `json:"s_pw_hex"` |
| + SeedLoginHex string `json:"seed_login_hex"` |
| + LKPWPubHex string `json:"lk_pw_public_hex"` |
| + SeedRCLoginHex string `json:"seed_rc_login_hex"` |
| + LKRCPubHex string `json:"lk_rc_public_hex"` |
| + KEKPWHex string `json:"kek_pw_hex"` |
| + KEKRCHex string `json:"kek_rc_hex"` |
| + KEKPRFHex string `json:"kek_prf_hex"` |
| + RKHex string `json:"rk_hex"` |
| + } `json:"account_chain"` |
| +} |
| + |
| +func unhex(t *testing.T, s string) []byte { |
| + t.Helper() |
| + b, err := hex.DecodeString(s) |
| + if err != nil { |
| + t.Fatalf("bad hex %q: %v", s, err) |
| + } |
| + return b |
| +} |
| + |
| +func eq(t *testing.T, what string, got []byte, wantHex string) { |
| + t.Helper() |
| + if g := hex.EncodeToString(got); g != wantHex { |
| + t.Errorf("%s\n got %s\n want %s", what, g, wantHex) |
| + } |
| +} |
| + |
| +func load(t *testing.T) *vectorFile { |
| + t.Helper() |
| + path := filepath.Join("..", "..", "vectors", "contra-v1.json") |
| + raw, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatalf("read %s: %v", path, err) |
| + } |
| + var v vectorFile |
| + if err := json.Unmarshal(raw, &v); err != nil { |
| + t.Fatalf("parse %s: %v", path, err) |
| + } |
| + if v.Format != "contra-golden-vectors" || v.Version != 1 { |
| + t.Fatalf("unexpected vector file %q v%d", v.Format, v.Version) |
| + } |
| + return &v |
| +} |
| + |
| +// The parameters in the file are the protocol's, so the constants have to |
| +// match them rather than the other way round. |
| +func TestArgon2Params(t *testing.T) { |
| + v := load(t) |
| + p := v.Argon2id.Params |
| + if p.MemKiB != Argon2MemoryKiB || p.Time != Argon2Time || |
| + p.Lanes != Argon2Lanes || p.TagLen != Argon2TagLen { |
| + 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", |
| + p.MemKiB, p.Time, p.Lanes, p.TagLen, |
| + Argon2MemoryKiB, Argon2Time, Argon2Lanes, Argon2TagLen) |
| + } |
| +} |
| + |
| +func TestArgon2id(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.Argon2id.Cases { |
| + t.Run(c.Name, func(t *testing.T) { |
| + eq(t, "S_pw", StretchPassphrase(c.Passphrase, unhex(t, c.SaltHex)), c.OutHex) |
| + }) |
| + } |
| +} |
| + |
| +func TestHKDFLabels(t *testing.T) { |
| + v := load(t) |
| + if len(v.HKDF.Cases) == 0 { |
| + t.Fatal("no HKDF cases") |
| + } |
| + seen := map[string]bool{} |
| + for _, c := range v.HKDF.Cases { |
| + t.Run(c.Name, func(t *testing.T) { |
| + var salt []byte |
| + if c.SaltHex != "" { |
| + salt = unhex(t, c.SaltHex) |
| + } |
| + got, err := Derive(unhex(t, c.IKMHex), salt, c.Info, c.L) |
| + if err != nil { |
| + t.Fatalf("Derive: %v", err) |
| + } |
| + eq(t, c.Info, got, c.OKMHex) |
| + }) |
| + seen[c.Info] = true |
| + } |
| + // DESIGN.md names five derivation labels. A label that loses its vector |
| + // is a label nothing pins. |
| + for _, l := range []string{LabelLogin, LabelKEK, LabelRCLogin, LabelRCKEK, LabelPRFKEK} { |
| + if !seen[l] { |
| + t.Errorf("no vector covers HKDF label %q", l) |
| + } |
| + } |
| +} |
| + |
| +func TestKCV(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.KCV.Cases { |
| + t.Run(c.Name, func(t *testing.T) { |
| + eq(t, "KCV", KCV(unhex(t, c.KEKHex)), c.KCVHex) |
| + }) |
| + } |
| +} |
| + |
| +func TestP256FromSeed(t *testing.T) { |
| + v := load(t) |
| + sawRejection := false |
| + for _, c := range v.P256.Cases { |
| + t.Run(c.Name, func(t *testing.T) { |
| + key, err := DeriveP256(unhex(t, c.SeedHex)) |
| + if err != nil { |
| + t.Fatalf("DeriveP256: %v", err) |
| + } |
| + eq(t, "d", key.Bytes(), c.DHex) |
| + eq(t, "public key", key.PublicKey().Bytes(), c.PubHex) |
| + }) |
| + if c.Tried > 1 { |
| + sawRejection = true |
| + } |
| + } |
| + // The whole reason a seed was searched for: without a case that rejects, |
| + // the loop past i=0 is dead code in every implementation that copies this. |
| + if !sawRejection { |
| + t.Error("no vector exercises the rejection branch") |
| + } |
| +} |
| + |
| +// The rejecting seed is worth its own test, because it is the one input in the |
| +// file that cost real compute to find and is trivially lost in a regeneration. |
| +func TestP256RejectionBranch(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.P256.Cases { |
| + if c.Tried < 2 { |
| + continue |
| + } |
| + seed := unhex(t, c.SeedHex) |
| + // Candidate 0 must be out of range: ecdh rejects it. |
| + if _, err := DeriveP256(seed); err != nil { |
| + t.Fatalf("rejecting seed did not derive: %v", err) |
| + } |
| + first, err := Derive(seed, nil, LabelECP256+"\x00", 32) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "candidate 0", first, c.Candidates[0]) |
| + if _, err := ecdhReject(first); err == nil { |
| + t.Error("candidate 0 was accepted; this seed no longer forces a rejection") |
| + } |
| + return |
| + } |
| + t.Skip("no rejecting seed in the vector file") |
| +} |
| + |
| +func TestCrockfordAlphabet(t *testing.T) { |
| + v := load(t) |
| + if v.Crockford.Alphabet != Alphabet { |
| + t.Fatalf("alphabet drift\n got %s\n want %s", Alphabet, v.Crockford.Alphabet) |
| + } |
| +} |
| + |
| +func TestCrockfordEncode(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.Crockford.EncodeCases { |
| + if got := Base32Encode(unhex(t, c.BytesHex)); got != c.Symbols { |
| + t.Errorf("encode %s\n got %s\n want %s", c.BytesHex, got, c.Symbols) |
| + } |
| + } |
| +} |
| + |
| +func TestCrockfordNormalize(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.Crockford.NormalizeCases { |
| + got, err := Normalize(c.Input) |
| + if c.Error != "" { |
| + if err == nil { |
| + t.Errorf("normalize %q: expected an error (%s), got %q", c.Input, c.Error, got) |
| + } |
| + continue |
| + } |
| + if err != nil { |
| + t.Errorf("normalize %q: %v", c.Input, err) |
| + continue |
| + } |
| + if got != c.Normalized { |
| + t.Errorf("normalize %q\n got %s\n want %s", c.Input, got, c.Normalized) |
| + } |
| + } |
| +} |
| + |
| +func TestCrockfordDecode(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.Crockford.DecodeCases { |
| + got, err := Base32Decode(c.Symbols, 16) |
| + if c.Error != "" { |
| + if err == nil { |
| + t.Errorf("decode %q: expected an error (%s)", c.Symbols, c.Error) |
| + } |
| + continue |
| + } |
| + if err != nil { |
| + t.Errorf("decode %q: %v", c.Symbols, err) |
| + continue |
| + } |
| + eq(t, "decode "+c.Symbols, got, c.BytesHex) |
| + } |
| +} |
| + |
| +func TestPrintedCode(t *testing.T) { |
| + v := load(t) |
| + for _, c := range v.PrintedCode.Cases { |
| + t.Run(c.Name, func(t *testing.T) { |
| + payload := unhex(t, c.PayloadHex) |
| + if got := Checksum20(payload); got != c.Checksum20 { |
| + t.Errorf("checksum20 got %d want %d", got, c.Checksum20) |
| + } |
| + if got := NormalizedCode(c.Tag, payload); got != c.Normalized { |
| + t.Errorf("normalized\n got %s\n want %s", got, c.Normalized) |
| + } |
| + if got := FormatCode(c.Tag, payload); got != c.Printed { |
| + t.Errorf("printed\n got %s\n want %s", got, c.Printed) |
| + } |
| + // Round trip through the printed form, hyphens and all. |
| + back, err := ParseCode(c.Tag, c.Printed) |
| + if err != nil { |
| + t.Fatalf("ParseCode: %v", err) |
| + } |
| + eq(t, "round trip", back, c.PayloadHex) |
| + // And through a lowercase, O/I/L-confused rendering of it, which |
| + // is what aged paper and OCR actually hand us. |
| + confused := confuse(c.Printed) |
| + back, err = ParseCode(c.Tag, confused) |
| + if err != nil { |
| + t.Fatalf("ParseCode(%q): %v", confused, err) |
| + } |
| + eq(t, "confused round trip", back, c.PayloadHex) |
| + }) |
| + } |
| +} |
| + |
| +func TestRepair(t *testing.T) { |
| + v := load(t) |
| + r := v.PrintedCode.Repair |
| + if r.Typo == "" { |
| + t.Skip("no repair case in the vector file") |
| + } |
| + if _, err := ParseCode(TagRecovery, r.Typo); err == nil { |
| + t.Fatal("the typo case validates, so it is not testing repair") |
| + } |
| + got, err := RepairCode(TagRecovery, r.Typo) |
| + if err != nil { |
| + t.Fatalf("RepairCode: %v", err) |
| + } |
| + if len(got) != r.Count { |
| + t.Errorf("candidate count got %d want %d (%v)", len(got), r.Count, got) |
| + } |
| + for i := range r.Candidates { |
| + if i >= len(got) || got[i] != r.Candidates[i] { |
| + t.Errorf("candidate %d\n got %v\n want %v", i, got, r.Candidates) |
| + break |
| + } |
| + } |
| + found := false |
| + for _, c := range got { |
| + if c == r.Correct { |
| + found = true |
| + } |
| + } |
| + if !found { |
| + t.Errorf("repair did not recover the correct code %s", r.Correct) |
| + } |
| +} |
| + |
| +func TestAEADWraps(t *testing.T) { |
| + v := load(t) |
| + accountID := unhex(t, v.AEAD.AccountIDHex) |
| + rk := unhex(t, v.AEAD.RKHex) |
| + |
| + for _, w := range v.AEAD.Wraps { |
| + t.Run(w.Name, func(t *testing.T) { |
| + aad := AAD(accountID, w.ObjectType, unhex(t, w.WrapIDHex), w.Version) |
| + eq(t, "AAD", aad, w.AADHex) |
| + eq(t, "KCV", KCV(unhex(t, w.KEKHex)), w.KCVHex) |
| + |
| + sealed, err := Seal(unhex(t, w.KEKHex), unhex(t, w.NonceHex), aad, rk) |
| + if err != nil { |
| + t.Fatalf("Seal: %v", err) |
| + } |
| + eq(t, "ciphertext", sealed[:len(sealed)-16], w.CTHex) |
| + eq(t, "tag", sealed[len(sealed)-16:], w.TagHex) |
| + |
| + opened, err := Open(unhex(t, w.KEKHex), unhex(t, w.NonceHex), aad, sealed) |
| + if err != nil { |
| + t.Fatalf("Open: %v", err) |
| + } |
| + if !bytes.Equal(opened, rk) { |
| + t.Error("Open did not return RK") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestAEADItem(t *testing.T) { |
| + v := load(t) |
| + it := v.AEAD.Item |
| + aad := AAD(unhex(t, it.VaultIDHex), it.ObjectType, unhex(t, it.ItemIDHex), it.Version) |
| + eq(t, "item AAD", aad, it.AADHex) |
| + |
| + sealed, err := Seal(unhex(t, it.KVaultHex), unhex(t, it.NonceHex), aad, unhex(t, it.PlainHex)) |
| + if err != nil { |
| + t.Fatalf("Seal: %v", err) |
| + } |
| + eq(t, "item ciphertext", sealed[:len(sealed)-16], it.CTHex) |
| + eq(t, "item tag", sealed[len(sealed)-16:], it.TagHex) |
| +} |
| + |
| +// The binding is only worth anything if breaking it breaks decryption. This is |
| +// the transplant a malicious server would attempt: same key, same nonce, same |
| +// bytes, a different slot. |
| +func TestAADBindingRejectsTransplant(t *testing.T) { |
| + v := load(t) |
| + it := v.AEAD.Item |
| + key, nonce := unhex(t, it.KVaultHex), unhex(t, it.NonceHex) |
| + good := AAD(unhex(t, it.VaultIDHex), it.ObjectType, unhex(t, it.ItemIDHex), it.Version) |
| + |
| + sealed, err := Seal(key, nonce, good, unhex(t, it.PlainHex)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + otherItem := append([]byte(nil), unhex(t, it.ItemIDHex)...) |
| + otherItem[0] ^= 0x01 |
| + otherVault := append([]byte(nil), unhex(t, it.VaultIDHex)...) |
| + otherVault[0] ^= 0x01 |
| + |
| + for _, bad := range []struct { |
| + name string |
| + aad []byte |
| + }{ |
| + {"different item", AAD(unhex(t, it.VaultIDHex), it.ObjectType, otherItem, it.Version)}, |
| + {"different vault", AAD(otherVault, it.ObjectType, unhex(t, it.ItemIDHex), it.Version)}, |
| + {"different version", AAD(unhex(t, it.VaultIDHex), it.ObjectType, unhex(t, it.ItemIDHex), it.Version+1)}, |
| + {"different type", AAD(unhex(t, it.VaultIDHex), ObjectWrapPW, unhex(t, it.ItemIDHex), it.Version)}, |
| + } { |
| + if _, err := Open(key, nonce, bad.aad, sealed); err == nil { |
| + t.Errorf("%s: transplant succeeded — the AAD binding is not holding", bad.name) |
| + } |
| + } |
| +} |
| + |
| +// Length prefixes exist so two different tuples cannot produce one AAD. This |
| +// is the collision that unprefixed concatenation would allow. |
| +func TestAADIsInjective(t *testing.T) { |
| + id := bytes.Repeat([]byte{0xab}, 16) |
| + a := AAD(id, "wrap.p", append(append([]byte(nil), 'w'), id...), 1) |
| + b := AAD(id, "wrap.pw", id, 1) |
| + if bytes.Equal(a, b) { |
| + t.Fatal("two different tuples produced the same AAD") |
| + } |
| +} |
| + |
| +// The whole of DESIGN.md's "The shape", re-derived from the printed inputs. |
| +// It is what a new implementation bisects against when it diverges somewhere |
| +// in the middle. |
| +func TestAccountChain(t *testing.T) { |
| + v := load(t) |
| + c := v.AccountChain |
| + |
| + sPW := StretchPassphrase(c.Passphrase, unhex(t, c.SaltPWHex)) |
| + eq(t, "S_pw", sPW, c.SPWHex) |
| + |
| + seedLogin, err := Derive(sPW, nil, LabelLogin, 32) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "seed_login", seedLogin, c.SeedLoginHex) |
| + |
| + lkPW, err := DeriveP256(seedLogin) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "LK_pw public", lkPW.PublicKey().Bytes(), c.LKPWPubHex) |
| + |
| + kekPW, err := Derive(sPW, unhex(t, c.AccountKeyHex), LabelKEK, 32) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "KEK_pw", kekPW, c.KEKPWHex) |
| + |
| + seedRC, err := Derive(unhex(t, c.RecoveryHex), nil, LabelRCLogin, 32) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "seed_rc_login", seedRC, c.SeedRCLoginHex) |
| + |
| + lkRC, err := DeriveP256(seedRC) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "LK_rc public", lkRC.PublicKey().Bytes(), c.LKRCPubHex) |
| + |
| + kekRC, err := Derive(unhex(t, c.RecoveryHex), nil, LabelRCKEK, 32) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "KEK_rc", kekRC, c.KEKRCHex) |
| + |
| + kekPRF, err := Derive(unhex(t, c.PRFOutHex), unhex(t, c.CredSaltHex), LabelPRFKEK, 32) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + eq(t, "KEK_prf", kekPRF, c.KEKPRFHex) |
| + |
| + // The three wraps are peers: each opens RK on its own, and none is |
| + // derivable from another. |
| + rk := unhex(t, c.RKHex) |
| + for _, w := range v.AEAD.Wraps { |
| + aad := AAD(unhex(t, v.AEAD.AccountIDHex), w.ObjectType, unhex(t, w.WrapIDHex), w.Version) |
| + sealed := append(unhex(t, w.CTHex), unhex(t, w.TagHex)...) |
| + got, err := Open(unhex(t, w.KEKHex), unhex(t, w.NonceHex), aad, sealed) |
| + if err != nil { |
| + t.Errorf("%s did not open: %v", w.Name, err) |
| + continue |
| + } |
| + if !bytes.Equal(got, rk) { |
| + t.Errorf("%s opened to something other than RK", w.Name) |
| + } |
| + } |
| +} |
| + |
| +// The KEK branches must be independent: knowing one says nothing about |
| +// another. This cannot prove HKDF's PRF property, but it does catch the |
| +// copy-paste that gives two labels the same output. |
| +func TestBranchesAreDistinct(t *testing.T) { |
| + v := load(t) |
| + seen := map[string]string{} |
| + for _, c := range v.HKDF.Cases { |
| + if prev, ok := seen[c.OKMHex]; ok { |
| + t.Errorf("labels %q and %q derive the same key", prev, c.Info) |
| + } |
| + seen[c.OKMHex] = c.Info |
| + } |
| +} |