diff --git a/alert.go b/alert.go index b1ea91b..577cddc 100644 --- a/alert.go +++ b/alert.go @@ -58,6 +58,7 @@ const ( alertUnknownPSKIdentity alert = 115 alertCertificateRequired alert = 116 alertNoApplicationProtocol alert = 120 + alertECHRequired alert = 121 ) var alertText = map[alert]string{ @@ -94,6 +95,7 @@ var alertText = map[alert]string{ alertUnknownPSKIdentity: "unknown PSK identity", alertCertificateRequired: "certificate required", alertNoApplicationProtocol: "no application protocol", + alertECHRequired: "encrypted client hello required", } func (e alert) String() string { diff --git a/common.go b/common.go index 8b99cf5..f628ba3 100644 --- a/common.go +++ b/common.go @@ -124,6 +124,8 @@ const ( extensionKeyShare uint16 = 51 extensionQUICTransportParameters uint16 = 57 extensionRenegotiationInfo uint16 = 0xff01 + extensionECHOuterExtensions uint16 = 0xfd00 + extensionEncryptedClientHello uint16 = 0xfe0d ) // TLS signaling cipher suite values @@ -286,6 +288,11 @@ type ConnectionState struct { // resumed connections that don't support Extended Master Secret (RFC 7627). TLSUnique []byte + // ECHAccepted indicates if Encrypted Client Hello was offered by the client + // and accepted by the server. Currently, ECH is supported only on the + // client side. + ECHAccepted bool + // ekm is a closure exposed via ExportKeyingMaterial. ekm func(label string, context []byte, length int) ([]byte, error) @@ -790,6 +797,41 @@ type Config struct { // used for debugging. KeyLogWriter io.Writer + // EncryptedClientHelloConfigList is a serialized ECHConfigList. If + // provided, clients will attempt to connect to servers using Encrypted + // Client Hello (ECH) using one of the provided ECHConfigs. Servers + // currently ignore this field. + // + // If the list contains no valid ECH configs, the handshake will fail + // and return an error. + // + // If EncryptedClientHelloConfigList is set, MinVersion, if set, must + // be VersionTLS13. + // + // When EncryptedClientHelloConfigList is set, the handshake will only + // succeed if ECH is sucessfully negotiated. If the server rejects ECH, + // an ECHRejectionError error will be returned, which may contain a new + // ECHConfigList that the server suggests using. + // + // How this field is parsed may change in future Go versions, if the + // encoding described in the final Encrypted Client Hello RFC changes. + EncryptedClientHelloConfigList []byte + + // EncryptedClientHelloRejectionVerify, if not nil, is called when ECH is + // rejected, in order to verify the ECH provider certificate in the outer + // Client Hello. If it returns a non-nil error, the handshake is aborted and + // that error results. + // + // Unlike VerifyPeerCertificate and VerifyConnection, normal certificate + // verification will not be performed before calling + // EncryptedClientHelloRejectionVerify. + // + // If EncryptedClientHelloRejectionVerify is nil and ECH is rejected, the + // roots in RootCAs will be used to verify the ECH providers public + // certificate. VerifyPeerCertificate and VerifyConnection are not called + // when ECH is rejected, even if set, and InsecureSkipVerify is ignored. + EncryptedClientHelloRejectionVerify func(ConnectionState) error + // mutex protects sessionTicketKeys and autoSessionTicketKeys. mutex sync.RWMutex // sessionTicketKeys contains zero or more ticket keys. If set, it means @@ -849,47 +891,49 @@ func (c *Config) Clone() *Config { c.mutex.RLock() defer c.mutex.RUnlock() return &Config{ - DialContext: c.DialContext, - Show: c.Show, - Type: c.Type, - Dest: c.Dest, - Xver: c.Xver, - ServerNames: c.ServerNames, - PrivateKey: c.PrivateKey, - MinClientVer: c.MinClientVer, - MaxClientVer: c.MaxClientVer, - MaxTimeDiff: c.MaxTimeDiff, - ShortIds: c.ShortIds, - Rand: c.Rand, - Time: c.Time, - Certificates: c.Certificates, - NameToCertificate: c.NameToCertificate, - GetCertificate: c.GetCertificate, - GetClientCertificate: c.GetClientCertificate, - GetConfigForClient: c.GetConfigForClient, - VerifyPeerCertificate: c.VerifyPeerCertificate, - VerifyConnection: c.VerifyConnection, - RootCAs: c.RootCAs, - NextProtos: c.NextProtos, - ServerName: c.ServerName, - ClientAuth: c.ClientAuth, - ClientCAs: c.ClientCAs, - InsecureSkipVerify: c.InsecureSkipVerify, - CipherSuites: c.CipherSuites, - PreferServerCipherSuites: c.PreferServerCipherSuites, - SessionTicketsDisabled: c.SessionTicketsDisabled, - SessionTicketKey: c.SessionTicketKey, - ClientSessionCache: c.ClientSessionCache, - UnwrapSession: c.UnwrapSession, - WrapSession: c.WrapSession, - MinVersion: c.MinVersion, - MaxVersion: c.MaxVersion, - CurvePreferences: c.CurvePreferences, - DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled, - Renegotiation: c.Renegotiation, - KeyLogWriter: c.KeyLogWriter, - sessionTicketKeys: c.sessionTicketKeys, - autoSessionTicketKeys: c.autoSessionTicketKeys, + DialContext: c.DialContext, + Show: c.Show, + Type: c.Type, + Dest: c.Dest, + Xver: c.Xver, + ServerNames: c.ServerNames, + PrivateKey: c.PrivateKey, + MinClientVer: c.MinClientVer, + MaxClientVer: c.MaxClientVer, + MaxTimeDiff: c.MaxTimeDiff, + ShortIds: c.ShortIds, + Rand: c.Rand, + Time: c.Time, + Certificates: c.Certificates, + NameToCertificate: c.NameToCertificate, + GetCertificate: c.GetCertificate, + GetClientCertificate: c.GetClientCertificate, + GetConfigForClient: c.GetConfigForClient, + VerifyPeerCertificate: c.VerifyPeerCertificate, + VerifyConnection: c.VerifyConnection, + RootCAs: c.RootCAs, + NextProtos: c.NextProtos, + ServerName: c.ServerName, + ClientAuth: c.ClientAuth, + ClientCAs: c.ClientCAs, + InsecureSkipVerify: c.InsecureSkipVerify, + CipherSuites: c.CipherSuites, + PreferServerCipherSuites: c.PreferServerCipherSuites, + SessionTicketsDisabled: c.SessionTicketsDisabled, + SessionTicketKey: c.SessionTicketKey, + ClientSessionCache: c.ClientSessionCache, + UnwrapSession: c.UnwrapSession, + WrapSession: c.WrapSession, + MinVersion: c.MinVersion, + MaxVersion: c.MaxVersion, + CurvePreferences: c.CurvePreferences, + DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled, + Renegotiation: c.Renegotiation, + KeyLogWriter: c.KeyLogWriter, + EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList, + EncryptedClientHelloRejectionVerify: c.EncryptedClientHelloRejectionVerify, + sessionTicketKeys: c.sessionTicketKeys, + autoSessionTicketKeys: c.autoSessionTicketKeys, } } @@ -1072,6 +1116,9 @@ func (c *Config) supportedVersions(isClient bool) []uint16 { if (c == nil || c.MinVersion == 0) && v < VersionTLS12 { continue } + if isClient && c.EncryptedClientHelloConfigList != nil && v < VersionTLS13 { + continue + } if c != nil && c.MinVersion != 0 && v < c.MinVersion { continue } diff --git a/conn.go b/conn.go index 868792f..3db488d 100644 --- a/conn.go +++ b/conn.go @@ -75,6 +75,7 @@ type Conn struct { // resumptionSecret is the resumption_master_secret for handling // or sending NewSessionTicket messages. resumptionSecret []byte + echAccepted bool // ticketKeys is the set of active session ticket keys for this // connection. The first one is used to encrypt new tickets and @@ -1713,6 +1714,7 @@ func (c *Conn) connectionStateLocked() ConnectionState { } else { state.ekm = c.ekm } + state.ECHAccepted = c.echAccepted return state } diff --git a/ech.go b/ech.go new file mode 100644 index 0000000..56477fb --- /dev/null +++ b/ech.go @@ -0,0 +1,284 @@ +// 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 reality + +import ( + "errors" + "strings" + + "golang.org/x/crypto/cryptobyte" + + "github.com/xtls/reality/hpke" +) + +type echCipher struct { + KDFID uint16 + AEADID uint16 +} + +type echExtension struct { + Type uint16 + Data []byte +} + +type echConfig struct { + raw []byte + + Version uint16 + Length uint16 + + ConfigID uint8 + KemID uint16 + PublicKey []byte + SymmetricCipherSuite []echCipher + + MaxNameLength uint8 + PublicName []byte + Extensions []echExtension +} + +var errMalformedECHConfig = errors.New("tls: malformed ECHConfigList") + +// parseECHConfigList parses a draft-ietf-tls-esni-18 ECHConfigList, returning a +// slice of parsed ECHConfigs, in the same order they were parsed, or an error +// if the list is malformed. +func parseECHConfigList(data []byte) ([]echConfig, error) { + s := cryptobyte.String(data) + // Skip the length prefix + var length uint16 + if !s.ReadUint16(&length) { + return nil, errMalformedECHConfig + } + if length != uint16(len(data)-2) { + return nil, errMalformedECHConfig + } + var configs []echConfig + for len(s) > 0 { + var ec echConfig + ec.raw = []byte(s) + if !s.ReadUint16(&ec.Version) { + return nil, errMalformedECHConfig + } + if !s.ReadUint16(&ec.Length) { + return nil, errMalformedECHConfig + } + if len(ec.raw) < int(ec.Length)+4 { + return nil, errMalformedECHConfig + } + ec.raw = ec.raw[:ec.Length+4] + if ec.Version != extensionEncryptedClientHello { + s.Skip(int(ec.Length)) + continue + } + if !s.ReadUint8(&ec.ConfigID) { + return nil, errMalformedECHConfig + } + if !s.ReadUint16(&ec.KemID) { + return nil, errMalformedECHConfig + } + if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&ec.PublicKey)) { + return nil, errMalformedECHConfig + } + var cipherSuites cryptobyte.String + if !s.ReadUint16LengthPrefixed(&cipherSuites) { + return nil, errMalformedECHConfig + } + for !cipherSuites.Empty() { + var c echCipher + if !cipherSuites.ReadUint16(&c.KDFID) { + return nil, errMalformedECHConfig + } + if !cipherSuites.ReadUint16(&c.AEADID) { + return nil, errMalformedECHConfig + } + ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c) + } + if !s.ReadUint8(&ec.MaxNameLength) { + return nil, errMalformedECHConfig + } + var publicName cryptobyte.String + if !s.ReadUint8LengthPrefixed(&publicName) { + return nil, errMalformedECHConfig + } + ec.PublicName = publicName + var extensions cryptobyte.String + if !s.ReadUint16LengthPrefixed(&extensions) { + return nil, errMalformedECHConfig + } + for !extensions.Empty() { + var e echExtension + if !extensions.ReadUint16(&e.Type) { + return nil, errMalformedECHConfig + } + if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) { + return nil, errMalformedECHConfig + } + ec.Extensions = append(ec.Extensions, e) + } + + configs = append(configs, ec) + } + return configs, nil +} + +func pickECHConfig(list []echConfig) *echConfig { + 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 + } + var unsupportedExt bool + for _, ext := range ec.Extensions { + // If high order bit is set to 1 the extension is mandatory. + // Since we don't support any extensions, if we see a mandatory + // bit, we skip the config. + if ext.Type&uint16(1<<15) != 0 { + unsupportedExt = true + } + } + 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 { + continue + } + if _, ok := hpke.SupportedKDFs[s.KDFID]; !ok { + continue + } + return s, nil + } + return echCipher{}, errors.New("tls: no supported symmetric ciphersuites for ECH") +} + +func encodeInnerClientHello(inner *clientHelloMsg, maxNameLength int) ([]byte, error) { + h, err := inner.marshalMsg(true) + if err != nil { + return nil, err + } + h = h[4:] // strip four byte prefix + + var paddingLen int + if inner.serverName != "" { + paddingLen = max(0, maxNameLength-len(inner.serverName)) + } else { + paddingLen = maxNameLength + 9 + } + paddingLen = 31 - ((len(h) + paddingLen - 1) % 32) + + return append(h, make([]byte, paddingLen)...), nil +} + +func generateOuterECHExt(id uint8, kdfID, aeadID uint16, encodedKey []byte, payload []byte) ([]byte, error) { + var b cryptobyte.Builder + b.AddUint8(0) // outer + b.AddUint16(kdfID) + b.AddUint16(aeadID) + b.AddUint8(id) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(encodedKey) }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(payload) }) + return b.Bytes() +} + +func computeAndUpdateOuterECHExtension(outer, inner *clientHelloMsg, ech *echContext, useKey bool) error { + var encapKey []byte + if useKey { + encapKey = ech.encapsulatedKey + } + encodedInner, err := encodeInnerClientHello(inner, int(ech.config.MaxNameLength)) + if err != nil { + return err + } + // NOTE: the tag lengths for all of the supported AEADs are the same (16 + // bytes), so we have hardcoded it here. If we add support for another AEAD + // with a different tag length, we will need to change this. + encryptedLen := len(encodedInner) + 16 // AEAD tag length + outer.encryptedClientHello, err = generateOuterECHExt(ech.config.ConfigID, ech.kdfID, ech.aeadID, encapKey, make([]byte, encryptedLen)) + if err != nil { + return err + } + serializedOuter, err := outer.marshal() + if err != nil { + return err + } + serializedOuter = serializedOuter[4:] // strip the four byte prefix + encryptedInner, err := ech.hpkeContext.Seal(serializedOuter, encodedInner) + if err != nil { + return err + } + outer.encryptedClientHello, err = generateOuterECHExt(ech.config.ConfigID, ech.kdfID, ech.aeadID, encapKey, encryptedInner) + if err != nil { + return err + } + return nil +} + +// validDNSName is a rather rudimentary check for the validity of a DNS name. +// This is used to check if the public_name in a ECHConfig is valid when we are +// picking a config. This can be somewhat lax because even if we pick a +// valid-looking name, the DNS layer will later reject it anyway. +func validDNSName(name string) bool { + if len(name) > 253 { + return false + } + labels := strings.Split(name, ".") + if len(labels) <= 1 { + return false + } + for _, l := range labels { + labelLen := len(l) + if labelLen == 0 { + return false + } + for i, r := range l { + if r == '-' && (i == 0 || i == labelLen-1) { + return false + } + if (r < '0' || r > '9') && (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '-' { + return false + } + } + } + return true +} + +// ECHRejectionError is the error type returned when ECH is rejected by a remote +// server. If the server offered a ECHConfigList to use for retries, the +// RetryConfigList field will contain this list. +// +// The client may treat an ECHRejectionError with an empty set of RetryConfigs +// as a secure signal from the server. +type ECHRejectionError struct { + RetryConfigList []byte +} + +func (e *ECHRejectionError) Error() string { + return "tls: server rejected ECH" +} \ No newline at end of file diff --git a/handshake_client.go b/handshake_client.go index dd562a2..153e002 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/xtls/reality/hpke" "github.com/xtls/reality/mlkem768" ) @@ -39,27 +40,27 @@ type clientHandshakeState struct { var testingOnlyForceClientHelloSignatureAlgorithms []SignatureScheme -func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) { +func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echContext, error) { config := c.config if len(config.ServerName) == 0 && !config.InsecureSkipVerify { - return nil, nil, errors.New("tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config") + return nil, nil, nil, errors.New("tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config") } nextProtosLength := 0 for _, proto := range config.NextProtos { if l := len(proto); l == 0 || l > 255 { - return nil, nil, errors.New("tls: invalid NextProtos value") + return nil, nil, nil, errors.New("tls: invalid NextProtos value") } else { nextProtosLength += 1 + l } } if nextProtosLength > 0xffff { - return nil, nil, errors.New("tls: NextProtos values too large") + return nil, nil, nil, errors.New("tls: NextProtos values too large") } supportedVersions := config.supportedVersions(roleClient) if len(supportedVersions) == 0 { - return nil, nil, errors.New("tls: no supported versions satisfy MinVersion and MaxVersion") + return nil, nil, nil, errors.New("tls: no supported versions satisfy MinVersion and MaxVersion") } maxVersion := config.maxSupportedVersion(roleClient) @@ -112,7 +113,7 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) _, err := io.ReadFull(config.rand(), hello.random) if err != nil { - return nil, nil, errors.New("tls: short read from Rand: " + err.Error()) + return nil, nil, nil, errors.New("tls: short read from Rand: " + err.Error()) } // A random session ID is used to detect when the server accepted a ticket @@ -123,7 +124,7 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) if c.quic == nil { hello.sessionId = make([]byte, 32) if _, err := io.ReadFull(config.rand(), hello.sessionId); err != nil { - return nil, nil, errors.New("tls: short read from Rand: " + err.Error()) + return nil, nil, nil, errors.New("tls: short read from Rand: " + err.Error()) } } @@ -151,15 +152,15 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) if curveID == x25519Kyber768Draft00 { keyShareKeys.ecdhe, err = generateECDHEKey(config.rand(), X25519) if err != nil { - return nil, nil, err + return nil, nil, nil, err } seed := make([]byte, mlkem768.SeedSize) if _, err := io.ReadFull(config.rand(), seed); err != nil { - return nil, nil, err + return nil, nil, nil, err } keyShareKeys.kyber, err = mlkem768.NewKeyFromSeed(seed) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // For draft-tls-westerbaan-xyber768d00-03, we send both a hybrid // and a standard X25519 key share, since most servers will only @@ -172,11 +173,11 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) } } else { if _, ok := curveForCurveID(curveID); !ok { - return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") + return nil, nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") } keyShareKeys.ecdhe, err = generateECDHEKey(config.rand(), curveID) if err != nil { - return nil, nil, err + return nil, nil, nil, err } hello.keyShares = []keyShare{{group: curveID, data: keyShareKeys.ecdhe.PublicKey().Bytes()}} } @@ -185,7 +186,7 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) if c.quic != nil { p, err := c.quicGetTransportParameters() if err != nil { - return nil, nil, err + return nil, nil, nil, err } if p == nil { p = []byte{} @@ -193,7 +194,60 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, error) hello.quicTransportParameters = p } - return hello, keyShareKeys, nil + var ech *echContext + if c.config.EncryptedClientHelloConfigList != nil { + if c.config.MinVersion != 0 && c.config.MinVersion < VersionTLS13 { + return nil, nil, nil, errors.New("tls: MinVersion must be >= VersionTLS13 if EncryptedClientHelloConfigList is populated") + } + if c.config.MaxVersion != 0 && c.config.MaxVersion <= VersionTLS12 { + return nil, nil, nil, errors.New("tls: MaxVersion must be >= VersionTLS13 if EncryptedClientHelloConfigList is populated") + } + echConfigs, err := parseECHConfigList(c.config.EncryptedClientHelloConfigList) + if err != nil { + return nil, nil, nil, err + } + echConfig := pickECHConfig(echConfigs) + if echConfig == nil { + return nil, nil, nil, errors.New("tls: EncryptedClientHelloConfigList contains no valid configs") + } + ech = &echContext{config: echConfig} + 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 + // will later mismatch. + hello.supportedPoints = nil + hello.ticketSupported = false + 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) + if err != nil { + return nil, nil, nil, err + } + } + + return hello, keyShareKeys, ech, nil +} + +type echContext struct { + config *echConfig + hpkeContext *hpke.Sender + encapsulatedKey []byte + innerHello *clientHelloMsg + innerTranscript hash.Hash + kdfID uint16 + aeadID uint16 + echRejected bool } func (c *Conn) clientHandshake(ctx context.Context) (err error) { @@ -205,7 +259,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { // need to be reset. c.didResume = false - hello, keyShareKeys, err := c.makeClientHello() + hello, keyShareKeys, ech, err := c.makeClientHello() if err != nil { return err } @@ -231,6 +285,31 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { }() } + if ech != nil { + // Split hello into inner and outer + ech.innerHello = hello.clone() + + // Overwrite the server name in the outer hello with the public facing + // name. + hello.serverName = string(ech.config.PublicName) + // Generate a new random for the outer hello. + hello.random = make([]byte, 32) + _, err = io.ReadFull(c.config.rand(), hello.random) + if err != nil { + return errors.New("tls: short read from Rand: " + err.Error()) + } + + // NOTE: we don't do PSK GREASE, in line with boringssl, it's meant to + // work around _possibly_ broken middleboxes, but there is little-to-no + // evidence that this is actually a problem. + + if err := computeAndUpdateOuterECHExtension(hello, ech.innerHello, ech, true); err != nil { + return err + } + } + + c.serverName = hello.serverName + if _, err := c.writeHandshakeRecord(hello, nil); err != nil { return err } @@ -283,6 +362,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { session: session, earlySecret: earlySecret, binderKey: binderKey, + echContext: ech, } return hs.handshake() } @@ -304,7 +384,11 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( return nil, nil, nil, nil } - hello.ticketSupported = true + echInner := bytes.Equal(hello.encryptedClientHello, []byte{1}) + + // ticketSupported is a TLS 1.2 extension (as TLS 1.3 replaced tickets with PSK + // identities) and ECH requires and forces TLS 1.3. + hello.ticketSupported = true && !echInner if hello.supportedVersions[0] == VersionTLS13 { // Require DHE on resumption as it guarantees forward secrecy against @@ -423,13 +507,7 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( earlySecret = cipherSuite.extract(session.secret, nil) binderKey = cipherSuite.deriveSecret(earlySecret, resumptionBinderLabel, nil) transcript := cipherSuite.hash.New() - helloBytes, err := hello.marshalWithoutBinders() - if err != nil { - return nil, nil, nil, err - } - transcript.Write(helloBytes) - pskBinders := [][]byte{cipherSuite.finishedHash(binderKey, transcript)} - if err := hello.updateBinders(pskBinders); err != nil { + if err := computeAndUpdatePSK(hello, binderKey, transcript, cipherSuite.finishedHash); err != nil { return nil, nil, nil, err } @@ -1001,7 +1079,32 @@ func (c *Conn) verifyServerCertificate(certificates [][]byte) error { certs[i] = cert.cert } - if !c.config.InsecureSkipVerify { + echRejected := c.config.EncryptedClientHelloConfigList != nil && !c.echAccepted + if echRejected { + if c.config.EncryptedClientHelloRejectionVerify != nil { + if err := c.config.EncryptedClientHelloRejectionVerify(c.connectionStateLocked()); err != nil { + c.sendAlert(alertBadCertificate) + return err + } + } else { + opts := x509.VerifyOptions{ + Roots: c.config.RootCAs, + CurrentTime: c.config.time(), + DNSName: c.serverName, + Intermediates: x509.NewCertPool(), + } + + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + var err error + c.verifiedChains, err = certs[0].Verify(opts) + if err != nil { + c.sendAlert(alertBadCertificate) + return &CertificateVerificationError{UnverifiedCertificates: certs, Err: err} + } + } + } else if !c.config.InsecureSkipVerify { opts := x509.VerifyOptions{ Roots: c.config.RootCAs, CurrentTime: c.config.time(), @@ -1031,14 +1134,14 @@ func (c *Conn) verifyServerCertificate(certificates [][]byte) error { c.activeCertHandles = activeHandles c.peerCertificates = certs - if c.config.VerifyPeerCertificate != nil { + if c.config.VerifyPeerCertificate != nil && !echRejected { if err := c.config.VerifyPeerCertificate(certificates, c.verifiedChains); err != nil { c.sendAlert(alertBadCertificate) return err } } - if c.config.VerifyConnection != nil { + if c.config.VerifyConnection != nil && !echRejected { if err := c.config.VerifyConnection(c.connectionStateLocked()); err != nil { c.sendAlert(alertBadCertificate) return err @@ -1161,3 +1264,13 @@ func hostnameInSNI(name string) string { } return name } + +func computeAndUpdatePSK(m *clientHelloMsg, binderKey []byte, transcript hash.Hash, finishedHash func([]byte, hash.Hash) []byte) error { + helloBytes, err := m.marshalWithoutBinders() + if err != nil { + return err + } + transcript.Write(helloBytes) + pskBinders := [][]byte{finishedHash(binderKey, transcript)} + return m.updateBinders(pskBinders) +} diff --git a/handshake_client_tls13.go b/handshake_client_tls13.go index a361019..8ce4d0a 100644 --- a/handshake_client_tls13.go +++ b/handshake_client_tls13.go @@ -10,6 +10,7 @@ import ( "crypto" "crypto/hmac" "crypto/rsa" + "crypto/subtle" "errors" "hash" "slices" @@ -36,6 +37,7 @@ type clientHandshakeStateTLS13 struct { transcript hash.Hash masterSecret []byte trafficSecret []byte // client_application_traffic_secret_0 + echContext *echContext } // handshake requires hs.c, hs.hello, hs.serverHello, hs.keyShareKeys, and, @@ -69,6 +71,13 @@ func (hs *clientHandshakeStateTLS13) handshake() error { return err } + if hs.echContext != nil { + hs.echContext.innerTranscript = hs.suite.hash.New() + if err := transcriptMsg(hs.echContext.innerHello, hs.echContext.innerTranscript); err != nil { + return err + } + } + if bytes.Equal(hs.serverHello.random, helloRetryRequestRandom) { if err := hs.sendDummyChangeCipherSpec(); err != nil { return err @@ -78,6 +87,41 @@ func (hs *clientHandshakeStateTLS13) handshake() error { } } + var echRetryConfigList []byte + if hs.echContext != nil { + confTranscript := cloneHash(hs.echContext.innerTranscript, hs.suite.hash) + confTranscript.Write(hs.serverHello.original[:30]) + confTranscript.Write(make([]byte, 8)) + confTranscript.Write(hs.serverHello.original[38:]) + acceptConfirmation := hs.suite.expandLabel( + hs.suite.extract(hs.echContext.innerHello.random, nil), + "ech accept confirmation", + confTranscript.Sum(nil), + 8, + ) + if subtle.ConstantTimeCompare(acceptConfirmation, hs.serverHello.random[len(hs.serverHello.random)-8:]) == 1 { + hs.hello = hs.echContext.innerHello + c.serverName = c.config.ServerName + hs.transcript = hs.echContext.innerTranscript + c.echAccepted = true + + if hs.serverHello.encryptedClientHello != nil { + c.sendAlert(alertUnsupportedExtension) + return errors.New("tls: unexpected encrypted_client_hello extension in server hello despite ECH being accepted") + } + + if hs.hello.serverName == "" && hs.serverHello.serverNameAck { + c.sendAlert(alertUnsupportedExtension) + return errors.New("tls: unexpected server_name extension in server hello") + } + } else { + hs.echContext.echRejected = true + // If the server sent us retry configs, we'll return these to + // the user so they can update their Config. + echRetryConfigList = hs.serverHello.encryptedClientHello + } + } + if err := transcriptMsg(hs.serverHello, hs.transcript); err != nil { return err } @@ -111,6 +155,11 @@ func (hs *clientHandshakeStateTLS13) handshake() error { return err } + if hs.echContext != nil && hs.echContext.echRejected { + c.sendAlert(alertECHRequired) + return &ECHRejectionError{echRetryConfigList} + } + c.isHandshakeComplete.Store(true) return nil @@ -202,6 +251,48 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { return err } + var isInnerHello bool + hello := hs.hello + if hs.echContext != nil { + chHash = hs.echContext.innerTranscript.Sum(nil) + hs.echContext.innerTranscript.Reset() + hs.echContext.innerTranscript.Write([]byte{typeMessageHash, 0, 0, uint8(len(chHash))}) + hs.echContext.innerTranscript.Write(chHash) + + if hs.serverHello.encryptedClientHello != nil { + if len(hs.serverHello.encryptedClientHello) != 8 { + hs.c.sendAlert(alertDecodeError) + return errors.New("tls: malformed encrypted client hello extension") + } + + confTranscript := cloneHash(hs.echContext.innerTranscript, hs.suite.hash) + hrrHello := make([]byte, len(hs.serverHello.original)) + copy(hrrHello, hs.serverHello.original) + hrrHello = bytes.Replace(hrrHello, hs.serverHello.encryptedClientHello, make([]byte, 8), 1) + confTranscript.Write(hrrHello) + acceptConfirmation := hs.suite.expandLabel( + hs.suite.extract(hs.echContext.innerHello.random, nil), + "hrr ech accept confirmation", + confTranscript.Sum(nil), + 8, + ) + if subtle.ConstantTimeCompare(acceptConfirmation, hs.serverHello.encryptedClientHello) == 1 { + hello = hs.echContext.innerHello + c.serverName = c.config.ServerName + isInnerHello = true + c.echAccepted = true + } + } + + if err := transcriptMsg(hs.serverHello, hs.echContext.innerTranscript); err != nil { + return err + } + } else if hs.serverHello.encryptedClientHello != nil { + // Unsolicited ECH extension should be rejected + c.sendAlert(alertUnsupportedExtension) + return errors.New("tls: unexpected ECH extension in serverHello") + } + // The only HelloRetryRequest extensions we support are key_share and // cookie, and clients must abort the handshake if the HRR would not result // in any change in the ClientHello. @@ -211,7 +302,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { } if hs.serverHello.cookie != nil { - hs.hello.cookie = hs.serverHello.cookie + hello.cookie = hs.serverHello.cookie } if hs.serverHello.serverShare.group != 0 { @@ -223,7 +314,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { // a group we advertised but did not send a key share for, and send a key // share for it this time. if curveID := hs.serverHello.selectedGroup; curveID != 0 { - if !slices.Contains(hs.hello.supportedCurves, curveID) { + if !slices.Contains(hello.supportedCurves, curveID) { c.sendAlert(alertIllegalParameter) return errors.New("tls: server selected unsupported group") } @@ -249,10 +340,10 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { return err } hs.keyShareKeys = &keySharePrivateKeys{curveID: curveID, ecdhe: key} - hs.hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} + hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}} } - if len(hs.hello.pskIdentities) > 0 { + if len(hello.pskIdentities) > 0 { pskSuite := cipherSuiteTLS13ByID(hs.session.cipherSuite) if pskSuite == nil { return c.sendAlert(alertInternalError) @@ -260,7 +351,7 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { if pskSuite.hash == hs.suite.hash { // Update binders and obfuscated_ticket_age. ticketAge := c.config.time().Sub(time.Unix(int64(hs.session.createdAt), 0)) - hs.hello.pskIdentities[0].obfuscatedTicketAge = uint32(ticketAge/time.Millisecond) + hs.session.ageAdd + hello.pskIdentities[0].obfuscatedTicketAge = uint32(ticketAge/time.Millisecond) + hs.session.ageAdd transcript := hs.suite.hash.New() transcript.Write([]byte{typeMessageHash, 0, 0, uint8(len(chHash))}) @@ -268,27 +359,40 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { if err := transcriptMsg(hs.serverHello, transcript); err != nil { return err } - helloBytes, err := hs.hello.marshalWithoutBinders() - if err != nil { - return err - } - transcript.Write(helloBytes) - pskBinders := [][]byte{hs.suite.finishedHash(hs.binderKey, transcript)} - if err := hs.hello.updateBinders(pskBinders); err != nil { + + if err := computeAndUpdatePSK(hello, hs.binderKey, transcript, hs.suite.finishedHash); err != nil { return err } } else { // Server selected a cipher suite incompatible with the PSK. - hs.hello.pskIdentities = nil - hs.hello.pskBinders = nil + hello.pskIdentities = nil + hello.pskBinders = nil } } - if hs.hello.earlyData { - hs.hello.earlyData = false + if hello.earlyData { + hello.earlyData = false c.quicRejectedEarlyData() } + if isInnerHello { + // Any extensions which have changed in hello, but are mirrored in the + // outer hello and compressed, need to be copied to the outer hello, so + // they can be properly decompressed by the server. For now, the only + // extension which may have changed is keyShares. + hs.hello.keyShares = hello.keyShares + hs.echContext.innerHello = hello + if err := transcriptMsg(hs.echContext.innerHello, hs.echContext.innerTranscript); err != nil { + return err + } + + if err := computeAndUpdateOuterECHExtension(hs.hello, hs.echContext.innerHello, hs.echContext, false); err != nil { + return err + } + } else { + hs.hello = hello + } + if _, err := hs.c.writeHandshakeRecord(hs.hello, hs.transcript); err != nil { return err } @@ -504,6 +608,10 @@ func (hs *clientHandshakeStateTLS13) readServerParameters() error { return errors.New("tls: server accepted 0-RTT with the wrong ALPN") } } + if hs.echContext != nil && !hs.echContext.echRejected && encryptedExtensions.echRetryConfigs != nil { + c.sendAlert(alertUnsupportedExtension) + return errors.New("tls: server sent ECH retry configs after accepting ECH") + } return nil } @@ -657,6 +765,13 @@ func (hs *clientHandshakeStateTLS13) sendClientCertificate() error { return nil } + if hs.echContext != nil && hs.echContext.echRejected { + if _, err := hs.c.writeHandshakeRecord(&certificateMsgTLS13{}, hs.transcript); err != nil { + return err + } + return nil + } + cert, err := c.getClientCertificate(&CertificateRequestInfo{ AcceptableCAs: hs.certReq.certificateAuthorities, SignatureSchemes: hs.certReq.supportedSignatureAlgorithms, diff --git a/handshake_messages.go b/handshake_messages.go index d7ca168..f5b0897 100644 --- a/handshake_messages.go +++ b/handshake_messages.go @@ -7,6 +7,7 @@ package reality import ( "errors" "fmt" + "slices" "strings" "golang.org/x/crypto/cryptobyte" @@ -95,9 +96,10 @@ type clientHelloMsg struct { pskIdentities []pskIdentity pskBinders [][]byte quicTransportParameters []byte + encryptedClientHello []byte } -func (m *clientHelloMsg) marshal() ([]byte, error) { +func (m *clientHelloMsg) marshalMsg(echInner bool) ([]byte, error) { var exts cryptobyte.Builder if len(m.serverName) > 0 { // RFC 6066, Section 3 @@ -111,7 +113,7 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { }) }) } - if len(m.supportedPoints) > 0 { + if len(m.supportedPoints) > 0 && !echInner { // RFC 4492, Section 5.1.2 exts.AddUint16(extensionSupportedPoints) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { @@ -120,14 +122,14 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { }) }) } - if m.ticketSupported { + if m.ticketSupported && !echInner { // RFC 5077, Section 3.2 exts.AddUint16(extensionSessionTicket) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { exts.AddBytes(m.sessionTicket) }) } - if m.secureRenegotiationSupported { + if m.secureRenegotiationSupported && !echInner { // RFC 5746, Section 3.2 exts.AddUint16(extensionRenegotiationInfo) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { @@ -136,7 +138,7 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { }) }) } - if m.extendedMasterSecret { + if m.extendedMasterSecret && !echInner { // RFC 7627 exts.AddUint16(extensionExtendedMasterSecret) exts.AddUint16(0) // empty extension_data @@ -158,101 +160,158 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { exts.AddBytes(m.quicTransportParameters) }) } + if len(m.encryptedClientHello) > 0 { + exts.AddUint16(extensionEncryptedClientHello) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddBytes(m.encryptedClientHello) + }) + } + // Note that any extension that can be compressed during ECH must be + // contiguous. If any additional extensions are to be compressed they must + // be added to the following block, so that they can be properly + // decompressed on the other side. + var echOuterExts []uint16 if m.ocspStapling { // RFC 4366, Section 3.6 - exts.AddUint16(extensionStatusRequest) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - exts.AddUint8(1) // status_type = ocsp - exts.AddUint16(0) // empty responder_id_list - exts.AddUint16(0) // empty request_extensions - }) + if echInner { + echOuterExts = append(echOuterExts, extensionStatusRequest) + } else { + exts.AddUint16(extensionStatusRequest) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddUint8(1) // status_type = ocsp + exts.AddUint16(0) // empty responder_id_list + exts.AddUint16(0) // empty request_extensions + }) + } } if len(m.supportedCurves) > 0 { // RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7 - exts.AddUint16(extensionSupportedCurves) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + if echInner { + echOuterExts = append(echOuterExts, extensionSupportedCurves) + } else { + exts.AddUint16(extensionSupportedCurves) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - for _, curve := range m.supportedCurves { - exts.AddUint16(uint16(curve)) - } + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + for _, curve := range m.supportedCurves { + exts.AddUint16(uint16(curve)) + } + }) }) - }) + } } if len(m.supportedSignatureAlgorithms) > 0 { // RFC 5246, Section 7.4.1.4.1 - exts.AddUint16(extensionSignatureAlgorithms) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + if echInner { + echOuterExts = append(echOuterExts, extensionSignatureAlgorithms) + } else { + exts.AddUint16(extensionSignatureAlgorithms) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - for _, sigAlgo := range m.supportedSignatureAlgorithms { - exts.AddUint16(uint16(sigAlgo)) - } + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + for _, sigAlgo := range m.supportedSignatureAlgorithms { + exts.AddUint16(uint16(sigAlgo)) + } + }) }) - }) + } } if len(m.supportedSignatureAlgorithmsCert) > 0 { // RFC 8446, Section 4.2.3 - exts.AddUint16(extensionSignatureAlgorithmsCert) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + if echInner { + echOuterExts = append(echOuterExts, extensionSignatureAlgorithmsCert) + } else { + exts.AddUint16(extensionSignatureAlgorithmsCert) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - for _, sigAlgo := range m.supportedSignatureAlgorithmsCert { - exts.AddUint16(uint16(sigAlgo)) - } + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + for _, sigAlgo := range m.supportedSignatureAlgorithmsCert { + exts.AddUint16(uint16(sigAlgo)) + } + }) }) - }) + } } if len(m.alpnProtocols) > 0 { // RFC 7301, Section 3.1 - exts.AddUint16(extensionALPN) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + if echInner { + echOuterExts = append(echOuterExts, extensionALPN) + } else { + exts.AddUint16(extensionALPN) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - for _, proto := range m.alpnProtocols { - exts.AddUint8LengthPrefixed(func(exts *cryptobyte.Builder) { - exts.AddBytes([]byte(proto)) - }) - } + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + for _, proto := range m.alpnProtocols { + exts.AddUint8LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddBytes([]byte(proto)) + }) + } + }) }) - }) + } } if len(m.supportedVersions) > 0 { // RFC 8446, Section 4.2.1 - exts.AddUint16(extensionSupportedVersions) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - exts.AddUint8LengthPrefixed(func(exts *cryptobyte.Builder) { - for _, vers := range m.supportedVersions { - exts.AddUint16(vers) - } + if echInner { + echOuterExts = append(echOuterExts, extensionSupportedVersions) + } else { + exts.AddUint16(extensionSupportedVersions) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddUint8LengthPrefixed(func(exts *cryptobyte.Builder) { + for _, vers := range m.supportedVersions { + exts.AddUint16(vers) + } + }) }) - }) + } } if len(m.cookie) > 0 { // RFC 8446, Section 4.2.2 - exts.AddUint16(extensionCookie) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + if echInner { + echOuterExts = append(echOuterExts, extensionCookie) + } else { + exts.AddUint16(extensionCookie) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - exts.AddBytes(m.cookie) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddBytes(m.cookie) + }) }) - }) + } } if len(m.keyShares) > 0 { // RFC 8446, Section 4.2.8 - exts.AddUint16(extensionKeyShare) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + if echInner { + echOuterExts = append(echOuterExts, extensionKeyShare) + } else { + exts.AddUint16(extensionKeyShare) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - for _, ks := range m.keyShares { - exts.AddUint16(uint16(ks.group)) - exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { - exts.AddBytes(ks.data) - }) - } + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + for _, ks := range m.keyShares { + exts.AddUint16(uint16(ks.group)) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddBytes(ks.data) + }) + } + }) }) - }) + } } if len(m.pskModes) > 0 { // RFC 8446, Section 4.2.9 - exts.AddUint16(extensionPSKModes) + if echInner { + echOuterExts = append(echOuterExts, extensionPSKModes) + } else { + exts.AddUint16(extensionPSKModes) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddUint8LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddBytes(m.pskModes) + }) + }) + } + } + if len(echOuterExts) > 0 && echInner { + exts.AddUint16(extensionECHOuterExtensions) exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { exts.AddUint8LengthPrefixed(func(exts *cryptobyte.Builder) { - exts.AddBytes(m.pskModes) + for _, e := range echOuterExts { + exts.AddUint16(e) + } }) }) } @@ -288,7 +347,9 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { b.AddUint16(m.vers) addBytesWithLength(b, m.random, 32) b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { - b.AddBytes(m.sessionId) + if !echInner { + b.AddBytes(m.sessionId) + } }) b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { for _, suite := range m.cipherSuites { @@ -309,6 +370,10 @@ func (m *clientHelloMsg) marshal() ([]byte, error) { return b.Bytes() } +func (m *clientHelloMsg) marshal() ([]byte, error) { + return m.marshalMsg(false) +} + // marshalWithoutBinders returns the ClientHello through the // PreSharedKeyExtension.identities field, according to RFC 8446, Section // 4.2.11.2. Note that m.pskBinders must be set to slices of the correct length. @@ -611,6 +676,39 @@ func (m *clientHelloMsg) originalBytes() []byte { return m.original } +func (m *clientHelloMsg) clone() *clientHelloMsg { + return &clientHelloMsg{ + original: slices.Clone(m.original), + vers: m.vers, + random: slices.Clone(m.random), + sessionId: slices.Clone(m.sessionId), + cipherSuites: slices.Clone(m.cipherSuites), + compressionMethods: slices.Clone(m.compressionMethods), + serverName: m.serverName, + ocspStapling: m.ocspStapling, + supportedCurves: slices.Clone(m.supportedCurves), + supportedPoints: slices.Clone(m.supportedPoints), + ticketSupported: m.ticketSupported, + sessionTicket: slices.Clone(m.sessionTicket), + supportedSignatureAlgorithms: slices.Clone(m.supportedSignatureAlgorithms), + supportedSignatureAlgorithmsCert: slices.Clone(m.supportedSignatureAlgorithmsCert), + secureRenegotiationSupported: m.secureRenegotiationSupported, + secureRenegotiation: slices.Clone(m.secureRenegotiation), + extendedMasterSecret: m.extendedMasterSecret, + alpnProtocols: slices.Clone(m.alpnProtocols), + scts: m.scts, + supportedVersions: slices.Clone(m.supportedVersions), + cookie: slices.Clone(m.cookie), + keyShares: slices.Clone(m.keyShares), + earlyData: m.earlyData, + pskModes: slices.Clone(m.pskModes), + pskIdentities: slices.Clone(m.pskIdentities), + pskBinders: slices.Clone(m.pskBinders), + quicTransportParameters: slices.Clone(m.quicTransportParameters), + encryptedClientHello: slices.Clone(m.encryptedClientHello), + } +} + type serverHelloMsg struct { original []byte vers uint16 @@ -630,6 +728,8 @@ type serverHelloMsg struct { selectedIdentityPresent bool selectedIdentity uint16 supportedPoints []uint8 + encryptedClientHello []byte + serverNameAck bool // HelloRetryRequest extensions cookie []byte @@ -724,6 +824,16 @@ func (m *serverHelloMsg) marshal() ([]byte, error) { }) }) } + if len(m.encryptedClientHello) > 0 { + exts.AddUint16(extensionEncryptedClientHello) + exts.AddUint16LengthPrefixed(func(exts *cryptobyte.Builder) { + exts.AddBytes(m.encryptedClientHello) + }) + } + if m.serverNameAck { + exts.AddUint16(extensionServerName) + exts.AddUint16(0) + } extBytes, err := exts.Bytes() if err != nil { @@ -856,6 +966,16 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool { len(m.supportedPoints) == 0 { return false } + case extensionEncryptedClientHello: // encrypted_client_hello + m.encryptedClientHello = make([]byte, len(extData)) + if !extData.CopyBytes(m.encryptedClientHello) { + return false + } + case extensionServerName: + if len(extData) != 0 { + return false + } + m.serverNameAck = true default: // Ignore unknown extensions. continue @@ -877,6 +997,7 @@ type encryptedExtensionsMsg struct { alpnProtocol string quicTransportParameters []byte earlyData bool + echRetryConfigs []byte } func (m *encryptedExtensionsMsg) marshal() ([]byte, error) { @@ -906,6 +1027,12 @@ func (m *encryptedExtensionsMsg) marshal() ([]byte, error) { b.AddUint16(extensionEarlyData) b.AddUint16(0) // empty extension_data } + if len(m.echRetryConfigs) > 0 { + b.AddUint16(extensionEncryptedClientHello) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(m.echRetryConfigs) + }) + } }) }) @@ -950,6 +1077,11 @@ func (m *encryptedExtensionsMsg) unmarshal(data []byte) bool { case extensionEarlyData: // RFC 8446, Section 4.2.10 m.earlyData = true + case extensionEncryptedClientHello: + m.echRetryConfigs = make([]byte, len(extData)) + if !extData.CopyBytes(m.echRetryConfigs) { + return false + } default: // Ignore unknown extensions. continue diff --git a/hpke/hpye.go b/hpke/hpye.go new file mode 100644 index 0000000..611c89a --- /dev/null +++ b/hpke/hpye.go @@ -0,0 +1,259 @@ +// 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 +}