mirror of
https://github.com/XTLS/REALITY.git
synced 2025-08-23 06:58:39 +00:00
This CL adds a (very opinionated) client-side ECH implementation. In particular, if a user configures a ECHConfigList, by setting the Config.EncryptedClientHelloConfigList, but we determine that none of the configs are appropriate, we will not fallback to plaintext SNI, and will instead return an error. It is then up to the user to decide if they wish to fallback to plaintext themselves (by removing the config list). Additionally if Config.EncryptedClientHelloConfigList is provided, we will not offer TLS support lower than 1.3, since negotiating any other version, while offering ECH, is a hard error anyway. Similarly, if a user wishes to fallback to plaintext SNI by using 1.2, they may do so by removing the config list. With regard to PSK GREASE, we match the boringssl behavior, which does not include PSK identities/binders in the outer hello when doing ECH. If the server rejects ECH, we will return a ECHRejectionError error, which, if provided by the server, will contain a ECHConfigList in the RetryConfigList field containing configs that should be used if the user wishes to retry. It is up to the user to replace their existing Config.EncryptedClientHelloConfigList with the retry config list. Fixes #63369 Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf Reviewed-on: https://go-review.googlesource.com/c/go/+/578575 Reviewed-by: Ian Lance Taylor <iant@google.com> Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Than McIntosh <thanm@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org> Auto-Submit: Roland Shoemaker <roland@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
260 lines
7.0 KiB
Go
260 lines
7.0 KiB
Go
// 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/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/bits"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
// 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(suiteID []byte, salt []byte, label string, inputKey []byte) []byte {
|
|
labeledIKM := make([]byte, 0, 7+len(suiteID)+len(label)+len(inputKey))
|
|
labeledIKM = append(labeledIKM, []byte("HPKE-v1")...)
|
|
labeledIKM = append(labeledIKM, suiteID...)
|
|
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 {
|
|
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...)
|
|
out := make([]byte, length)
|
|
n, err := hkdf.Expand(kdf.hash.New, randomKey, labeledInfo).Read(out)
|
|
if err != nil || n != int(length) {
|
|
panic("hpke: LabeledExpand failed unexpectedly")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// dhKEM implements the KEM specified in RFC 9180, Section 4.1.
|
|
type dhKEM struct {
|
|
dh ecdh.Curve
|
|
kdf hkdfKDF
|
|
|
|
suiteID []byte
|
|
nSecret uint16
|
|
}
|
|
|
|
var SupportedKEMs = map[uint16]struct {
|
|
curve ecdh.Curve
|
|
hash crypto.Hash
|
|
nSecret uint16
|
|
}{
|
|
// RFC 9180 Section 7.1
|
|
0x0020: {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 {
|
|
eaePRK := dh.kdf.LabeledExtract(dh.suiteID[:], nil, "eae_prk", dhKey)
|
|
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...)
|
|
|
|
return dh.ExtractAndExpand(dhVal, kemContext), encPubEph, nil
|
|
}
|
|
|
|
type Sender struct {
|
|
aead cipher.AEAD
|
|
kem *dhKEM
|
|
|
|
sharedSecret []byte
|
|
|
|
suiteID []byte
|
|
|
|
key []byte
|
|
baseNonce []byte
|
|
exporterSecret []byte
|
|
|
|
seqNum uint128
|
|
}
|
|
|
|
var aesGCMNew = func(key []byte) (cipher.AEAD, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cipher.NewGCM(block)
|
|
}
|
|
|
|
var SupportedAEADs = map[uint16]struct {
|
|
keySize int
|
|
nonceSize int
|
|
aead func([]byte) (cipher.AEAD, error)
|
|
}{
|
|
// RFC 9180, Section 7.3
|
|
0x0001: {keySize: 16, nonceSize: 12, aead: aesGCMNew},
|
|
0x0002: {keySize: 32, nonceSize: 12, aead: aesGCMNew},
|
|
0x0003: {keySize: chacha20poly1305.KeySize, nonceSize: chacha20poly1305.NonceSize, aead: chacha20poly1305.New},
|
|
}
|
|
|
|
var SupportedKDFs = map[uint16]func() *hkdfKDF{
|
|
// RFC 9180, Section 7.2
|
|
0x0001: func() *hkdfKDF { return &hkdfKDF{crypto.SHA256} },
|
|
}
|
|
|
|
func SetupSender(kemID, kdfID, aeadID uint16, pub crypto.PublicKey, info []byte) ([]byte, *Sender, error) {
|
|
suiteID := SuiteID(kemID, kdfID, aeadID)
|
|
|
|
kem, err := newDHKem(kemID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
pubRecipient, ok := pub.(*ecdh.PublicKey)
|
|
if !ok {
|
|
return nil, nil, errors.New("incorrect public key type")
|
|
}
|
|
sharedSecret, encapsulatedKey, err := kem.Encap(pubRecipient)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
kdfInit, ok := SupportedKDFs[kdfID]
|
|
if !ok {
|
|
return nil, nil, errors.New("unsupported KDF id")
|
|
}
|
|
kdf := kdfInit()
|
|
|
|
aeadInfo, ok := SupportedAEADs[aeadID]
|
|
if !ok {
|
|
return nil, nil, errors.New("unsupported AEAD id")
|
|
}
|
|
|
|
pskIDHash := kdf.LabeledExtract(suiteID, nil, "psk_id_hash", nil)
|
|
infoHash := kdf.LabeledExtract(suiteID, nil, "info_hash", info)
|
|
ksContext := append([]byte{0}, pskIDHash...)
|
|
ksContext = append(ksContext, infoHash...)
|
|
|
|
secret := kdf.LabeledExtract(suiteID, sharedSecret, "secret", nil)
|
|
|
|
key := kdf.LabeledExpand(suiteID, secret, "key", ksContext, uint16(aeadInfo.keySize) /* Nk - key size for AEAD */)
|
|
baseNonce := kdf.LabeledExpand(suiteID, secret, "base_nonce", ksContext, uint16(aeadInfo.nonceSize) /* Nn - nonce size for AEAD */)
|
|
exporterSecret := kdf.LabeledExpand(suiteID, secret, "exp", ksContext, uint16(kdf.hash.Size()) /* Nh - hash output size of the kdf*/)
|
|
|
|
aead, err := aeadInfo.aead(key)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return encapsulatedKey, &Sender{
|
|
kem: kem,
|
|
aead: aead,
|
|
sharedSecret: sharedSecret,
|
|
suiteID: suiteID,
|
|
key: key,
|
|
baseNonce: baseNonce,
|
|
exporterSecret: exporterSecret,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Sender) nextNonce() []byte {
|
|
nonce := s.seqNum.bytes()[16-s.aead.NonceSize():]
|
|
for i := range s.baseNonce {
|
|
nonce[i] ^= s.baseNonce[i]
|
|
}
|
|
// Message limit is, according to the RFC, 2^95+1, which
|
|
// is somewhat confusing, but we do as we're told.
|
|
if s.seqNum.bitLen() >= (s.aead.NonceSize()*8)-1 {
|
|
panic("message limit reached")
|
|
}
|
|
s.seqNum = s.seqNum.addOne()
|
|
return nonce
|
|
}
|
|
|
|
func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) {
|
|
|
|
ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad)
|
|
return ciphertext, 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)
|
|
}
|
|
|
|
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
|
|
}
|