diff --git a/go.mod b/go.mod index 999619c..8579734 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,16 @@ go 1.24.0 require ( filippo.io/edwards25519 v1.1.0 + filippo.io/hpke v0.4.0 filippo.io/nistec v0.0.3 - golang.org/x/crypto v0.24.0 - golang.org/x/sys v0.21.0 - golang.org/x/term v0.21.0 + golang.org/x/crypto v0.45.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 ) // 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 70c89dc..13c692d 100644 --- a/go.sum +++ b/go.sum @@ -2,17 +2,17 @@ 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/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= +filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= 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= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= diff --git a/tag/internal/hpke/hpke.go b/tag/internal/hpke/hpke.go deleted file mode 100644 index 37c55ba..0000000 --- a/tag/internal/hpke/hpke.go +++ /dev/null @@ -1,477 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package hpke - -import ( - "crypto/cipher" - "crypto/ecdh" - "crypto/hkdf" - "crypto/mlkem" - "crypto/rand" - "crypto/sha256" - "crypto/sha3" - "encoding/binary" - "errors" - "hash" - "math/bits" - - "golang.org/x/crypto/chacha20poly1305" -) - -type KEMSender interface { - Encap() (sharedSecret, enc []byte, err error) - ID() uint16 -} - -type KEMRecipient interface { - Decap(enc []byte) (sharedSecret []byte, err error) - ID() uint16 -} - -type dhKEM struct { - kdf KDF - id uint16 - nSecret uint16 -} - -func (dh *dhKEM) extractAndExpand(dhKey, kemContext []byte) ([]byte, error) { - suiteID := binary.BigEndian.AppendUint16([]byte("KEM"), dh.id) - eaePRK, err := dh.kdf.LabeledExtract(suiteID, nil, "eae_prk", dhKey) - if err != nil { - return nil, err - } - return dh.kdf.LabeledExpand(suiteID, eaePRK, "shared_secret", kemContext, dh.nSecret) -} - -func (dh *dhKEM) ID() uint16 { - return dh.id -} - -type dhkemSender struct { - dhKEM - pub *ecdh.PublicKey -} - -// DHKEMSender returns a KEMSender implementing DHKEM(P-256, HKDF-SHA256). -func DHKEMSender(pub *ecdh.PublicKey) (KEMSender, error) { - switch pub.Curve() { - case ecdh.P256(): - return &dhkemSender{ - pub: pub, - dhKEM: dhKEM{ - kdf: HKDFSHA256(), - id: 0x0010, - nSecret: 32, - }, - }, nil - default: - return nil, errors.New("unsupported curve") - } -} - -// testingOnlyGenerateKey is only used during testing, to provide -// a fixed test key to use when checking the RFC 9180 vectors. -var testingOnlyGenerateKey func() *ecdh.PrivateKey - -func (dh *dhkemSender) Encap() (sharedSecret []byte, encapPub []byte, err error) { - privEph, err := dh.pub.Curve().GenerateKey(rand.Reader) - if err != nil { - return nil, nil, err - } - if testingOnlyGenerateKey != nil { - privEph = testingOnlyGenerateKey() - } - dhVal, err := privEph.ECDH(dh.pub) - if err != nil { - return nil, nil, err - } - encPubEph := privEph.PublicKey().Bytes() - - encPubRecip := dh.pub.Bytes() - kemContext := append(encPubEph, encPubRecip...) - sharedSecret, err = dh.extractAndExpand(dhVal, kemContext) - if err != nil { - return nil, nil, err - } - return sharedSecret, encPubEph, nil -} - -type dhkemRecipient struct { - dhKEM - priv *ecdh.PrivateKey -} - -// DHKEMRecipient returns a KEMRecipient implementing DHKEM(P-256, HKDF-SHA256). -func DHKEMRecipient(priv *ecdh.PrivateKey) (KEMRecipient, error) { - switch priv.Curve() { - case ecdh.P256(): - return &dhkemRecipient{ - priv: priv, - dhKEM: dhKEM{ - kdf: HKDFSHA256(), - id: 0x0010, - nSecret: 32, - }, - }, nil - default: - return nil, errors.New("unsupported curve") - } -} - -func (dh *dhkemRecipient) Decap(encPubEph []byte) ([]byte, error) { - pubEph, err := dh.priv.Curve().NewPublicKey(encPubEph) - if err != nil { - return nil, err - } - dhVal, err := dh.priv.ECDH(pubEph) - if err != nil { - return nil, err - } - kemContext := append(encPubEph, dh.priv.PublicKey().Bytes()...) - 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) - ID() uint16 -} - -type hkdfKDF struct { - hash func() hash.Hash - id uint16 -} - -func HKDFSHA256() KDF { - return &hkdfKDF{hash: sha256.New, id: 0x0001} -} - -func (kdf *hkdfKDF) ID() uint16 { - return kdf.id -} - -func (kdf *hkdfKDF) LabeledExtract(sid []byte, salt []byte, label string, inputKey []byte) ([]byte, error) { - labeledIKM := make([]byte, 0, 7+len(sid)+len(label)+len(inputKey)) - labeledIKM = append(labeledIKM, []byte("HPKE-v1")...) - labeledIKM = append(labeledIKM, sid...) - labeledIKM = append(labeledIKM, label...) - labeledIKM = append(labeledIKM, inputKey...) - return hkdf.Extract(kdf.hash, labeledIKM, salt) -} - -func (kdf *hkdfKDF) LabeledExpand(suiteID []byte, randomKey []byte, label string, info []byte, length uint16) ([]byte, error) { - labeledInfo := make([]byte, 0, 2+7+len(suiteID)+len(label)+len(info)) - labeledInfo = binary.BigEndian.AppendUint16(labeledInfo, length) - labeledInfo = append(labeledInfo, []byte("HPKE-v1")...) - labeledInfo = append(labeledInfo, suiteID...) - labeledInfo = append(labeledInfo, label...) - labeledInfo = append(labeledInfo, info...) - return hkdf.Expand(kdf.hash, randomKey, string(labeledInfo), int(length)) -} - -type AEAD interface { - AEAD(key []byte) (cipher.AEAD, error) - KeySize() int - NonceSize() int - ID() uint16 -} - -type aead struct { - keySize int - nonceSize int - aead func([]byte) (cipher.AEAD, error) - id uint16 -} - -func ChaCha20Poly1305() AEAD { - return &aead{ - keySize: chacha20poly1305.KeySize, - nonceSize: chacha20poly1305.NonceSize, - aead: chacha20poly1305.New, - id: 0x0003, - } -} - -func (a *aead) ID() uint16 { - return a.id -} - -func (a *aead) AEAD(key []byte) (cipher.AEAD, error) { - if len(key) != a.keySize { - return nil, errors.New("invalid key size") - } - return a.aead(key) -} - -func (a *aead) KeySize() int { - return a.keySize -} - -func (a *aead) NonceSize() int { - return a.nonceSize -} - -type context struct { - aead cipher.AEAD - suiteID []byte - - key []byte - baseNonce []byte - - seqNum uint128 -} - -type Sender struct { - *context -} - -type Recipient struct { - *context -} - -func newContext(sharedSecret []byte, kemID uint16, kdf KDF, aead AEAD, info []byte) (*context, error) { - sid := suiteID(kemID, kdf.ID(), aead.ID()) - - pskIDHash, err := kdf.LabeledExtract(sid, nil, "psk_id_hash", nil) - if err != nil { - return nil, err - } - infoHash, err := kdf.LabeledExtract(sid, nil, "info_hash", info) - if err != nil { - return nil, err - } - ksContext := append([]byte{0}, pskIDHash...) - ksContext = append(ksContext, infoHash...) - - secret, err := kdf.LabeledExtract(sid, sharedSecret, "secret", nil) - if err != nil { - return nil, err - } - key, err := kdf.LabeledExpand(sid, secret, "key", ksContext, uint16(aead.KeySize())) - if err != nil { - return nil, err - } - baseNonce, err := kdf.LabeledExpand(sid, secret, "base_nonce", ksContext, uint16(aead.NonceSize())) - if err != nil { - return nil, err - } - - a, err := aead.AEAD(key) - if err != nil { - return nil, err - } - - return &context{ - aead: a, - suiteID: sid, - key: key, - baseNonce: baseNonce, - }, nil -} - -func SetupSender(kem KEMSender, kdf KDF, aead AEAD, info []byte) ([]byte, *Sender, error) { - sharedSecret, encapsulatedKey, err := kem.Encap() - if err != nil { - return nil, nil, err - } - context, err := newContext(sharedSecret, kem.ID(), kdf, aead, info) - if err != nil { - return nil, nil, err - } - return encapsulatedKey, &Sender{context}, nil -} - -func SetupRecipient(kem KEMRecipient, kdf KDF, aead AEAD, info, enc []byte) (*Recipient, error) { - sharedSecret, err := kem.Decap(enc) - if err != nil { - return nil, err - } - context, err := newContext(sharedSecret, kem.ID(), kdf, aead, info) - if err != nil { - return nil, err - } - return &Recipient{context}, nil -} - -func (ctx *context) nextNonce() []byte { - nonce := ctx.seqNum.bytes()[16-ctx.aead.NonceSize():] - for i := range ctx.baseNonce { - nonce[i] ^= ctx.baseNonce[i] - } - return nonce -} - -func (ctx *context) incrementNonce() { - ctx.seqNum = ctx.seqNum.addOne() -} - -func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) { - ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad) - s.incrementNonce() - return ciphertext, nil -} - -func (r *Recipient) Open(aad, ciphertext []byte) ([]byte, error) { - plaintext, err := r.aead.Open(nil, r.nextNonce(), ciphertext, aad) - if err != nil { - return nil, err - } - r.incrementNonce() - return plaintext, nil -} - -func suiteID(kemID, kdfID, aeadID uint16) []byte { - suiteID := make([]byte, 0, 4+2+2+2) - suiteID = append(suiteID, []byte("HPKE")...) - suiteID = binary.BigEndian.AppendUint16(suiteID, kemID) - suiteID = binary.BigEndian.AppendUint16(suiteID, kdfID) - suiteID = binary.BigEndian.AppendUint16(suiteID, aeadID) - return suiteID -} - -type uint128 struct { - hi, lo uint64 -} - -func (u uint128) addOne() uint128 { - lo, carry := bits.Add64(u.lo, 1, 0) - return uint128{u.hi + carry, lo} -} - -func (u uint128) bytes() []byte { - b := make([]byte, 16) - binary.BigEndian.PutUint64(b[0:], u.hi) - binary.BigEndian.PutUint64(b[8:], u.lo) - return b -} diff --git a/tag/internal/hpke/hpke_test.go b/tag/internal/hpke/hpke_test.go deleted file mode 100644 index 96e8977..0000000 --- a/tag/internal/hpke/hpke_test.go +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package hpke - -import ( - "bytes" - "crypto/ecdh" - "crypto/elliptic" - "crypto/mlkem" - "crypto/sha3" - "encoding/binary" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "math/big" - "os" - "testing" - - "filippo.io/mlkem768" -) - -func mustDecodeHex(t *testing.T, in string) []byte { - t.Helper() - b, err := hex.DecodeString(in) - if err != nil { - t.Fatal(err) - } - return b -} - -func TestVectors(t *testing.T) { - vectorsJSON, err := os.ReadFile("testdata/hpke-pq.json") - if err != nil { - t.Fatal(err) - } - - var vectors []struct { - 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 { - 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) - - var kemSender KEMSender - if pubPQ != nil { - kemSender, err = QSFSender(pubT, pubPQ) - } else { - kemSender, err = DHKEMSender(pubT) - } - if err != nil { - t.Fatal(err) - } - kdf, err := getKDF(vector.KDF) - if err != nil { - t.Fatal(err) - } - aead, err := getAEAD(vector.AEAD) - if err != nil { - t.Fatal(err) - } - - encapsRand := mustDecodeHex(t, vector.EncapRand) - setupEncapDerand(t, vector.KEM, encapsRand, pubPQ, kdf) - - encap, sender, err := SetupSender(kemSender, kdf, aead, info) - if err != nil { - t.Fatal(err) - } - - expectedEncap := mustDecodeHex(t, vector.Enc) - if !bytes.Equal(encap, expectedEncap) { - t.Errorf("unexpected encapsulated key, got: %x, want %x", encap, expectedEncap) - } - - privKeyBytes := mustDecodeHex(t, vector.SkRm) - privT, privQ := parsePrivateKey(t, vector.KEM, privKeyBytes) - - var kemRecipient KEMRecipient - if privQ != nil { - kemRecipient, err = QSFRecipient(privT, privQ) - } else { - kemRecipient, err = DHKEMRecipient(privT) - } - if err != nil { - t.Fatal(err) - } - recipient, err := SetupRecipient(kemRecipient, kdf, aead, info, encap) - if err != nil { - t.Fatal(err) - } - - 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) - } - 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("%s: unexpected base nonce, got: %x, want %x", name, ctx.baseNonce, expectedBaseNonce) - } - } - - 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)) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(ciphertext, expectedCiphertext) { - 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)) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(plaintext, expectedPlaintext) { - t.Errorf("unexpected plaintext: got %x want %x", plaintext, expectedPlaintext) - } - }) - } - }) - } -} - -func parsePublicKey(t *testing.T, kemID uint16, keyBytes []byte) (*ecdh.PublicKey, *mlkem.EncapsulationKey768) { - switch kemID { - case 0x0010: // DHKEM(P-256, HKDF-SHA256) - 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: - t.Fatalf("unsupported KEM %04x", kemID) - panic("unreachable") - } -} - -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) - 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: - 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") - } -} - -func getKDF(kdfID uint16) (KDF, error) { - switch kdfID { - case 0x0001: // HKDF-SHA256 - return HKDFSHA256(), nil - default: - return nil, errors.New("unsupported KDF") - } -} - -func getAEAD(aeadID uint16) (AEAD, error) { - switch aeadID { - case 0x0003: // ChaCha20Poly1305 - return ChaCha20Poly1305(), nil - default: - return nil, errors.New("unsupported AEAD") - } -} diff --git a/tag/internal/hpke/testdata/hpke-pq.json b/tag/internal/hpke/testdata/hpke-pq.json deleted file mode 100644 index caabcdc..0000000 --- a/tag/internal/hpke/testdata/hpke-pq.json +++ /dev/null @@ -1,320 +0,0 @@ -[ - { - "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/tagtest/tagtest.go b/tag/internal/tagtest/tagtest.go new file mode 100644 index 0000000..9c43718 --- /dev/null +++ b/tag/internal/tagtest/tagtest.go @@ -0,0 +1,157 @@ +// Copyright 2025 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tagtest + +import ( + "crypto/ecdh" + "crypto/hkdf" + "crypto/sha256" + "crypto/subtle" + "fmt" + "testing" + + "filippo.io/age" + "filippo.io/age/internal/format" + "filippo.io/age/tag" + "filippo.io/hpke" + "filippo.io/nistec" +) + +type ClassicIdentity struct { + t *testing.T + k hpke.PrivateKey +} + +var _ age.Identity = &ClassicIdentity{} + +func NewClassicIdentity(t *testing.T) *ClassicIdentity { + k, err := hpke.DHKEM(ecdh.P256()).GenerateKey() + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + return &ClassicIdentity{k: k} +} + +func (i *ClassicIdentity) Recipient() *tag.Recipient { + uncompressed := i.k.PublicKey().Bytes() + p, err := nistec.NewP256Point().SetBytes(uncompressed) + if err != nil { + i.t.Fatalf("failed to parse public key: %v", err) + } + r, err := tag.NewClassicRecipient(p.BytesCompressed()) + if err != nil { + i.t.Fatalf("failed to create recipient: %v", err) + } + return r +} + +func (i *ClassicIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) { + for _, s := range ss { + if s.Type != "p256tag" { + continue + } + if len(s.Args) != 2 { + return nil, fmt.Errorf("malformed stanza") + } + tagArg, err := format.DecodeString(s.Args[0]) + if err != nil { + return nil, fmt.Errorf("malformed tag: %v", err) + } + if len(tagArg) != 4 { + return nil, fmt.Errorf("invalid tag length: %d", len(tagArg)) + } + enc, err := format.DecodeString(s.Args[1]) + if err != nil { + return nil, fmt.Errorf("malformed encapsulated key: %v", err) + } + if len(enc) != 65 { + return nil, fmt.Errorf("invalid encapsulated key length: %d", len(enc)) + } + if len(s.Body) != 32 { + return nil, fmt.Errorf("invalid encrypted file key length: %d", len(s.Body)) + } + + expTag, err := hkdf.Extract(sha256.New, append(enc, i.k.PublicKey().Bytes()...), []byte("age-encryption.org/p256tag")) + if err != nil { + return nil, fmt.Errorf("failed to compute tag: %v", err) + } + if subtle.ConstantTimeCompare(tagArg, expTag[:4]) != 1 { + return nil, age.ErrIncorrectIdentity + } + + r, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("age-encryption.org/p256tag")) + if err != nil { + return nil, fmt.Errorf("failed to unwrap file key: %v", err) + } + return r.Open(nil, s.Body) + } + return nil, age.ErrIncorrectIdentity +} + +type HybridIdentity struct { + t *testing.T + k hpke.PrivateKey +} + +var _ age.Identity = &HybridIdentity{} + +func NewHybridIdentity(t *testing.T) *HybridIdentity { + k, err := hpke.MLKEM768P256().GenerateKey() + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + return &HybridIdentity{k: k} +} + +func (i *HybridIdentity) Recipient() *tag.Recipient { + r, err := tag.NewHybridRecipient(i.k.PublicKey().Bytes()) + if err != nil { + i.t.Fatalf("failed to create recipient: %v", err) + } + return r +} + +func (i *HybridIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) { + for _, s := range ss { + if s.Type != "mlkem768p256tag" { + continue + } + if len(s.Args) != 2 { + return nil, fmt.Errorf("malformed stanza") + } + tagArg, err := format.DecodeString(s.Args[0]) + if err != nil { + return nil, fmt.Errorf("malformed tag: %v", err) + } + if len(tagArg) != 4 { + return nil, fmt.Errorf("invalid tag length: %d", len(tagArg)) + } + enc, err := format.DecodeString(s.Args[1]) + if err != nil { + return nil, fmt.Errorf("malformed encapsulated key: %v", err) + } + if len(enc) != 1153 { + return nil, fmt.Errorf("invalid encapsulated key length: %d", len(enc)) + } + if len(s.Body) != 32 { + return nil, fmt.Errorf("invalid encrypted file key length: %d", len(s.Body)) + } + + expTag, err := hkdf.Extract(sha256.New, append(enc[1088:], i.k.PublicKey().Bytes()[1184:]...), []byte("age-encryption.org/mlkem768p256tag")) + if err != nil { + return nil, fmt.Errorf("failed to compute tag: %v", err) + } + if subtle.ConstantTimeCompare(tagArg, expTag[:4]) != 1 { + return nil, age.ErrIncorrectIdentity + } + + r, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("age-encryption.org/mlkem768p256tag")) + if err != nil { + return nil, fmt.Errorf("failed to unwrap file key: %v", err) + } + return r.Open(nil, s.Body) + } + return nil, age.ErrIncorrectIdentity +} diff --git a/tag/tag.go b/tag/tag.go index ec8d9c7..1db3354 100644 --- a/tag/tag.go +++ b/tag/tag.go @@ -2,6 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package tag implements tagged P-256 or hybrid P-256 + ML-KEM-768 recipients, +// which can be used with identities stored on hardware keys, usually supported +// by dedicated plugins. +// +// The tag reduces privacy, by allowing an observer to correlate files with a +// recipient (but not files amongst them without knowledge of the recipient), +// but this is also a desirable property for hardware keys that require user +// interaction for each decryption operation. package tag import ( @@ -14,16 +22,17 @@ import ( "filippo.io/age" "filippo.io/age/internal/format" "filippo.io/age/plugin" - "filippo.io/age/tag/internal/hpke" + "filippo.io/hpke" "filippo.io/nistec" ) +// Recipient is a tagged P-256 or hybrid P-256 + ML-KEM-768 recipient. +// +// The latter recipient is safe against future cryptographically-relevant +// quantum computers, and can only be used along with other post-quantum +// recipients. type Recipient struct { - kem hpke.KEMSender - - mlkem *mlkem.EncapsulationKey768 - compressed [compressedPointSize]byte - uncompressed [uncompressedPointSize]byte + pk hpke.PublicKey } var _ age.Recipient = &Recipient{} @@ -37,7 +46,7 @@ func ParseRecipient(s string) (*Recipient, error) { } switch t { case "tag": - r, err := NewRecipient(k) + r, err := NewClassicRecipient(k) if err != nil { return nil, fmt.Errorf("malformed recipient %q: %v", s, err) } @@ -54,10 +63,9 @@ func ParseRecipient(s string) (*Recipient, error) { } 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) { +// NewClassicRecipient returns a new P-256 [Recipient] from a raw public key. +func NewClassicRecipient(publicKey []byte) (*Recipient, error) { if len(publicKey) != compressedPointSize { return nil, fmt.Errorf("invalid tag recipient public key size %d", len(publicKey)) } @@ -65,70 +73,64 @@ func NewRecipient(publicKey []byte) (*Recipient, error) { if err != nil { return nil, fmt.Errorf("invalid tag recipient public key: %v", err) } - k, err := ecdh.P256().NewPublicKey(p.Bytes()) + k, err := hpke.DHKEM(ecdh.P256()).NewPublicKey(p.Bytes()) if err != nil { return nil, fmt.Errorf("invalid tag recipient public key: %v", err) } - kem, err := hpke.DHKEMSender(k) - if err != nil { - return nil, fmt.Errorf("failed to create DHKEM sender: %v", err) - } - r := &Recipient{kem: kem} - copy(r.compressed[:], publicKey) - copy(r.uncompressed[:], p.Bytes()) - return r, nil + return &Recipient{k}, nil } -// NewHybridRecipient returns a new [Recipient] from raw concatenated public keys. +// NewHybridRecipient returns a new hybrid P-256 + ML-KEM-768 [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) + k, err := hpke.MLKEM768P256().NewPublicKey(publicKey) if err != nil { - return nil, fmt.Errorf("invalid tagpq recipient DH public key: %v", err) + return nil, fmt.Errorf("invalid tagpq recipient 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 + return &Recipient{k}, nil } -var p256TagLabel = []byte("age-encryption.org/p256tag") -var p256MLKEM768TagLabel = []byte("age-encryption.org/p256mlkem768tag") +// Hybrid reports whether r is a hybrid P-256 + ML-KEM-768 recipient. +func (r *Recipient) Hybrid() bool { + return r.pk.KEM().ID() == hpke.MLKEM768P256().ID() +} func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { - label, arg := p256TagLabel, "p256tag" - if r.mlkem != nil { - label, arg = p256MLKEM768TagLabel, "p256mlkem768tag" + s, _, err := r.WrapWithLabels(fileKey) + return s, err +} + +// WrapWithLabels implements [age.RecipientWithLabels], returning a single +// "postquantum" label if r is a hybrid P-256 + ML-KEM-768 recipient. This +// ensures a hybrid Recipient can't be mixed with other recipients that would +// defeat its post-quantum security. +// +// To unsafely bypass this restriction, wrap Recipient in an [age.Recipient] +// type that doesn't expose WrapWithLabels. +func (r *Recipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, error) { + label, arg := "age-encryption.org/p256tag", "p256tag" + if r.Hybrid() { + label, arg = "age-encryption.org/mlkem768p256tag", "mlkem768p256tag" } - enc, s, err := hpke.SetupSender(r.kem, - hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), label) + enc, s, err := hpke.NewSender(r.pk, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(label)) if err != nil { - return nil, fmt.Errorf("failed to set up HPKE sender: %v", err) + return nil, nil, fmt.Errorf("failed to set up HPKE sender: %v", err) } ct, err := s.Seal(nil, fileKey) if err != nil { - return nil, fmt.Errorf("failed to encrypt file key: %v", err) + return nil, nil, fmt.Errorf("failed to encrypt file key: %v", err) } - tag, err := hkdf.Extract(sha256.New, - append(enc[:uncompressedPointSize], r.uncompressed[:]...), label) + tagEnc, tagRecipient := enc, r.pk.Bytes() + if r.Hybrid() { + // In hybrid mode, the tag is computed over just the P-256 part. + tagEnc = enc[mlkem.CiphertextSize768:] + tagRecipient = tagRecipient[mlkem.EncapsulationKeySize768:] + } + tag, err := hkdf.Extract(sha256.New, append(tagEnc, tagRecipient...), []byte(label)) if err != nil { - return nil, fmt.Errorf("failed to compute tag: %v", err) + return nil, nil, fmt.Errorf("failed to compute tag: %v", err) } l := &age.Stanza{ @@ -140,13 +142,20 @@ func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { Body: ct, } - return []*age.Stanza{l}, nil + if r.Hybrid() { + return []*age.Stanza{l}, []string{"postquantum"}, nil + } + return []*age.Stanza{l}, nil, nil } // 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()...)) + if r.Hybrid() { + return plugin.EncodeRecipient("tagpq", r.pk.Bytes()) } - return plugin.EncodeRecipient("tag", r.compressed[:]) + p, err := nistec.NewP256Point().SetBytes(r.pk.Bytes()) + if err != nil { + panic("internal error: invalid P-256 public key") + } + return plugin.EncodeRecipient("tag", p.BytesCompressed()) } diff --git a/tag/tag_test.go b/tag/tag_test.go new file mode 100644 index 0000000..dcbe931 --- /dev/null +++ b/tag/tag_test.go @@ -0,0 +1,109 @@ +// Copyright 2025 The age Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag_test + +import ( + "bytes" + "io" + "testing" + + "filippo.io/age" + "filippo.io/age/tag" + "filippo.io/age/tag/internal/tagtest" +) + +func TestClassicRoundTrip(t *testing.T) { + i := tagtest.NewClassicIdentity(t) + r := i.Recipient() + + if r.Hybrid() { + t.Error("classic recipient incorrectly reports as hybrid") + } + + r1, err := tag.ParseRecipient(r.String()) + if err != nil { + t.Fatal(err) + } + if r1.String() != r.String() { + t.Errorf("recipient did not round-trip through parsing: got %q, want %q", r1.String(), r.String()) + } + if r1.Hybrid() { + t.Error("parsed classic recipient incorrectly reports as hybrid") + } + + plaintext := []byte("hello world") + + encrypted := &bytes.Buffer{} + w, err := age.Encrypt(encrypted, r) + if err != nil { + t.Fatal(err) + } + if _, err := w.Write(plaintext); err != nil { + t.Fatal(err) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + decrypted, err := age.Decrypt(encrypted, i) + if err != nil { + t.Fatal(err) + } + out, err := io.ReadAll(decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(plaintext, out) { + t.Errorf("invalid output: %q, expected %q", out, plaintext) + } +} + +func TestHybridRoundTrip(t *testing.T) { + i := tagtest.NewHybridIdentity(t) + r := i.Recipient() + + if !r.Hybrid() { + t.Error("hybrid recipient incorrectly reports as classic") + } + + r1, err := tag.ParseRecipient(r.String()) + if err != nil { + t.Fatal(err) + } + if r1.String() != r.String() { + t.Errorf("recipient did not round-trip through parsing: got %q, want %q", r1.String(), r.String()) + } + if !r1.Hybrid() { + t.Error("parsed hybrid recipient incorrectly reports as classic") + } + + plaintext := []byte("hello world") + + encrypted := &bytes.Buffer{} + w, err := age.Encrypt(encrypted, r) + if err != nil { + t.Fatal(err) + } + if _, err := w.Write(plaintext); err != nil { + t.Fatal(err) + } + if err := w.Close(); err != nil { + t.Fatal(err) + } + + decrypted, err := age.Decrypt(encrypted, i) + if err != nil { + t.Fatal(err) + } + out, err := io.ReadAll(decrypted) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(plaintext, out) { + t.Errorf("invalid output: %q, expected %q", out, plaintext) + } +}