diff --git a/cmd/age/parse.go b/cmd/age/parse.go index 2707484..70dcd86 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -31,7 +31,7 @@ func (gitHubRecipientError) Error() string { func parseRecipient(arg string) (age.Recipient, error) { switch { - case strings.HasPrefix(arg, "age1tag1"): + case strings.HasPrefix(arg, "age1tag1") || strings.HasPrefix(arg, "age1tagpq1"): return tag.ParseRecipient(arg) case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1: return plugin.NewRecipient(arg, pluginTerminalUI) diff --git a/go.mod b/go.mod index 7c01486..999619c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module filippo.io/age -go 1.19 +go 1.24.0 require ( filippo.io/edwards25519 v1.1.0 @@ -13,6 +13,7 @@ require ( // Test dependencies. require ( c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 + filippo.io/mlkem768 v0.0.0-20250818110517-29047ffe79fb github.com/rogpeppe/go-internal v1.12.0 golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index b3b4202..70c89dc 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3I c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/mlkem768 v0.0.0-20250818110517-29047ffe79fb h1:9eVxcquiUiJn/f8DtnqmsN/8Asqw+h9b1+sM3T/Wl44= +filippo.io/mlkem768 v0.0.0-20250818110517-29047ffe79fb/go.mod h1:ncYN/Z4GaQBV6TIbmQ7+lIaI+qGXCmZr88zrXHneVHs= filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw= filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= diff --git a/tag/internal/hpke/hpke.go b/tag/internal/hpke/hpke.go index 6d2bed7..37c55ba 100644 --- a/tag/internal/hpke/hpke.go +++ b/tag/internal/hpke/hpke.go @@ -8,8 +8,10 @@ import ( "crypto/cipher" "crypto/ecdh" "crypto/hkdf" + "crypto/mlkem" "crypto/rand" "crypto/sha256" + "crypto/sha3" "encoding/binary" "errors" "hash" @@ -131,6 +133,135 @@ func (dh *dhkemRecipient) Decap(encPubEph []byte) ([]byte, error) { return dh.extractAndExpand(dhVal, kemContext) } +type qsf struct { + id uint16 + label string +} + +func (q *qsf) ID() uint16 { + return q.id +} + +func (q *qsf) sharedSecret(ssPQ, ssT, ctT, ekT []byte) []byte { + h := sha3.New256() + h.Write(ssPQ) + h.Write(ssT) + h.Write(ctT) + h.Write(ekT) + h.Write([]byte(q.label)) + return h.Sum(nil) +} + +type qsfSender struct { + qsf + t *ecdh.PublicKey + pq *mlkem.EncapsulationKey768 +} + +// QSFSender returns a KEMSender implementing QSF-P256-MLKEM768-SHAKE256-SHA3256 +// or QSF-X25519-MLKEM768-SHA3256-SHAKE256 (aka X-Wing) from draft-ietf-hpke-pq +// and draft-irtf-cfrg-concrete-hybrid-kems-00. +func QSFSender(t *ecdh.PublicKey, pq *mlkem.EncapsulationKey768) (KEMSender, error) { + switch t.Curve() { + case ecdh.P256(): + return &qsfSender{ + t: t, pq: pq, + qsf: qsf{ + id: 0x0050, + label: "QSF-P256-MLKEM768-SHAKE256-SHA3256", + }, + }, nil + case ecdh.X25519(): + return &qsfSender{ + t: t, pq: pq, + qsf: qsf{ + id: 0x647a, + label: /**/ `\./` + + /* */ `/^\`, + }, + }, nil + default: + return nil, errors.New("unsupported curve") + } +} + +var testingOnlyEncapsulate func() (ss, ct []byte) + +func (s *qsfSender) Encap() (sharedSecret []byte, encapPub []byte, err error) { + skE, err := s.t.Curve().GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + if testingOnlyGenerateKey != nil { + skE = testingOnlyGenerateKey() + } + ssT, err := skE.ECDH(s.t) + if err != nil { + return nil, nil, err + } + ctT := skE.PublicKey().Bytes() + + ssPQ, ctPQ := s.pq.Encapsulate() + if testingOnlyEncapsulate != nil { + ssPQ, ctPQ = testingOnlyEncapsulate() + } + + ss := s.sharedSecret(ssPQ, ssT, ctT, s.t.Bytes()) + ct := append(ctPQ, ctT...) + return ss, ct, nil +} + +type qsfRecipient struct { + qsf + t *ecdh.PrivateKey + pq *mlkem.DecapsulationKey768 +} + +// QSFRecipient returns a KEMRecipient implementing QSF-P256-MLKEM768-SHAKE256-SHA3256 +// or QSF-MLKEM768-X25519-SHA3256-SHAKE256 (aka X-Wing) from draft-ietf-hpke-pq +// and draft-irtf-cfrg-concrete-hybrid-kems-00. +func QSFRecipient(t *ecdh.PrivateKey, pq *mlkem.DecapsulationKey768) (KEMRecipient, error) { + switch t.Curve() { + case ecdh.P256(): + return &qsfRecipient{ + t: t, pq: pq, + qsf: qsf{ + id: 0x0050, + label: "QSF-P256-MLKEM768-SHAKE256-SHA3256", + }, + }, nil + case ecdh.X25519(): + return &qsfRecipient{ + t: t, pq: pq, + qsf: qsf{ + id: 0x647a, + label: /**/ `\./` + + /* */ `/^\`, + }, + }, nil + default: + return nil, errors.New("unsupported curve") + } +} + +func (r *qsfRecipient) Decap(enc []byte) ([]byte, error) { + ctPQ, ctT := enc[:mlkem.CiphertextSize768], enc[mlkem.CiphertextSize768:] + ssPQ, err := r.pq.Decapsulate(ctPQ) + if err != nil { + return nil, err + } + pub, err := r.t.Curve().NewPublicKey(ctT) + if err != nil { + return nil, err + } + ssT, err := r.t.ECDH(pub) + if err != nil { + return nil, err + } + ss := r.sharedSecret(ssPQ, ssT, ctT, r.t.PublicKey().Bytes()) + return ss, nil +} + type KDF interface { LabeledExtract(sid, salt []byte, label string, inputKey []byte) ([]byte, error) LabeledExpand(suiteID, randomKey []byte, label string, info []byte, length uint16) ([]byte, error) diff --git a/tag/internal/hpke/hpke_test.go b/tag/internal/hpke/hpke_test.go index d15a657..96e8977 100644 --- a/tag/internal/hpke/hpke_test.go +++ b/tag/internal/hpke/hpke_test.go @@ -7,13 +7,19 @@ package hpke import ( "bytes" "crypto/ecdh" + "crypto/elliptic" + "crypto/mlkem" + "crypto/sha3" + "encoding/binary" "encoding/hex" "encoding/json" "errors" + "fmt" + "math/big" "os" - "strconv" - "strings" "testing" + + "filippo.io/mlkem768" ) func mustDecodeHex(t *testing.T, in string) []byte { @@ -25,106 +31,85 @@ func mustDecodeHex(t *testing.T, in string) []byte { return b } -func parseVectorSetup(vector string) map[string]string { - vals := map[string]string{} - for _, l := range strings.Split(vector, "\n") { - fields := strings.Split(l, ": ") - vals[fields[0]] = fields[1] - } - return vals -} - -func parseVectorEncryptions(vector string) []map[string]string { - vals := []map[string]string{} - for _, section := range strings.Split(vector, "\n\n") { - e := map[string]string{} - for _, l := range strings.Split(section, "\n") { - fields := strings.Split(l, ": ") - e[fields[0]] = fields[1] - } - vals = append(vals, e) - } - return vals -} - -func TestRFC9180Vectors(t *testing.T) { - vectorsJSON, err := os.ReadFile("testdata/rfc9180-vectors.json") +func TestVectors(t *testing.T) { + vectorsJSON, err := os.ReadFile("testdata/hpke-pq.json") if err != nil { t.Fatal(err) } var vectors []struct { - Name string - Setup string - Encryptions string + Mode uint16 `json:"mode"` + KEM uint16 `json:"kem_id"` + KDF uint16 `json:"kdf_id"` + AEAD uint16 `json:"aead_id"` + Info string `json:"info"` + EncapRand string `json:"encap_rand"` + IkmR string `json:"ikmR"` + SkRm string `json:"skRm"` + PkRm string `json:"pkRm"` + Enc string `json:"enc"` + SuiteID string `json:"suite_id"` + Key string `json:"key"` + BaseNonce string `json:"base_nonce"` + Encryptions []struct { + Aad string `json:"aad"` + Ct string `json:"ct"` + Nonce string `json:"nonce"` + Pt string `json:"pt"` + } `json:"encryptions"` } if err := json.Unmarshal(vectorsJSON, &vectors); err != nil { t.Fatal(err) } for _, vector := range vectors { - t.Run(vector.Name, func(t *testing.T) { - setup := parseVectorSetup(vector.Setup) + name := fmt.Sprintf("kem %04x kdf %04x aead %04x", + vector.KEM, vector.KDF, vector.AEAD) + t.Run(name, func(t *testing.T) { + info := mustDecodeHex(t, vector.Info) + pubKeyBytes := mustDecodeHex(t, vector.PkRm) + pubT, pubPQ := parsePublicKey(t, vector.KEM, pubKeyBytes) - kemID, err := strconv.Atoi(setup["kem_id"]) + var kemSender KEMSender + if pubPQ != nil { + kemSender, err = QSFSender(pubT, pubPQ) + } else { + kemSender, err = DHKEMSender(pubT) + } if err != nil { t.Fatal(err) } - kdfID, err := strconv.Atoi(setup["kdf_id"]) + kdf, err := getKDF(vector.KDF) if err != nil { t.Fatal(err) } - aeadID, err := strconv.Atoi(setup["aead_id"]) - if err != nil { - t.Fatal(err) - } - info := mustDecodeHex(t, setup["info"]) - pubKeyBytes := mustDecodeHex(t, setup["pkRm"]) - pub, err := parsePublicKey(uint16(kemID), pubKeyBytes) + aead, err := getAEAD(vector.AEAD) if err != nil { t.Fatal(err) } - ephemeralPrivKey := mustDecodeHex(t, setup["skEm"]) + encapsRand := mustDecodeHex(t, vector.EncapRand) + setupEncapDerand(t, vector.KEM, encapsRand, pubPQ, kdf) - testingOnlyGenerateKey = func() *ecdh.PrivateKey { - priv, err := parsePrivateKey(uint16(kemID), ephemeralPrivKey) - if err != nil { - t.Fatal(err) - } - return priv - } - t.Cleanup(func() { testingOnlyGenerateKey = nil }) - - kemSender, err := DHKEMSender(pub) - if err != nil { - t.Fatal(err) - } - kdf, err := getKDF(uint16(kdfID)) - if err != nil { - t.Fatal(err) - } - aead, err := getAEAD(uint16(aeadID)) - if err != nil { - t.Fatal(err) - } encap, sender, err := SetupSender(kemSender, kdf, aead, info) if err != nil { t.Fatal(err) } - expectedEncap := mustDecodeHex(t, setup["enc"]) + expectedEncap := mustDecodeHex(t, vector.Enc) if !bytes.Equal(encap, expectedEncap) { t.Errorf("unexpected encapsulated key, got: %x, want %x", encap, expectedEncap) } - privKeyBytes := mustDecodeHex(t, setup["skRm"]) - priv, err := parsePrivateKey(uint16(kemID), privKeyBytes) - if err != nil { - t.Fatal(err) - } + privKeyBytes := mustDecodeHex(t, vector.SkRm) + privT, privQ := parsePrivateKey(t, vector.KEM, privKeyBytes) - kemRecipient, err := DHKEMRecipient(priv) + var kemRecipient KEMRecipient + if privQ != nil { + kemRecipient, err = QSFRecipient(privT, privQ) + } else { + kemRecipient, err = DHKEMRecipient(privT) + } if err != nil { t.Fatal(err) } @@ -133,33 +118,33 @@ func TestRFC9180Vectors(t *testing.T) { t.Fatal(err) } - for _, ctx := range []*context{sender.context, recipient.context} { - expectedKey := mustDecodeHex(t, setup["key"]) - if !bytes.Equal(ctx.key, expectedKey) { - t.Errorf("unexpected key, got: %x, want %x", ctx.key, expectedKey) + for i, ctx := range []*context{sender.context, recipient.context} { + name := []string{"sender", "recipient"}[i] + expectedSuiteID := mustDecodeHex(t, vector.SuiteID) + if !bytes.Equal(ctx.suiteID, expectedSuiteID) { + t.Errorf("%s: unexpected suite ID, got: %x, want %x", name, ctx.suiteID, expectedSuiteID) } - expectedBaseNonce := mustDecodeHex(t, setup["base_nonce"]) + expectedKey := mustDecodeHex(t, vector.Key) + if !bytes.Equal(ctx.key, expectedKey) { + t.Errorf("%s: unexpected key, got: %x, want %x", name, ctx.key, expectedKey) + } + expectedBaseNonce := mustDecodeHex(t, vector.BaseNonce) if !bytes.Equal(ctx.baseNonce, expectedBaseNonce) { - t.Errorf("unexpected base nonce, got: %x, want %x", ctx.baseNonce, expectedBaseNonce) + t.Errorf("%s: unexpected base nonce, got: %x, want %x", name, ctx.baseNonce, expectedBaseNonce) } } - for _, enc := range parseVectorEncryptions(vector.Encryptions) { - t.Run("seq num "+enc["sequence number"], func(t *testing.T) { - seqNum, err := strconv.Atoi(enc["sequence number"]) - if err != nil { - t.Fatal(err) - } - sender.seqNum = uint128{lo: uint64(seqNum)} - recipient.seqNum = uint128{lo: uint64(seqNum)} - expectedNonce := mustDecodeHex(t, enc["nonce"]) + for i, enc := range vector.Encryptions { + name := fmt.Sprintf("encryption %d", i) + t.Run(name, func(t *testing.T) { + expectedNonce := mustDecodeHex(t, enc.Nonce) computedNonce := sender.nextNonce() if !bytes.Equal(computedNonce, expectedNonce) { t.Errorf("unexpected nonce: got %x, want %x", computedNonce, expectedNonce) } - expectedCiphertext := mustDecodeHex(t, enc["ct"]) - ciphertext, err := sender.Seal(mustDecodeHex(t, enc["aad"]), mustDecodeHex(t, enc["pt"])) + expectedCiphertext := mustDecodeHex(t, enc.Ct) + ciphertext, err := sender.Seal(mustDecodeHex(t, enc.Aad), mustDecodeHex(t, enc.Pt)) if err != nil { t.Fatal(err) } @@ -167,8 +152,8 @@ func TestRFC9180Vectors(t *testing.T) { t.Errorf("unexpected ciphertext: got %x want %x", ciphertext, expectedCiphertext) } - expectedPlaintext := mustDecodeHex(t, enc["pt"]) - plaintext, err := recipient.Open(mustDecodeHex(t, enc["aad"]), mustDecodeHex(t, enc["ct"])) + expectedPlaintext := mustDecodeHex(t, enc.Pt) + plaintext, err := recipient.Open(mustDecodeHex(t, enc.Aad), mustDecodeHex(t, enc.Ct)) if err != nil { t.Fatal(err) } @@ -181,21 +166,155 @@ func TestRFC9180Vectors(t *testing.T) { } } -func parsePublicKey(kemID uint16, keyBytes []byte) (*ecdh.PublicKey, error) { +func parsePublicKey(t *testing.T, kemID uint16, keyBytes []byte) (*ecdh.PublicKey, *mlkem.EncapsulationKey768) { switch kemID { case 0x0010: // DHKEM(P-256, HKDF-SHA256) - return ecdh.P256().NewPublicKey(keyBytes) + k, err := ecdh.P256().NewPublicKey(keyBytes) + if err != nil { + t.Fatal(err) + } + return k, nil + case 0x0050: // QSF-P256-MLKEM768-SHAKE256-SHA3256 + pq, err := mlkem.NewEncapsulationKey768(keyBytes[:mlkem.EncapsulationKeySize768]) + if err != nil { + t.Fatal(err) + } + k, err := ecdh.P256().NewPublicKey(keyBytes[mlkem.EncapsulationKeySize768:]) + if err != nil { + t.Fatal(err) + } + return k, pq + case 0x647a: // QSF-X25519-MLKEM768-SHAKE256-SHA3256 + pq, err := mlkem.NewEncapsulationKey768(keyBytes[:mlkem.EncapsulationKeySize768]) + if err != nil { + t.Fatal(err) + } + k, err := ecdh.X25519().NewPublicKey(keyBytes[mlkem.EncapsulationKeySize768:]) + if err != nil { + t.Fatal(err) + } + return k, pq default: - return nil, errors.New("unsupported KEM") + t.Fatalf("unsupported KEM %04x", kemID) + panic("unreachable") } } -func parsePrivateKey(kemID uint16, keyBytes []byte) (*ecdh.PrivateKey, error) { +func p256KeyFromSeedQSF(t *testing.T, seed []byte) *ecdh.PrivateKey { + t.Helper() + if len(seed) != 48 { + t.Fatalf("invalid seed length %d, expected 48", len(seed)) + } + s := new(big.Int).Mod(new(big.Int).SetBytes(seed), elliptic.P256().Params().P) + sb := make([]byte, 32) + s.FillBytes(sb) + k, err := ecdh.P256().NewPrivateKey(sb) + if err != nil { + t.Fatalf("failed to create P-256 private key: %v", err) + } + return k +} + +func p256KeyFromSeedDHKEM(t *testing.T, seed []byte, kdf KDF, suiteID []byte) *ecdh.PrivateKey { + // RFC 9180, Section 7.1.3. Only for testing, without rejection handling. + t.Helper() + if len(seed) != 32 { + t.Fatalf("invalid seed length %d, expected 32", len(seed)) + } + prk, err := kdf.LabeledExtract(suiteID, nil, "dkp_prk", seed) + if err != nil { + t.Fatalf("failed to extract PRK: %v", err) + } + s, err := kdf.LabeledExpand(suiteID, prk, "candidate", []byte{0x00}, 32) + if err != nil { + t.Fatalf("failed to expand candidate: %v", err) + } + k, err := ecdh.P256().NewPrivateKey(s) + if err != nil { + t.Fatalf("failed to create P-256 private key: %v", err) + } + return k +} + +func setupEncapDerand(t *testing.T, kemID uint16, randBytes []byte, pubPQ *mlkem.EncapsulationKey768, kdf KDF) { switch kemID { case 0x0010: // DHKEM(P-256, HKDF-SHA256) - return ecdh.P256().NewPrivateKey(keyBytes) + suiteID := binary.BigEndian.AppendUint16([]byte("KEM"), kemID) + k := p256KeyFromSeedDHKEM(t, randBytes, kdf, suiteID) + testingOnlyGenerateKey = func() *ecdh.PrivateKey { return k } + t.Cleanup(func() { testingOnlyGenerateKey = nil }) + case 0x0050: // QSF-P256-MLKEM768-SHAKE256-SHA3256 + pqRand, tRand := randBytes[:32], randBytes[32:] + k := p256KeyFromSeedQSF(t, tRand) + testingOnlyGenerateKey = func() *ecdh.PrivateKey { return k } + t.Cleanup(func() { testingOnlyGenerateKey = nil }) + testingOnlyEncapsulate = func() ([]byte, []byte) { + ct, ss, err := mlkem768.EncapsulateDerand(pubPQ.Bytes(), pqRand) + if err != nil { + t.Fatal(err) + } + return ss, ct + } + t.Cleanup(func() { testingOnlyEncapsulate = nil }) + case 0x647a: // QSF-X25519-MLKEM768-SHAKE256-SHA3256 + pqRand, tRand := randBytes[:32], randBytes[32:] + k, err := ecdh.X25519().NewPrivateKey(tRand) + if err != nil { + t.Fatal(err) + } + testingOnlyGenerateKey = func() *ecdh.PrivateKey { return k } + t.Cleanup(func() { testingOnlyGenerateKey = nil }) + testingOnlyEncapsulate = func() ([]byte, []byte) { + ct, ss, err := mlkem768.EncapsulateDerand(pubPQ.Bytes(), pqRand) + if err != nil { + t.Fatal(err) + } + return ss, ct + } + t.Cleanup(func() { testingOnlyEncapsulate = nil }) default: - return nil, errors.New("unsupported KEM") + t.Fatal("unsupported KEM") + } +} + +func parsePrivateKey(t *testing.T, kemID uint16, keyBytes []byte) (*ecdh.PrivateKey, *mlkem.DecapsulationKey768) { + switch kemID { + case 0x0010: // DHKEM(P-256, HKDF-SHA256) + k, err := ecdh.P256().NewPrivateKey(keyBytes) + if err != nil { + t.Fatal(err) + } + return k, nil + case 0x0050: // QSF-P256-MLKEM768-SHAKE256-SHA3256 + s := sha3.NewSHAKE256() + s.Write(keyBytes) + exp := make([]byte, mlkem.SeedSize+48) + s.Read(exp) + + pq, err := mlkem.NewDecapsulationKey768(exp[:mlkem.SeedSize]) + if err != nil { + t.Fatal(err) + } + k := p256KeyFromSeedQSF(t, exp[mlkem.SeedSize:]) + return k, pq + case 0x647a: // QSF-X25519-MLKEM768-SHAKE256-SHA3256 + s := sha3.NewSHAKE256() + s.Write(keyBytes) + exp := make([]byte, mlkem.SeedSize+32) + s.Read(exp) + + pq, err := mlkem.NewDecapsulationKey768(exp[:mlkem.SeedSize]) + if err != nil { + t.Fatal(err) + } + k, err := ecdh.X25519().NewPrivateKey(exp[mlkem.SeedSize:]) + if err != nil { + t.Fatal(err) + } + return k, pq + default: + t.Fatalf("unsupported KEM %04x", kemID) + panic("unreachable") } } diff --git a/tag/internal/hpke/testdata/hpke-pq.json b/tag/internal/hpke/testdata/hpke-pq.json new file mode 100644 index 0000000..caabcdc --- /dev/null +++ b/tag/internal/hpke/testdata/hpke-pq.json @@ -0,0 +1,320 @@ +[ + { + "mode": 0, + "kem_id": 25722, + "kdf_id": 1, + "aead_id": 3, + "info": "34663634363532303666366532303631323034373732363536333639363136653230353537323665", + "encap_rand": "19f270b5955d8c21d2033111b9d16d0c06c282a75eea4f7dc945e0939d6fa8d983985a0d098204532afa26bb0df2442d900999c8f6d53d1e619633a2270ea622", + "ikmR": "b78c64611bd91ab62f5d75855092796fae54f28863d47c58d3973b3748b0196b", + "skRm": "b78c64611bd91ab62f5d75855092796fae54f28863d47c58d3973b3748b0196b", + "pkRm": "193c149214bfa9d3c14ee192e5844cea7a0a3066b196a6527feba3f41370d65572bc65a5a80709867a299b5877a6a94f6e034576f58d3d534e05bba480e41ac5a63110c67e0bd4bea5e82fa2d0a5cbe6905001be7b267d3e283163c49569f9b343f56c4acaa55e5b24667b45aa86503c2780f5b97df0b29561790ad4f2118ca12daaeb9385d61e2703b6880c0acf8b9b19ec1a578c97f7aa036db2488f840094187dfd6067e6f744d9e50e4b441fc564308426ca92dcc89d41b28125a9544c4a2dbaacd43b8817699ebb104c1937be19e349cf5918a2d35c8ba301d4691b3d657b1d032971835b3a7cb4d554171f83a55f3455d1d34e44cb2f8ce699416c5a4eb79adefc3525b42adb433500c2987c0459e736a05399628b4179fd246ea06b090be610064856be191369b93d900a88fc39c93a941c37583529ec5757ab21004c3f97fc11ead8a015d75a5d9355c1287c84a36086e209b05a3038c2a64e21672bea1ff7c385904b099f747791161ca8b09bf222b3a4f615679b959f6424cf520078c42e7545ad80cb6a17d5be8f711a208653293782cc3c94a091cac50bb3b55b13f8c234f4c70b6bfa72e09357548386e785b523571a0c51a6874567a35b037e01b83340a575275c022a2426b0380a4a0f9f0491b7bacee32bbf8eab8a70e62abe31292b7503f274063415c7d676a862d2175738069a0644e8ab95bef87b1568192edc7a05a77843c70bbebb98f24cce20cc0aceb3713e6065b69b9e29987eaba0c669d3c7c3e8221f4680bf45825042892bb0556a694b7d954498435949917093581ee09a0c874c6a323c7d7447472c192cd5e3007742092369c606ab9e2279bbd06c7d2bcc8e43cc69487604294a457580af5962125fe25664e5c2abeb4be3b1943e9b5dfd14a68b873c7b3049647c2188d1b93b7947b38bc7665043193252f1f6450d640832b33cc7274eaa72734720afc0b9b2fed7c838a3778769299761508d507b76ccce76525c822b9a4f8ca3165c3fcd35b0af2c4d3d34c5aec05dd1fcc0222093f7ca5fec1a9ae0c48fc37862c1582d9c042fbdfca0de8550dbda839cf48c1e816bff17c8d7f5470f2463ea77c9c84a42737b3117e2a3086607543514927bac51b5b5d258bc71204519ec980927c2d2c31fe9d86a5b3869654230013583bff3a7c99210817588853acf6350c1976bbb9df0a82ee6a7a2d8305c575a46b570bfd39dde7754ef6273a1b30b78ec85dc6c1bcc49c6a7bc1591e4384acb13d2dbbd58a7153bdcb07e727fb765cd0b57667da0c32ec254c393a171c6abf3b88531827307d95f40b78f4c152183544f77f548e6c064a7aa567e40ae2ce0912e09b3f24169c78528121b10618c6e17341e7b8b26c9d821e584503c786102fb9110dac136fa52ce3acb9b492c9632aa84f7674898620119c1bf1103a6405fdcb25b40928857bb54a9297d80bc1cca7639b9f4422a666b3bd122e9c9baabe1a04dd950c3919784c647823b6f472673fc0012b960b546d17b7fa94d27d836c5709f86a9022a382518172fd6aa5be0a373ab12bd93687f6a52743f685f62b1472d5b0b59a740d484c56a2bcf0df47133dc95aa37529dda6b36679e1a924df2c88ee180134f41bdb90c4e77b2fc9be781a77f7e9beeb7badd04f884b7547b0279ece1cef0486d045bf812a32d802ff5f5a54549e93ebcab143e7b70c46148421f2d", + "enc": "c40b97a1af0f4066fa9626ba68b1980185dac3a7cb1ad6ca63650e78b4d305ed2ee557ae5dbd3df9c807cce1aa88d739d35feb4d06735484cd8507cd4eeb4c0fb2c1abef0c7e0cb1177841ba7397d8f6a1a2d226de046659903df93cd26322786b1626cd86579d03cc9d5c568bdb6123826380f46e2990fa9bae9dffb0126ca61da6326528a215c84bbd401999b9861cb81b8cca0ac72298dfa400589ba91c87dc1bc48981439bf02120637fac354e5bfea3b0c6de84b2e726a450b791ab38b96fadc74f6348118f9359b9eaf860463c76f7d1d9aafb213230bdf4dd9cb536c8a5c913307fea36dbffc53b59dbb6b88fb71c4526508eed79cb33d59c996e828e89ef29383a6236605020fdb1202b5aa1e7b30c54da7490e8228cfa460f95a0c17778609acac11ae969c54543f05078568b330ea6795aab1d049ebf871881a3192da35d045b3bbd6e5542fff3d060e880e6dc10cc27c123b003aa8c5e0ca36fea6bd20c34ad8ac8b759df0c87e3fa780a1c75b543a7d85ec7ee187eab34d53477cde9dd22503e602ccc9fa9bf725a0c6a176058ae05b2e44d5b790a46b432ca3f9c69b83f49c1b71b8585cdc0a4bc676f1d8744bf9d8a036c2628b11281ac2f243113a08dd81716f88c697a2b123c2e8bd61c74ce4d64edaca7313aaaa877f6f8c9c1f766aabf8734e6ac6117b7c3e8c1dfc1cd6f082f675cf28507cc09162b441180daaf1784f702c173f914478e1bef288e0d5ed644fa4e3d66e8e1de8fd9fc32a4f0cf31791c84d4a362ffd730caa6bbc0ee9d2d2e41422b5f4337b55566512580b5866bcd5390fa72b6a07396281818e2cfefbdef7c9859d4cde52ec098ef802c835eb7eeb453f4ad819a90929b2664533f427f56709f238753d284894bb0393d2dd7c3f31c80535764f5d4e20062b8fa7a025f1583aa3b8c145d064ce22730e68bc322d78d225110b0851f193555c3e07e517f3e79b445f5d7356f1ca681f7c678a018f250b18a08c6b87d83415707c33404609c969b6865cb5f0bdbc78f736b790ef588fee12fb38ea7f083608ab362e3c72a5e8b6c086223ecd234e1358f50823e11099337134bbec85a203e32b5915ca100c9fcd505b9f36055fff011e2f691f57fc6aa567b79492e8cbca09e174c2d2446b240ad20c9d9c16b321c1cdb16c709b1130ff0b3456abe305a9e2c0a514ce41251044fdc83b99d20c03b62b7bfd5a3e3bc4df60e5cbc7544de9d34c97d0bf9248d30b7eb686f0c520624eff3a60cd9b5999a3fef79184d0c63d1dde140de5a92d3f997ca4ceb0f002e5437e7ce6dd6640668dbc5afeb5795b65720f87f363fda1a0f9fcc38ba21e8f9b280083049a3378c6c119fe761759faf6e79d1fb487447b87e4daff98f36521bff863f9556ba7f0fbcc6c99c9b6c1db9f278fa6bf9dcc24790825947ed8b1b9404372e38fe07dee698b47c449b608718f92ef66dc19658db870997e21c1c262ab89a68504286214a03cd291d2a41d749c5f56633542f50ebdeaccbdfcbc7999ab8880319b9ce0ef73d15958f5f168b8ad29e108a20186d906567497f5b2218d06a35", + "shared_secret": "51d7c1b3038e92e6e402de53a915c490c0a3333f7a56fd4c5ab9e2d9e29c8e80", + "suite_id": "48504b45647a00010003", + "key": "edce21c81f84b213a0997c408a566fa901f97dcd27427d2bb94ca660119a8683", + "base_nonce": "2fda4392a0b23a352d70f0e0", + "exporter_secret": "0f2816531c4db55d8d2e45c0d55ff00fa07cfa98216e4dbe58632b8e0160d5c9", + "encryptions": [ + { + "aad": "436f756e742d30", + "ct": "b177c7b2d4d4dd604722231ee430f4c6334bfef1fe7bd9e95782f7995e37dcc0a2b902a9675cde8fd05f6d9a0f2f109de0533e16c34ed557516050eb9d620d2768e05278d451aeb390c9", + "nonce": "2fda4392a0b23a352d70f0e0", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d31", + "ct": "c4cc0bf69d0a6f059a61f32243955851757274af96075ba473cafa2615bbc1abc0fd1b8522213ea7b93a77881ce531818644266d2deb9c9ec2cea9c2922fceaa7e79b2d32678590a450c", + "nonce": "2fda4392a0b23a352d70f0e1", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d32", + "ct": "249a52262884fd861965d335eab7f6674460177390f607b83b9ac26c126d28141bffd5538607c73e9b1a3f2931e1e65f00034189a062d80f2560c00f24b506cab0d02d4ab95a3260d58e", + "nonce": "2fda4392a0b23a352d70f0e2", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d33", + "ct": "829ec3fb97f05b6ca0c392add4fa3ff518256c739072b84fc78315e15fc0cca9b129f02313729af796d27f0155fdb0e65dbc1c1bb68022a0ee47545d88fbcc3ec60e978f01656faa1a11", + "nonce": "2fda4392a0b23a352d70f0e3", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d34", + "ct": "bab611de27d0d7f951e4f9368d6486d51104da8e887ad8e20637b9db506fde90034f29610f88e0ef70358f836366b9c20b83999cd3a5076a6256e20223e172ed54b15e4b2548caeffdff", + "nonce": "2fda4392a0b23a352d70f0e4", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d35", + "ct": "fb480f295b8f3e41c5cfa7aa3064f727e648c2d43982dcbcc1d04f8c2f538055bda8aac03e9c7a77eb50eef87a3dd3f0a47fb7230433e13d91ff194912846f69effe3f4a223eb7e965fc", + "nonce": "2fda4392a0b23a352d70f0e5", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d36", + "ct": "0da3d3112e53368b006bbd510d33769f3487fec9563f62abac563287a3a53ad032591cb57128a12fcbe689be33e92b02e96ecb5c12fed23aa517ef7be59f4dca9060ca359c971e014a2f", + "nonce": "2fda4392a0b23a352d70f0e6", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d37", + "ct": "8c3a4ca5373704e664f5266cff459b40f0d2e1b855134b7e668453eb95bce1b7a09eefffcae866b7bd886a9aaba10dc1b012cf1037b0d80963c71aaf2c619ac877144dbf01a9d57390df", + "nonce": "2fda4392a0b23a352d70f0e7", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d38", + "ct": "144e33c04ab411a116253b6858ea27d57ccb0b247f83c013d10193e50010504155b4b8b4572b83410b8f008749b873d5c159027334c074762f860f4d60e41c207838e0f444545af1152c", + "nonce": "2fda4392a0b23a352d70f0e8", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d39", + "ct": "d92a59325eac0496a2c33c48e12bc76b637150d4db794ad5c8ea14bf8c742351b6e74b65464dc50af0ec4d364219ed36f56ece2b5260e850d5c24660e2240f440e07fdb462be20fac811", + "nonce": "2fda4392a0b23a352d70f0e9", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + } + ], + "exports": [ + { + "exporter_context": "70736575646f72616e646f6d30", + "L": 32, + "exported_value": "dfe6bdeea3a82eef95414480fdda8ecf9ea8d43846dd86072348a07183b52215" + }, + { + "exporter_context": "70736575646f72616e646f6d31", + "L": 32, + "exported_value": "cef76709dd0e384a97a5babcb77b221c2c30b223d3926b4b353b64767dbabedf" + }, + { + "exporter_context": "70736575646f72616e646f6d32", + "L": 32, + "exported_value": "b12381e05242f2a609545b9a3d826ecf2183f871314ee52c6f64760c6f636c86" + }, + { + "exporter_context": "70736575646f72616e646f6d33", + "L": 32, + "exported_value": "f0245f260ba33d995197bf06e6b8330cdac8b72dc3fc6e712ed4d3f6241b93dd" + }, + { + "exporter_context": "70736575646f72616e646f6d34", + "L": 32, + "exported_value": "2b279cb0c27ecce59d305e369b31b2c467a60946014278ca8bdca12c384f7a8d" + } + ] + }, + { + "mode": 0, + "kem_id": 16, + "kdf_id": 1, + "aead_id": 3, + "info": "34663634363532303666366532303631323034373732363536333639363136653230353537323665", + "encap_rand": "ae3c5f0e0d711f220f174b948620b6a9a84931f1510a1e78fe75735ffa585c29", + "ikmR": "f60dafbaa4dae9c499d09cedd84143297a66c23097bc4e69d1e5c89d1d6d7fc2", + "skRm": "5d1e0a06d9d5159783d89efb66b82fdf81f16f1ff5cd81e39a117275312a80d0", + "pkRm": "0469d46c0d5acbf0813fec4cbf81309675e822b6740983a55d5eb905d5e07a86dc70378bbfa6a1d9aa7269f98eeed2d9882346d5b0f5477e84918445853c267065", + "enc": "04885fb4ad2c5088593ee72afb295a709684ba2c016561b27d62d4fc39d2c884e5df85d77d3366d922726ebe95fd3aa2b8019fc1cde75b53684f21e8612ff48f6c", + "shared_secret": "3b26aca70d3510ae3acab4c117ede13249de20dc1fad0b0c6262137457c333e8", + "suite_id": "48504b45001000010003", + "key": "7aaed1082c439b9520fdaf4b7da76a52e53f92b592f35266b3683a72297436d2", + "base_nonce": "4e5d41b58438f9ddd494a510", + "exporter_secret": "c01ee22ac8dd68260f02c9e29aa0745819a8327443eadcf6a4e57c69c613ffb8", + "encryptions": [ + { + "aad": "436f756e742d30", + "ct": "1e26c3096c6c768dcc2ec1ec41d188bbeaf6bc6c6bd24e43ba313fac8d9eb3140592204e29cd1ee40aa5ccfacdd2c36e6cad773baf7bb9cfa8f53e626a4728d910043eda957fc01b04c8", + "nonce": "4e5d41b58438f9ddd494a510", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d31", + "ct": "e3a595f805e18a948dc5a84cdf693f8903ead0ab73c7cc9bdab7f749d607967c7672f2ffced518116bf55a1db2157b73079ea4cb1a07b3df6826858e609e0e1a1900e6a6a867ff33d8f3", + "nonce": "4e5d41b58438f9ddd494a511", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d32", + "ct": "0dd72194de92d55f648f029ccf7665a4635356c3c0ad9d9877ca99bfeb0c26f0a9194c00e025019dccb2015bfabed58542798caf305a25d03b934add6c8894632c9490cab1d5d8b8fb52", + "nonce": "4e5d41b58438f9ddd494a512", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d33", + "ct": "053ef6c31808e8c250debc14794e135b077441746b4760f13fe4a90cbeffcaf63ae284c1156aa0db6a9f8d5dc8189e6e9d284f0dbe96b8dea196a631d0f287ff3bb3df185a136b6e4a12", + "nonce": "4e5d41b58438f9ddd494a513", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d34", + "ct": "d8add82d80ba3e65c72d5b34a9edd19980ca2fc28b473867c4205d4fc7b4f0a3d84070624d4f6d922311e80dc4de402f90a745cd58bf022b00852ecc4187c27da22518b4132121601562", + "nonce": "4e5d41b58438f9ddd494a514", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d35", + "ct": "d13fbdda5d6048bbdef281fb2207487f71144c0e4025c439eac0a73f679e0c2f3ac2cf748f1a3af8607434507f85f63f06f9ee1891a951b3d5222c807acbad3759fe49e0b0cd86fa4872", + "nonce": "4e5d41b58438f9ddd494a515", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d36", + "ct": "f05bcdce13576896bc8044965477e1c2450cec84ceaad9a04c93fae1ee2863fbe9c6d1944f681b7aa44ae803e9849b8a5f47ea6464f26f1b5e19af28daa56ff5cc969f5d21ce061fc446", + "nonce": "4e5d41b58438f9ddd494a516", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d37", + "ct": "5c6fe7f4e7f61305e99f8de862102d00bb0300f5342c78b535a2159359d5fe7512bd3232988e97e6b46a988e3e0cf1207e749e3d2206631c994792dc1075f8d63f49ec8f02956239dce0", + "nonce": "4e5d41b58438f9ddd494a517", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d38", + "ct": "e7af3bea35ed4d63b9a4a9a9125112e13e783620f6767c92cf1daad63a27c6dd68f87d17009e70094b2ca9eee4ffaabc9a45d966fd74c7ac2cae4aa3bf4007dae88d7929a2623569ce04", + "nonce": "4e5d41b58438f9ddd494a518", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d39", + "ct": "082552f1ca06613e68279ef115742dd353259260ca055b38436d570f3dc404e335baddd4368ae21cc24e1e1850e97d7580659c0796221ed37b561732a01bc2234bc4dc653af483e88955", + "nonce": "4e5d41b58438f9ddd494a519", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + } + ], + "exports": [ + { + "exporter_context": "70736575646f72616e646f6d30", + "L": 32, + "exported_value": "4d06b6f07867c5031ba504e4f5467c316fca853f4f79b8246311af7d4bb77263" + }, + { + "exporter_context": "70736575646f72616e646f6d31", + "L": 32, + "exported_value": "304ca02f1e562aff01e6059955a8c1aff4189e5b274e8d2f4f46cb56333fa48b" + }, + { + "exporter_context": "70736575646f72616e646f6d32", + "L": 32, + "exported_value": "25003705b62981a81cb6a6b7c5766f62163f2e52bac488ae47e10a2eb439a68e" + }, + { + "exporter_context": "70736575646f72616e646f6d33", + "L": 32, + "exported_value": "c787490330f5c4b667f2f51a03117c36e983b87e1cc7c2c8def1e0df42cbd034" + }, + { + "exporter_context": "70736575646f72616e646f6d34", + "L": 32, + "exported_value": "157124564aab5e3ec50342ed8b76cff77ff589314b636b66df4d8aff5f96716e" + } + ] + }, + { + "mode": 0, + "kem_id": 80, + "kdf_id": 1, + "aead_id": 3, + "info": "34663634363532303666366532303631323034373732363536333639363136653230353537323665", + "encap_rand": "bd31d63122a4c4cf37244a31ba6acec390ce06f412ad3cbef973c03f3a32602e899409cbd7b4f9ea2a29d5f45952dc1368836b7d1b2a627e1fa94bcc799fcdd20d63f763872837ffe279632acc12d85f", + "ikmR": "9ed4e7555bc2c65f43e2ff3a5beb826daca1a79e7bc89ddae587659ac87e82fb", + "skRm": "9ed4e7555bc2c65f43e2ff3a5beb826daca1a79e7bc89ddae587659ac87e82fb", + "pkRm": "65070243e439adeac9e3bc7bb3c569d22027d1009640b7ae404a6ed0849c2f9599e16943d105b81a196f1f2990ac0204f8e35041032cddb1170a39c79c79568b01bcb62549fcc78bde6546387ac5e64767a1ca9f65cc2fd8704ad199c90d8184257cac4e853d0a0c89eed239f3d5bb52a4005f11cce49bb7569b8553968923056902fa5ac080bc80e404116143eba0210f2b43231a28058824f7136fcb1b14759a3e81e5ca292505747659c488abcf25219ac7cfa5887b2bba7a9754922148cd45054e3b5b57ac9b6666b17f13787284359eb695109923491513930d173dc8c2708dabc6303cac77eb51f349a31ed3c108fb8a3f8537247bc7fae93c706b264ca0323e122c5b5b78b4f0c385867fdb6440ecd804eff8be81362043156740a183016a4f418573db6ccbedc9b9dcc9347eca8912f46bcf02a115c67b206994becc3104c45f04154a0aac9391d57e2d835479385eea571db7921ccbb35575ca8bae196a9784a4de76ac6d003d6be1a090dbc993795e39f5243924717b8462e2e7b564b7770e81abcf47a96abb872c758d68d7a0cb1ba00ee43fab1abd3435b31bc33f2c3b2a0b51c4c60c5e2c280bbfc423d07c31e4e6475ee138daab3d8c799d59d04eca814204e58168798e4188b53685c0df2085f006c709e3b63e7a16c3027a12ac081be6cc822376bdf8168e8253b1271d63501480b535127a068eeab7a7da2c26764dbb73b1f749a5ab572ef0904d7ed072782206df76a3e27570be88a33f364d65e5679d3c3ff3522f843396aa71536b3578fb8336f7f5adf1a59308007e5da949fc80332b40098da8611a0139e3315fc90c2454a2243759b8d1f6a5925685637a43a0f14199e9bce0213d5d20c808694680a01c75c057f3b19fb8218e18d3cad6743616e8814381a25a54a1a32a5f1f045288f5b567f03d7a052cd54400da215c2d7a0acc7c4fb0f06c1024a7100940e328843037449b04a0c2114937fa279ab94ec83a7e3b232df9b5a947c0a3bdf06b31544d253bbc0439050fd352149119d5526cf8c9abe565b6414b721b76422c752a4176205f82a3d89a0a4b5b20b56bb9bd0bb43c51814843ab8f402f34820cfadc71558713e41023373ab5bf0ac5a54541bfb0ad54e17e177a9b8ddb284741beb188c1e396006479044a12afe0281096a0c554b4a843c97d45ca4def0bb68e8345453c6899fa63c3208b98b60b48424cc2926e96514ca1d5bb09d2c3db043f29fcbeee064c8b860d9a01d094aacb2f351f76c70f2774b55a24a74fd29007f206a1acbde0044820cbb708e2a9b08a11910989accab8d001c7d83cb3063393e5bc1450577658b6b5ae1b9fe3c0c36e39658f33abba40ce7a357706186af3f621db8952fa7311d03c05a50274159009951233e6c15cfc2151b7a6c44596beb4090d9bb39c6b1aa75d3c638ad0768b2180dd672ab179654b144f8f1c237a5785315b162858570f1a2bd7a5201133cfa9eb0725f154011a960862bbf48bba699288966a4543a99f26ca44fa65aef8c4bb02f833b3685def4b270ecb470c3c7b0bc0ceb74137c64449963751989c63196acb64bc63ea9a48d96672f44035949b3c6439a5cec59433c335d832bbffc92a1f3e03e8a5d3272e35b725657f724711059361818c23285f53dca378840a4b5e04486919d414ea7b6203a1e71a8204b78696220a9f385232f097e28505c93b963ac7d2a3d6cc53794d14e70319332bc67c9b93c787fef6808dd84e10a208bb3890", + "enc": "0ec138280990dd2deeeb9ceba63f944e12e551e788dd268e8680622ec47a9500ad1f83f21f3ffffd764e318c86fce86ca75e5de9c9124d8ab5b4baa36b64743ed449738166677a6564c2ee9b4abd33d42c2c7e66bc9bf1fef96028214934deedd763be2e2567f6021aad99ba3a477d1565f4ae35a129549e620464daa564f7569db5a5d24ac6a01f43e7879885349c547d288fe253c3feadb244510b38cd65344477b2bff0e2b12db4f69b3cc0219b868d11a1a6d61ee2e76c1b598920c30149004b77d523d3991863df21011bd4ca589c1081880c00fce107292d0bfd770dd42a68e7b12ebb173766421986c0015ad7cabc1c191a26e84d692f167deac56cae31fc992a9fc6b2fd15e7c161f926d46e5d9ee478c90ea5195005f62de0d75beafeb828c2336c6070fc254e5aacd4ae74ff148b614468aaeeccd0c0089de312524e343b72805d71bd534c4e3daadeac64a1cc683dc311c917663d81e0a937f553e59c8f17ec754b476212c6a4e155c05ee2c2b79dcd3bc0e75aa2b7185a2345b981c710083574389f2710f2c658d6ae236ed0e8e75ccf3aaeb9bc34fd7251306128d2b3a3cd921c51f93ede6ae7a68192775efd4c242f80fa87142394615dd1a27e92fa4c9a7030f416e183c42b63e73ab2dd42775fbbc26e0040defd97530d1142da3f5bd0a3f021478e1c46f45f0ff520a474c067544fc3ead1d7782ee666082a40c1dfe12a7d679ca19e5a0775c444e48a7e1c6a4223178dabaa5f99b202cddf0834373e90f54368a7892b17784b4ec04ae7546d0cf29914d1672c9e67cae3fc92943487053f00ad23c1adfa4f70c4297dc68914eefcf9f0b37f06832651766a67ba5bce8a020433ae1fad356cdccecb9b8d37b8fc41217e1d5093ffd156aaca4fe7821151c03e730806ec770be5977e7e68ce1c02ffcfd20f773dece498f1683d568eb171f5122fa1e2ae1aa186a4782ca2adc40927f05b59b577fe0c7e95591b7936517780fdec2177eb1061347716e931f97f0d7da89fee1ea790564af8eaf7314aaf5196af51e8590d1bda044b0f504f0040f83798b22c1722bcf7a0bc713efed24dcbe2c30c77cdf4f22fda3a96ddebcf7c3546be2daf728bc7f312cff4b5706af353d74a519652cd274a0bd4116168a138d311da2877aa190803aef91455d9faa733eb0912177739f47b1951f7da23104f5b9e249f25270bdb0e58c290d3c64f681ab9202b42987d392d3f94d58c1e81a4f4e62293e95fb011cb2b8f49f5ca2fd85f399a386f91ead63bbd97cf2bb68b372cbd1b081ac9bcf14109ab0298f4b0d7c9b16f75090b1c49721b71747a357065c4833cccd52a2c7406f1c141e8cc669678b62fdfe119610dbdec0ec87b0ea54c71f64ea0598672b40becd957594e2c07d5cb0e90dc4378ed110a1b4450f42497c2efd699f91a774ca29a370f76d4fb04dd1ac3fa0779805587819ab6f1785a3330ef60aec8e707e945c776611f530f245e6f554ab408fc1193d0f08dd7773629132e65ae4f3e70bcb0a71004494def2db94f30c53f4c17e39ebf15efbae07810dce16868256c9abfb5b1c339e670c223739a81057db8e7d8bda8ecd9a7cdadaf4350f050ecd10e679137eb1e", + "shared_secret": "61765d0dc46c62192c9800d74ab8ec77c633810720055663fbe6a0d57bded97c", + "suite_id": "48504b45005000010003", + "key": "9b8e5d4f868de455a2f29cd8dc127513be97ec9e0f616e45b7ece8d8e55ac17d", + "base_nonce": "599544220e8706a477505030", + "exporter_secret": "dbfe1ea45410d7b5c857e41c5ce41fad4ba0d9a3ad5617b9a13e0fd26c62b6b9", + "encryptions": [ + { + "aad": "436f756e742d30", + "ct": "def109ce3c4d4d489e585b1cebf86c679665121425c729754c034036b914f7f0ca14c52da51419e2c9a677f1186994c8eb6e707f42acd66f07d15bba920eb87d6687158ebe8f9615215e", + "nonce": "599544220e8706a477505030", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d31", + "ct": "677528b4d1a8eb24b9964c95f7a844bd97e6b1e40eed75575326336e4d6fc57d9b41fc42bf2bd61152a76578ff5729279c5d975e2f4f1fae7263a9370a40c9ae28105064487ad4feb3ff", + "nonce": "599544220e8706a477505031", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d32", + "ct": "0d11a1ac1f6b6d29702c358793f440685b74505f8b0d318997a2fb6c451ccd633e342690120a9c767eb0614fe3cfd518484c974df0c1459881c897b7ed590785ccdea03baa086d5da33b", + "nonce": "599544220e8706a477505032", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d33", + "ct": "0f23a23ae250f808c2a29ba52c214b10ca6cc95fbbc99e48d35ac8bb38f9a251bceaac0d73ef168ec68f645e1cf3ef56175fd63608e4cf0eeb02aaa2005535530d77da6a80ab63aac334", + "nonce": "599544220e8706a477505033", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d34", + "ct": "5e3c3d049535375c3a6de075c1cc5b619b734c99b9c2be9e9413087fe66576a9ca7629094e3293fc8fb443c21464e9070dd8b14d31701bc61a9aa8dbe0a6c45531322f6616d96197c79c", + "nonce": "599544220e8706a477505034", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d35", + "ct": "f8ffd8ad031ea2e8dac99d810b2f969db2ca7c1a5d581f8a8f16a9db6b3cc7a8620ea16ff438568100c40b094cf951c53abc30c5ff6f2e2a25b3242ed04b193d29138de02c792d0b883c", + "nonce": "599544220e8706a477505035", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d36", + "ct": "fee465b871efa4e2f428f9a66458cfa2a7b99774ebeb630ed548f2fa22caaf5ab50e9fed77293aa312c7c5209100a97455dc92f4b4cb4c4e07b3547e7e73228e18f152c67915c71cdcac", + "nonce": "599544220e8706a477505036", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d37", + "ct": "582afc61ffb3ffd7704d76088d2c74cc2530f6bdd6593cbb2977239b6f484716bdedb0d6b1b129f45e1d4afc8f407d17fadd3a3d971c82f8369fd6772d5f2d5274cfff48c3d2db63a44d", + "nonce": "599544220e8706a477505037", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d38", + "ct": "81ce8e9ae429b85b4dd4b40d7f7bb426565d23f2c3d63f74ea96dbba881dfcfe33b95c8202ff37b15bb14f85d52de1a506c3e2b42d650e850fe97a63017670ee52815705dd61421589fc", + "nonce": "599544220e8706a477505038", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + }, + { + "aad": "436f756e742d39", + "ct": "691608a0e0e29d0c2def20bf6f0991bb7eccf57f26722975f3640b4dc23f4be29cfa60352ef3831e6b560e896c766a0126746c380dd3f695b82e4039549202ef01809ee5fde406f2c8a1", + "nonce": "599544220e8706a477505039", + "pt": "34323635363137353734373932303639373332303734373237353734363832633230373437323735373436383230363236353631373537343739" + } + ], + "exports": [ + { + "exporter_context": "70736575646f72616e646f6d30", + "L": 32, + "exported_value": "fe89a3b25515355f1e831529a2306040e6a9edb1643acab8bb9182dc09a029f5" + }, + { + "exporter_context": "70736575646f72616e646f6d31", + "L": 32, + "exported_value": "d79a2633bd36ff4220e355f381303398abc48ceca0491410791afb733c5fc882" + }, + { + "exporter_context": "70736575646f72616e646f6d32", + "L": 32, + "exported_value": "60a3cfd587ce2bf68235328399ca47ef336f63a2c16a57129aa824b8238e8e85" + }, + { + "exporter_context": "70736575646f72616e646f6d33", + "L": 32, + "exported_value": "678cbb9bda711aa6e4f1108db0fa9e9e0585764895d9adbd3b5c633d92dacdc0" + }, + { + "exporter_context": "70736575646f72616e646f6d34", + "L": 32, + "exported_value": "1df9fd502558a98bdd8c80b54d2811e9537850d6b8254567a23c6f049818fc17" + } + ] + } +] diff --git a/tag/internal/hpke/testdata/rfc9180-vectors.json b/tag/internal/hpke/testdata/rfc9180-vectors.json deleted file mode 100644 index 44dc418..0000000 --- a/tag/internal/hpke/testdata/rfc9180-vectors.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "Name": "DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305", - "Setup": "mode: 0\nkem_id: 16\nkdf_id: 1\naead_id: 3\ninfo: 4f6465206f6e2061204772656369616e2055726e\nikmE: f1f1a3bc95416871539ecb51c3a8f0cf608afb40fbbe305c0a72819d35c33f1f\npkEm: 04c07836a0206e04e31d8ae99bfd549380b072a1b1b82e563c935c095827824fc1559eac6fb9e3c70cd3193968994e7fe9781aa103f5b50e934b5b2f387e381291\nskEm: 7550253e1147aae48839c1f8af80d2770fb7a4c763afe7d0afa7e0f42a5b3689\nikmR: 61092f3f56994dd424405899154a9918353e3e008171517ad576b900ddb275e7\npkRm: 04a697bffde9405c992883c5c439d6cc358170b51af72812333b015621dc0f40bad9bb726f68a5c013806a790ec716ab8669f84f6b694596c2987cf35baba2a006\nskRm: a4d1c55836aa30f9b3fbb6ac98d338c877c2867dd3a77396d13f68d3ab150d3b\nenc: 04c07836a0206e04e31d8ae99bfd549380b072a1b1b82e563c935c095827824fc1559eac6fb9e3c70cd3193968994e7fe9781aa103f5b50e934b5b2f387e381291\nshared_secret: 806520f82ef0b03c823b7fc524b6b55a088f566b9751b89551c170f4113bd850\nkey_schedule_context: 00b738cd703db7b4106e93b4621e9a19c89c838e55964240e5d3f331aaf8b0d58b2e986ea1c671b61cf45eec134dac0bae58ec6f63e790b1400b47c33038b0269c\nsecret: fe891101629aa355aad68eff3cc5170d057eca0c7573f6575e91f9783e1d4506\nkey: a8f45490a92a3b04d1dbf6cf2c3939ad8bfc9bfcb97c04bffe116730c9dfe3fc\nbase_nonce: 726b4390ed2209809f58c693\nexporter_secret: 4f9bd9b3a8db7d7c3a5b9d44fdc1f6e37d5d77689ade5ec44a7242016e6aa205", - "Encryptions": "sequence number: 0\npt: 4265617574792069732074727574682c20747275746820626561757479\naad: 436f756e742d30\nnonce: 726b4390ed2209809f58c693\nct: 6469c41c5c81d3aa85432531ecf6460ec945bde1eb428cb2fedf7a29f5a685b4ccb0d057f03ea2952a27bb458b\n\nsequence number: 1\npt: 4265617574792069732074727574682c20747275746820626561757479\naad: 436f756e742d31\nnonce: 726b4390ed2209809f58c692\nct: f1564199f7e0e110ec9c1bcdde332177fc35c1adf6e57f8d1df24022227ffa8716862dbda2b1dc546c9d114374\n\nsequence number: 2\npt: 4265617574792069732074727574682c20747275746820626561757479\naad: 436f756e742d32\nnonce: 726b4390ed2209809f58c691\nct: 39de89728bcb774269f882af8dc5369e4f3d6322d986e872b3a8d074c7c18e8549ff3f85b6d6592ff87c3f310c\n\nsequence number: 4\npt: 4265617574792069732074727574682c20747275746820626561757479\naad: 436f756e742d34\nnonce: 726b4390ed2209809f58c697\nct: bc104a14fbede0cc79eeb826ea0476ce87b9c928c36e5e34dc9b6905d91473ec369a08b1a25d305dd45c6c5f80\n\nsequence number: 255\npt: 4265617574792069732074727574682c20747275746820626561757479\naad: 436f756e742d323535\nnonce: 726b4390ed2209809f58c66c\nct: 8f2814a2c548b3be50259713c6724009e092d37789f6856553d61df23ebc079235f710e6af3c3ca6eaba7c7c6c\n\nsequence number: 256\npt: 4265617574792069732074727574682c20747275746820626561757479\naad: 436f756e742d323536\nnonce: 726b4390ed2209809f58c793\nct: b45b69d419a9be7219d8c94365b89ad6951caf4576ea4774ea40e9b7047a09d6537d1aa2f7c12d6ae4b729b4d0" - } -] diff --git a/tag/tag.go b/tag/tag.go index 78326e4..ec8d9c7 100644 --- a/tag/tag.go +++ b/tag/tag.go @@ -7,6 +7,7 @@ package tag import ( "crypto/ecdh" "crypto/hkdf" + "crypto/mlkem" "crypto/sha256" "fmt" @@ -20,32 +21,44 @@ import ( type Recipient struct { kem hpke.KEMSender - compressed [33]byte - uncompressed [65]byte + mlkem *mlkem.EncapsulationKey768 + compressed [compressedPointSize]byte + uncompressed [uncompressedPointSize]byte } var _ age.Recipient = &Recipient{} // ParseRecipient returns a new [Recipient] from a Bech32 public key -// encoding with the "age1tag1" prefix. +// encoding with the "age1tag1" or "age1tagpq1" prefix. func ParseRecipient(s string) (*Recipient, error) { t, k, err := plugin.ParseRecipient(s) if err != nil { return nil, fmt.Errorf("malformed recipient %q: %v", s, err) } - if t != "tag" { + switch t { + case "tag": + r, err := NewRecipient(k) + if err != nil { + return nil, fmt.Errorf("malformed recipient %q: %v", s, err) + } + return r, nil + case "tagpq": + r, err := NewHybridRecipient(k) + if err != nil { + return nil, fmt.Errorf("malformed recipient %q: %v", s, err) + } + return r, nil + default: return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t) } - r, err := NewRecipient(k) - if err != nil { - return nil, fmt.Errorf("malformed recipient %q: %v", s, err) - } - return r, nil } +const compressedPointSize = 1 + 32 +const uncompressedPointSize = 1 + 32 + 32 + // NewRecipient returns a new [Recipient] from a raw public key. func NewRecipient(publicKey []byte) (*Recipient, error) { - if len(publicKey) != 1+32 { + if len(publicKey) != compressedPointSize { return nil, fmt.Errorf("invalid tag recipient public key size %d", len(publicKey)) } p, err := nistec.NewP256Point().SetBytes(publicKey) @@ -66,12 +79,44 @@ func NewRecipient(publicKey []byte) (*Recipient, error) { return r, nil } +// NewHybridRecipient returns a new [Recipient] from raw concatenated public keys. +func NewHybridRecipient(publicKey []byte) (*Recipient, error) { + if len(publicKey) != compressedPointSize+mlkem.EncapsulationKeySize768 { + return nil, fmt.Errorf("invalid tagpq recipient public key size %d", len(publicKey)) + } + p, err := nistec.NewP256Point().SetBytes(publicKey) + if err != nil { + return nil, fmt.Errorf("invalid tagpq recipient DH public key: %v", err) + } + k, err := ecdh.P256().NewPublicKey(p.Bytes()) + if err != nil { + return nil, fmt.Errorf("invalid tagpq recipient DH public key: %v", err) + } + pq, err := mlkem.NewEncapsulationKey768(publicKey[compressedPointSize:]) + if err != nil { + return nil, fmt.Errorf("invalid tagpq recipient PQ public key: %v", err) + } + kem, err := hpke.QSFSender(k, pq) + if err != nil { + return nil, fmt.Errorf("failed to create DHKEM sender: %v", err) + } + r := &Recipient{kem: kem, mlkem: pq} + copy(r.compressed[:], publicKey[:compressedPointSize]) + copy(r.uncompressed[:], p.Bytes()) + return r, nil +} + var p256TagLabel = []byte("age-encryption.org/p256tag") +var p256MLKEM768TagLabel = []byte("age-encryption.org/p256mlkem768tag") func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { + label, arg := p256TagLabel, "p256tag" + if r.mlkem != nil { + label, arg = p256MLKEM768TagLabel, "p256mlkem768tag" + } + enc, s, err := hpke.SetupSender(r.kem, - hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), - p256TagLabel) + hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), label) if err != nil { return nil, fmt.Errorf("failed to set up HPKE sender: %v", err) } @@ -80,13 +125,14 @@ func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { return nil, fmt.Errorf("failed to encrypt file key: %v", err) } - tag, err := hkdf.Extract(sha256.New, append(enc, r.uncompressed[:]...), p256TagLabel) + tag, err := hkdf.Extract(sha256.New, + append(enc[:uncompressedPointSize], r.uncompressed[:]...), label) if err != nil { return nil, fmt.Errorf("failed to compute tag: %v", err) } l := &age.Stanza{ - Type: "p256tag", + Type: arg, Args: []string{ format.EncodeToString(tag[:4]), format.EncodeToString(enc), @@ -99,5 +145,8 @@ func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { // String returns the Bech32 public key encoding of r. func (r *Recipient) String() string { + if r.mlkem != nil { + return plugin.EncodeRecipient("tagpq", append(r.compressed[:], r.mlkem.Bytes()...)) + } return plugin.EncodeRecipient("tag", r.compressed[:]) }