From 5cfbb640c720ede479129e0bb00b19c28e8b93fb Mon Sep 17 00:00:00 2001 From: yuhan6665 <1588741+yuhan6665@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:59:29 -0400 Subject: [PATCH] crypto/internal/hpke: modularize API and support more ciphersuites Updates #75300 Change-Id: I6a6a6964de449b36bc6f5594e08c3c47a0a2f17f Reviewed-on: https://go-review.googlesource.com/c/go/+/701435 Reviewed-by: Daniel McCarney LUCI-TryBot-Result: Go LUCI Auto-Submit: Filippo Valsorda Reviewed-by: Mark Freeman Reviewed-by: Junyang Shao --- common.go | 28 ++- ech.go | 84 ++++----- handshake_client.go | 17 +- handshake_server_tls13.go | 2 +- hpke/hpye.go | 355 -------------------------------------- 5 files changed, 59 insertions(+), 427 deletions(-) delete mode 100644 hpke/hpye.go diff --git a/common.go b/common.go index d56388f..ed0d1ee 100644 --- a/common.go +++ b/common.go @@ -914,13 +914,29 @@ type Config struct { // with a specific ECH config known to a client. type EncryptedClientHelloKey struct { // Config should be a marshalled ECHConfig associated with PrivateKey. This - // must match the config provided to clients byte-for-byte. The config - // should only specify the DHKEM(X25519, HKDF-SHA256) KEM ID (0x0020), the - // HKDF-SHA256 KDF ID (0x0001), and a subset of the following AEAD IDs: - // AES-128-GCM (0x0001), AES-256-GCM (0x0002), ChaCha20Poly1305 (0x0003). + // must match the config provided to clients byte-for-byte. The config must + // use as KEM one of + // + // - DHKEM(P-256, HKDF-SHA256) (0x0010) + // - DHKEM(P-384, HKDF-SHA384) (0x0011) + // - DHKEM(P-521, HKDF-SHA512) (0x0012) + // - DHKEM(X25519, HKDF-SHA256) (0x0020) + // + // and as KDF one of + // + // - HKDF-SHA256 (0x0001) + // - HKDF-SHA384 (0x0002) + // - HKDF-SHA512 (0x0003) + // + // and as AEAD one of + // + // - AES-128-GCM (0x0001) + // - AES-256-GCM (0x0002) + // - ChaCha20Poly1305 (0x0003) + // Config []byte - // PrivateKey should be a marshalled private key. Currently, we expect - // this to be the output of [ecdh.PrivateKey.Bytes]. + // PrivateKey should be a marshalled private key, in the format expected by + // HPKE's DeserializePrivateKey (see RFC 9180), for the KEM used in Config. PrivateKey []byte // SendAsRetry indicates if Config should be sent as part of the list of // retry configs when ECH is requested by the client but rejected by the diff --git a/ech.go b/ech.go index bf24595..da55425 100644 --- a/ech.go +++ b/ech.go @@ -6,28 +6,14 @@ package reality import ( "bytes" + "crypto/hpke" "errors" "fmt" - "slices" "strings" "golang.org/x/crypto/cryptobyte" - - "github.com/xtls/reality/hpke" ) -// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS. -// We need this so that when we insert them into ECHConfigs the ordering -// is stable. -var sortedSupportedAEADs []uint16 - -func init() { - for aeadID := range hpke.SupportedAEADs { - sortedSupportedAEADs = append(sortedSupportedAEADs, aeadID) - } - slices.Sort(sortedSupportedAEADs) -} - type EchCipher struct { KDFID uint16 AEADID uint16 @@ -163,25 +149,8 @@ func parseECHConfigList(data []byte) ([]EchConfig, error) { return configs, nil } -func pickECHConfig(list []EchConfig) *EchConfig { +func pickECHConfig(list []echConfig) (*echConfig, hpke.KEMSender, hpke.KDF, hpke.AEAD) { for _, ec := range list { - if _, ok := hpke.SupportedKEMs[ec.KemID]; !ok { - continue - } - var validSCS bool - for _, cs := range ec.SymmetricCipherSuite { - if _, ok := hpke.SupportedAEADs[cs.AEADID]; !ok { - continue - } - if _, ok := hpke.SupportedKDFs[cs.KDFID]; !ok { - continue - } - validSCS = true - break - } - if !validSCS { - continue - } if !validDNSName(string(ec.PublicName)) { continue } @@ -197,25 +166,26 @@ func pickECHConfig(list []EchConfig) *EchConfig { if unsupportedExt { continue } - return &ec - } - return nil -} - -func pickECHCipherSuite(suites []EchCipher) (EchCipher, error) { - for _, s := range suites { - // NOTE: all of the supported AEADs and KDFs are fine, rather than - // imposing some sort of preference here, we just pick the first valid - // suite. - if _, ok := hpke.SupportedAEADs[s.AEADID]; !ok { + s, err := hpke.NewKEMSender(ec.KemID, ec.PublicKey) + if err != nil { continue } - if _, ok := hpke.SupportedKDFs[s.KDFID]; !ok { - continue + for _, cs := range ec.SymmetricCipherSuite { + // All of the supported AEADs and KDFs are fine, rather than + // imposing some sort of preference here, we just pick the first + // valid suite. + kdf, err := hpke.NewKDF(cs.KDFID) + if err != nil { + continue + } + aead, err := hpke.NewAEAD(cs.AEADID) + if err != nil { + continue + } + return &ec, s, kdf, aead } - return s, nil } - return EchCipher{}, errors.New("tls: no supported symmetric ciphersuites for ECH") + return nil, nil, nil, nil } func encodeInnerClientHello(inner *clientHelloMsg, maxNameLength int) ([]byte, error) { @@ -593,18 +563,28 @@ func (c *Conn) processECHClientHello(outer *clientHelloMsg, echKeys []EncryptedC skip, config, err := parseECHConfig(echKey.Config) if err != nil || skip { c.sendAlert(alertInternalError) - return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKeys Config: %s", err) + return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config: %s", err) } if skip { continue } - echPriv, err := hpke.ParseHPKEPrivateKey(config.KemID, echKey.PrivateKey) + echPriv, err := hpke.NewKEMRecipient(config.KemID, echKey.PrivateKey) if err != nil { c.sendAlert(alertInternalError) - return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKeys PrivateKey: %s", err) + return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey PrivateKey: %s", err) + } + kdf, err := hpke.NewKDF(echCiphersuite.KDFID) + if err != nil { + c.sendAlert(alertInternalError) + return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config KDF: %s", err) + } + aead, err := hpke.NewAEAD(echCiphersuite.AEADID) + if err != nil { + c.sendAlert(alertInternalError) + return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config AEAD: %s", err) } info := append([]byte("tls ech\x00"), echKey.Config...) - hpkeContext, err := hpke.SetupRecipient(hpke.DHKEM_X25519_HKDF_SHA256, echCiphersuite.KDFID, echCiphersuite.AEADID, echPriv, info, encap) + hpkeContext, err := hpke.NewRecipient(encap, echPriv, kdf, aead, info) if err != nil { // attempt next trial decryption continue diff --git a/handshake_client.go b/handshake_client.go index 5ccc7b8..c6c9e1d 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -10,6 +10,7 @@ import ( "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/hpke" "crypto/mlkem" "crypto/rsa" "crypto/subtle" @@ -24,7 +25,6 @@ import ( "time" "github.com/xtls/reality/fips140tls" - "github.com/xtls/reality/hpke" "github.com/xtls/reality/tls13" ) @@ -205,11 +205,11 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli if err != nil { return nil, nil, nil, err } - echConfig := pickECHConfig(echConfigs) + echConfig, echPK, kdf, aead := pickECHConfig(echConfigs) if echConfig == nil { return nil, nil, nil, errors.New("tls: EncryptedClientHelloConfigList contains no valid configs") } - ech = &echClientContext{config: echConfig} + ech = &echClientContext{config: echConfig, kdfID: kdf.ID(), aeadID: aead.ID()} hello.encryptedClientHello = []byte{1} // indicate inner hello // We need to explicitly set these 1.2 fields to nil, as we do not // marshal them when encoding the inner hello, otherwise transcripts @@ -219,17 +219,8 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli hello.secureRenegotiationSupported = false hello.extendedMasterSecret = false - echPK, err := hpke.ParseHPKEPublicKey(ech.config.KemID, ech.config.PublicKey) - if err != nil { - return nil, nil, nil, err - } - suite, err := pickECHCipherSuite(ech.config.SymmetricCipherSuite) - if err != nil { - return nil, nil, nil, err - } - ech.kdfID, ech.aeadID = suite.KDFID, suite.AEADID info := append([]byte("tls ech\x00"), ech.config.raw...) - ech.encapsulatedKey, ech.hpkeContext, err = hpke.SetupSender(ech.config.KemID, suite.KDFID, suite.AEADID, echPK, info) + ech.encapsulatedKey, ech.hpkeContext, err = hpke.NewSender(echPK, kdf, aead, info) if err != nil { return nil, nil, nil, err } diff --git a/handshake_server_tls13.go b/handshake_server_tls13.go index 19ca0a4..e8063c3 100644 --- a/handshake_server_tls13.go +++ b/handshake_server_tls13.go @@ -11,6 +11,7 @@ import ( "crypto/ed25519" "crypto/hkdf" "crypto/hmac" + "crypto/hpke" "crypto/mlkem" "crypto/rand" "crypto/rsa" @@ -29,7 +30,6 @@ import ( "github.com/cloudflare/circl/sign/mldsa/mldsa65" "github.com/xtls/reality/fips140tls" - "github.com/xtls/reality/hpke" "github.com/xtls/reality/tls13" ) diff --git a/hpke/hpye.go b/hpke/hpye.go deleted file mode 100644 index 9ef26be..0000000 --- a/hpke/hpye.go +++ /dev/null @@ -1,355 +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" - "crypto/aes" - "crypto/cipher" - "crypto/ecdh" - "crypto/hkdf" - "crypto/rand" - "encoding/binary" - "errors" - "math/bits" - - "golang.org/x/crypto/chacha20poly1305" -) - -// 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, error) - -type hkdfKDF struct { - hash crypto.Hash -} - -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.New, 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.New, randomKey, string(labeledInfo), int(length)) -} - -// dhKEM implements the KEM specified in RFC 9180, Section 4.1. -type dhKEM struct { - dh ecdh.Curve - kdf hkdfKDF - - suiteID []byte - nSecret uint16 -} - -type KemID uint16 - -const DHKEM_X25519_HKDF_SHA256 = 0x0020 - -var SupportedKEMs = map[uint16]struct { - curve ecdh.Curve - hash crypto.Hash - nSecret uint16 -}{ - // RFC 9180 Section 7.1 - DHKEM_X25519_HKDF_SHA256: {ecdh.X25519(), crypto.SHA256, 32}, -} - -func newDHKem(kemID uint16) (*dhKEM, error) { - suite, ok := SupportedKEMs[kemID] - if !ok { - return nil, errors.New("unsupported suite ID") - } - return &dhKEM{ - dh: suite.curve, - kdf: hkdfKDF{suite.hash}, - suiteID: binary.BigEndian.AppendUint16([]byte("KEM"), kemID), - nSecret: suite.nSecret, - }, nil -} - -func (dh *dhKEM) ExtractAndExpand(dhKey, kemContext []byte) ([]byte, error) { - eaePRK, err := dh.kdf.LabeledExtract(dh.suiteID[:], nil, "eae_prk", dhKey) - if err != nil { - return nil, err - } - return dh.kdf.LabeledExpand(dh.suiteID[:], eaePRK, "shared_secret", kemContext, dh.nSecret) -} - -func (dh *dhKEM) Encap(pubRecipient *ecdh.PublicKey) (sharedSecret []byte, encapPub []byte, err error) { - var privEph *ecdh.PrivateKey - if testingOnlyGenerateKey != nil { - privEph, err = testingOnlyGenerateKey() - } else { - privEph, err = dh.dh.GenerateKey(rand.Reader) - } - if err != nil { - return nil, nil, err - } - dhVal, err := privEph.ECDH(pubRecipient) - if err != nil { - return nil, nil, err - } - encPubEph := privEph.PublicKey().Bytes() - - encPubRecip := pubRecipient.Bytes() - kemContext := append(encPubEph, encPubRecip...) - sharedSecret, err = dh.ExtractAndExpand(dhVal, kemContext) - if err != nil { - return nil, nil, err - } - return sharedSecret, encPubEph, nil -} - -func (dh *dhKEM) Decap(encPubEph []byte, secRecipient *ecdh.PrivateKey) ([]byte, error) { - pubEph, err := dh.dh.NewPublicKey(encPubEph) - if err != nil { - return nil, err - } - dhVal, err := secRecipient.ECDH(pubEph) - if err != nil { - return nil, err - } - kemContext := append(encPubEph, secRecipient.PublicKey().Bytes()...) - return dh.ExtractAndExpand(dhVal, kemContext) -} - -type context struct { - aead cipher.AEAD - - sharedSecret []byte - - suiteID []byte - - key []byte - baseNonce []byte - exporterSecret []byte - - seqNum uint128 -} - -type Sender struct { - *context -} - -type Recipient struct { - *context -} - -var aesGCMNew = func(key []byte) (cipher.AEAD, error) { - block, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - return cipher.NewGCM(block) -} - -type AEADID uint16 - -const ( - AEAD_AES_128_GCM = 0x0001 - AEAD_AES_256_GCM = 0x0002 - AEAD_ChaCha20Poly1305 = 0x0003 -) - -var SupportedAEADs = map[uint16]struct { - keySize int - nonceSize int - aead func([]byte) (cipher.AEAD, error) -}{ - // RFC 9180, Section 7.3 - AEAD_AES_128_GCM: {keySize: 16, nonceSize: 12, aead: aesGCMNew}, - AEAD_AES_256_GCM: {keySize: 32, nonceSize: 12, aead: aesGCMNew}, - AEAD_ChaCha20Poly1305: {keySize: chacha20poly1305.KeySize, nonceSize: chacha20poly1305.NonceSize, aead: chacha20poly1305.New}, -} - -type KDFID uint16 - -const KDF_HKDF_SHA256 = 0x0001 - -var SupportedKDFs = map[uint16]func() *hkdfKDF{ - // RFC 9180, Section 7.2 - KDF_HKDF_SHA256: func() *hkdfKDF { return &hkdfKDF{crypto.SHA256} }, -} - -func newContext(sharedSecret []byte, kemID, kdfID, aeadID uint16, info []byte) (*context, error) { - sid := suiteID(kemID, kdfID, aeadID) - - kdfInit, ok := SupportedKDFs[kdfID] - if !ok { - return nil, errors.New("unsupported KDF id") - } - kdf := kdfInit() - - aeadInfo, ok := SupportedAEADs[aeadID] - if !ok { - return nil, errors.New("unsupported 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(aeadInfo.keySize) /* Nk - key size for AEAD */) - if err != nil { - return nil, err - } - baseNonce, err := kdf.LabeledExpand(sid, secret, "base_nonce", ksContext, uint16(aeadInfo.nonceSize) /* Nn - nonce size for AEAD */) - if err != nil { - return nil, err - } - exporterSecret, err := kdf.LabeledExpand(sid, secret, "exp", ksContext, uint16(kdf.hash.Size()) /* Nh - hash output size of the kdf*/) - if err != nil { - return nil, err - } - - aead, err := aeadInfo.aead(key) - if err != nil { - return nil, err - } - - return &context{ - aead: aead, - sharedSecret: sharedSecret, - suiteID: sid, - key: key, - baseNonce: baseNonce, - exporterSecret: exporterSecret, - }, nil -} - -func SetupSender(kemID, kdfID, aeadID uint16, pub *ecdh.PublicKey, info []byte) ([]byte, *Sender, error) { - kem, err := newDHKem(kemID) - if err != nil { - return nil, nil, err - } - sharedSecret, encapsulatedKey, err := kem.Encap(pub) - if err != nil { - return nil, nil, err - } - - context, err := newContext(sharedSecret, kemID, kdfID, aeadID, info) - if err != nil { - return nil, nil, err - } - - return encapsulatedKey, &Sender{context}, nil -} - -func SetupRecipient(kemID, kdfID, aeadID uint16, priv *ecdh.PrivateKey, info, encPubEph []byte) (*Recipient, error) { - kem, err := newDHKem(kemID) - if err != nil { - return nil, err - } - sharedSecret, err := kem.Decap(encPubEph, priv) - if err != nil { - return nil, err - } - - context, err := newContext(sharedSecret, kemID, kdfID, aeadID, 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() { - // Message limit is, according to the RFC, 2^95+1, which - // is somewhat confusing, but we do as we're told. - if ctx.seqNum.bitLen() >= (ctx.aead.NonceSize()*8)-1 { - panic("message limit reached") - } - 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 -} - -func ParseHPKEPublicKey(kemID uint16, bytes []byte) (*ecdh.PublicKey, error) { - kemInfo, ok := SupportedKEMs[kemID] - if !ok { - return nil, errors.New("unsupported KEM id") - } - return kemInfo.curve.NewPublicKey(bytes) -} - -func ParseHPKEPrivateKey(kemID uint16, bytes []byte) (*ecdh.PrivateKey, error) { - kemInfo, ok := SupportedKEMs[kemID] - if !ok { - return nil, errors.New("unsupported KEM id") - } - return kemInfo.curve.NewPrivateKey(bytes) -} - -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) bitLen() int { - return bits.Len64(u.hi) + bits.Len64(u.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 -} \ No newline at end of file