mirror of
https://github.com/golang/go
synced 2024-11-25 09:57:57 -07:00
crypto/tls: add ech client support
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>
This commit is contained in:
parent
83ff4fd571
commit
9eeb627f60
6
api/next/63369.txt
Normal file
6
api/next/63369.txt
Normal file
@ -0,0 +1,6 @@
|
||||
pkg crypto/tls, type Config struct, EncryptedClientHelloConfigList []uint8 #63369
|
||||
pkg crypto/tls, type Config struct, EncryptedClientHelloRejectionVerify func(ConnectionState) error #63369
|
||||
pkg crypto/tls, type ConnectionState struct, ECHAccepted bool #63369
|
||||
pkg crypto/tls, type ECHRejectionError struct #63369
|
||||
pkg crypto/tls, type ECHRejectionError struct, RetryConfigList []uint8 #63369
|
||||
pkg crypto/tls, method (*ECHRejectionError) Error() string #63369
|
3
doc/next/6-stdlib/99-minor/crypto/tls/63369.md
Normal file
3
doc/next/6-stdlib/99-minor/crypto/tls/63369.md
Normal file
@ -0,0 +1,3 @@
|
||||
The TLS client now supports the Encrypted Client Hello [draft specification](https://www.ietf.org/archive/id/draft-ietf-tls-esni-18.html).
|
||||
This feature can be enabled by setting the [Config.EncryptedClientHelloConfigList]
|
||||
field to an encoded ECHConfigList for the host that is being connected to.
|
@ -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 {
|
||||
|
@ -1,5 +1,45 @@
|
||||
{
|
||||
"DisabledTests": {
|
||||
"*-Async": "We don't support boringssl concept of async",
|
||||
|
||||
"TLS-ECH-Client-Reject-NoClientCertificate-TLS12": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
"TLS-ECH-Client-Reject-TLS12": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
"TLS-ECH-Client-TLS12-RejectRetryConfigs": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
"TLS-ECH-Client-Rejected-OverrideName-TLS12": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
"TLS-ECH-Client-Reject-TLS12-NoFalseStart": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
"TLS-ECH-Client-TLS12SessionTicket": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
"TLS-ECH-Client-TLS12SessionID": "We won't attempt to negotiate 1.2 if ECH is enabled",
|
||||
|
||||
"TLS-ECH-Client-Reject-ResumeInnerSession-TLS12": "We won't attempt to negotiate 1.2 if ECH is enabled (we could possibly test this if we had the ability to indicate not to send ECH on resumption?)",
|
||||
|
||||
"TLS-ECH-Client-Reject-EarlyDataRejected": "We don't support switiching out ECH configs with this level of granularity",
|
||||
|
||||
"TLS-ECH-Client-NoNPN": "We don't support NPN",
|
||||
|
||||
"TLS-ECH-Client-ChannelID": "We don't support sending channel ID",
|
||||
"TLS-ECH-Client-Reject-NoChannelID-TLS13": "We don't support sending channel ID",
|
||||
"TLS-ECH-Client-Reject-NoChannelID-TLS12": "We don't support sending channel ID",
|
||||
|
||||
"TLS-ECH-Client-GREASE-IgnoreHRRExtension": "We don't support ECH GREASE because we don't fallback to plaintext",
|
||||
"TLS-ECH-Client-NoSupportedConfigs-GREASE": "We don't support ECH GREASE because we don't fallback to plaintext",
|
||||
"TLS-ECH-Client-GREASEExtensions": "We don't support ECH GREASE because we don't fallback to plaintext",
|
||||
"TLS-ECH-Client-GREASE-NoOverrideName": "We don't support ECH GREASE because we don't fallback to plaintext",
|
||||
|
||||
"TLS-ECH-Client-UnsolicitedInnerServerNameAck": "We don't allow sending empty SNI without skipping certificate verification, TODO: could add special flag to bogo to indicate 'empty sni'",
|
||||
|
||||
"TLS-ECH-Client-NoSupportedConfigs": "We don't support fallback to cleartext when there are no valid ECH configs",
|
||||
"TLS-ECH-Client-SkipInvalidPublicName": "We don't support fallback to cleartext when there are no valid ECH configs",
|
||||
|
||||
"TLS-ECH-Client-Reject-RandomHRRExtension": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
"TLS-ECH-Client-Reject-UnsupportedRetryConfigs": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
"TLS-ECH-Client-Reject-NoRetryConfigs": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
"TLS-ECH-Client-Reject": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
"TLS-ECH-Client-Reject-HelloRetryRequest": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
"TLS-ECH-Client-Reject-NoClientCertificate-TLS13": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
"TLS-ECH-Client-Reject-OverrideName-TLS13": "TODO: bogo test cases have mismatching public certificates and public names in ECH configs. Can be removed once bogo fixed",
|
||||
|
||||
"*ECH-Server*": "no ECH server support",
|
||||
"SendV2ClientHello*": "We don't support SSLv2",
|
||||
"*QUIC*": "No QUIC support",
|
||||
"Compliance-fips*": "No FIPS",
|
||||
"*DTLS*": "No DTLS",
|
||||
@ -16,8 +56,6 @@
|
||||
"GarbageCertificate*": "TODO ask davidben, alertDecode vs alertBadCertificate",
|
||||
"SendBogusAlertType": "sending wrong alert type",
|
||||
"EchoTLS13CompatibilitySessionID": "TODO reject compat session ID",
|
||||
"*ECH-Server*": "no ECH server support",
|
||||
"TLS-ECH-Client-UnsolictedHRRExtension": "TODO",
|
||||
"*Client-P-224*": "no P-224 support",
|
||||
"*Server-P-224*": "no P-224 support",
|
||||
"CurveID-Resume*": "unexposed curveID is not stored in the ticket yet",
|
||||
@ -180,6 +218,23 @@
|
||||
"DuplicateCertCompressionExt-TLS13": "TODO: first pass, this should be fixed",
|
||||
"Client-RejectJDK11DowngradeRandom": "TODO: first pass, this should be fixed",
|
||||
"CheckClientCertificateTypes": "TODO: first pass, this should be fixed",
|
||||
"CheckECDSACurve-TLS12": "TODO: first pass, this should be fixed"
|
||||
"CheckECDSACurve-TLS12": "TODO: first pass, this should be fixed",
|
||||
"ALPNClient-RejectUnknown-TLS-TLS1": "TODO: first pass, this should be fixed",
|
||||
"ALPNClient-RejectUnknown-TLS-TLS11": "TODO: first pass, this should be fixed",
|
||||
"ALPNClient-RejectUnknown-TLS-TLS12": "TODO: first pass, this should be fixed",
|
||||
"ALPNClient-RejectUnknown-TLS-TLS13": "TODO: first pass, this should be fixed",
|
||||
"ClientHelloPadding": "TODO: first pass, this should be fixed",
|
||||
"TLS13-ExpectTicketEarlyDataSupport": "TODO: first pass, this should be fixed",
|
||||
"TLS13-EarlyData-TooMuchData-Client-TLS-Sync": "TODO: first pass, this should be fixed",
|
||||
"TLS13-EarlyData-TooMuchData-Client-TLS-Sync-SplitHandshakeRecords": "TODO: first pass, this should be fixed",
|
||||
"TLS13-EarlyData-TooMuchData-Client-TLS-Sync-PackHandshake": "TODO: first pass, this should be fixed",
|
||||
"WrongMessageType-TLS13-EndOfEarlyData-TLS": "TODO: first pass, this should be fixed",
|
||||
"TrailingMessageData-TLS13-EndOfEarlyData-TLS": "TODO: first pass, this should be fixed",
|
||||
"SendHelloRetryRequest-2-TLS13": "TODO: first pass, this should be fixed",
|
||||
"EarlyData-SkipEndOfEarlyData-TLS13": "TODO: first pass, this should be fixed",
|
||||
"EarlyData-Server-BadFinished-TLS13": "TODO: first pass, this should be fixed",
|
||||
"EarlyData-UnexpectedHandshake-Server-TLS13": "TODO: first pass, this should be fixed",
|
||||
"EarlyData-CipherMismatch-Client-TLS13": "TODO: first pass, this should be fixed",
|
||||
"Resume-Server-UnofferedCipher-TLS13": "TODO: first pass, this should be fixed"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
@ -48,93 +50,35 @@ var (
|
||||
shimID = flag.Uint64("shim-id", 0, "")
|
||||
_ = flag.Bool("ipv6", false, "")
|
||||
|
||||
// Unimplemented flags
|
||||
// -advertise-alpn
|
||||
// -advertise-npn
|
||||
// -allow-hint-mismatch
|
||||
// -async
|
||||
// -check-close-notify
|
||||
// -cipher
|
||||
// -curves
|
||||
// -delegated-credential
|
||||
// -dtls
|
||||
// -ech-config-list
|
||||
// -ech-server-config
|
||||
// -enable-channel-id
|
||||
// -enable-early-data
|
||||
// -enable-ech-grease
|
||||
// -enable-grease
|
||||
// -enable-ocsp-stapling
|
||||
// -enable-signed-cert-timestamps
|
||||
// -expect-advertised-alpn
|
||||
// -expect-certificate-types
|
||||
// -expect-channel-id
|
||||
// -expect-cipher-aes
|
||||
// -expect-client-ca-list
|
||||
// -expect-curve-id
|
||||
// -expect-early-data-reason
|
||||
// -expect-extended-master-secret
|
||||
// -expect-hrr
|
||||
// -expect-key-usage-invalid
|
||||
// -expect-msg-callback
|
||||
// -expect-no-session
|
||||
// -expect-peer-cert-file
|
||||
// -expect-peer-signature-algorithm
|
||||
// -expect-peer-verify-pref
|
||||
// -expect-secure-renegotiation
|
||||
// -expect-server-name
|
||||
// -expect-ticket-supports-early-data
|
||||
// -export-keying-material
|
||||
// -export-traffic-secrets
|
||||
// -fail-cert-callback
|
||||
// -fail-early-callback
|
||||
// -fallback-scsv
|
||||
// -false-start
|
||||
// -forbid-renegotiation-after-handshake
|
||||
// -handshake-twice
|
||||
// -host-name
|
||||
// -ignore-rsa-key-usage
|
||||
// -implicit-handshake
|
||||
// -install-cert-compression-algs
|
||||
// -install-ddos-callback
|
||||
// -install-one-cert-compression-alg
|
||||
// -jdk11-workaround
|
||||
// -key-update
|
||||
// -max-cert-list
|
||||
// -max-send-fragment
|
||||
// -no-ticket
|
||||
// -no-tls1
|
||||
// -no-tls11
|
||||
// -no-tls12
|
||||
// -ocsp-response
|
||||
// -on-resume-expect-accept-early-data
|
||||
// -on-resume-expect-reject-early-data
|
||||
// -on-shim-cipher
|
||||
// -on-shim-curves
|
||||
// -peek-then-read
|
||||
// -psk
|
||||
// -read-with-unfinished-write
|
||||
// -reject-alpn
|
||||
// -renegotiate-explicit
|
||||
// -renegotiate-freely
|
||||
// -renegotiate-ignore
|
||||
// -renegotiate-once
|
||||
// -select-alpn
|
||||
// -select-next-proto
|
||||
// -send-alert
|
||||
// -send-channel-id
|
||||
// -server-preference
|
||||
// -shim-shuts-down
|
||||
// -signed-cert-timestamps
|
||||
// -signing-prefs
|
||||
// -srtp-profiles
|
||||
// -tls-unique
|
||||
// -use-client-ca-list
|
||||
// -use-ocsp-callback
|
||||
// -use-old-client-cert-callback
|
||||
// -verify-fail
|
||||
// -verify-peer
|
||||
// -verify-prefs
|
||||
echConfigListB64 = flag.String("ech-config-list", "", "")
|
||||
expectECHAccepted = flag.Bool("expect-ech-accept", false, "")
|
||||
expectHRR = flag.Bool("expect-hrr", false, "")
|
||||
expectedECHRetryConfigs = flag.String("expect-ech-retry-configs", "", "")
|
||||
expectNoECHRetryConfigs = flag.Bool("expect-no-ech-retry-configs", false, "")
|
||||
onInitialExpectECHAccepted = flag.Bool("on-initial-expect-ech-accept", false, "")
|
||||
_ = flag.Bool("expect-no-ech-name-override", false, "")
|
||||
_ = flag.String("expect-ech-name-override", "", "")
|
||||
_ = flag.Bool("reverify-on-resume", false, "")
|
||||
onResumeECHConfigListB64 = flag.String("on-resume-ech-config-list", "", "")
|
||||
_ = flag.Bool("on-resume-expect-reject-early-data", false, "")
|
||||
onResumeExpectECHAccepted = flag.Bool("on-resume-expect-ech-accept", false, "")
|
||||
_ = flag.Bool("on-resume-expect-no-ech-name-override", false, "")
|
||||
expectedServerName = flag.String("expect-server-name", "", "")
|
||||
|
||||
expectSessionMiss = flag.Bool("expect-session-miss", false, "")
|
||||
|
||||
_ = flag.Bool("enable-early-data", false, "")
|
||||
_ = flag.Bool("on-resume-expect-accept-early-data", false, "")
|
||||
_ = flag.Bool("expect-ticket-supports-early-data", false, "")
|
||||
onResumeShimWritesFirst = flag.Bool("on-resume-shim-writes-first", false, "")
|
||||
|
||||
advertiseALPN = flag.String("advertise-alpn", "", "")
|
||||
expectALPN = flag.String("expect-alpn", "", "")
|
||||
|
||||
hostName = flag.String("host-name", "", "")
|
||||
|
||||
verifyPeer = flag.Bool("verify-peer", false, "")
|
||||
_ = flag.Bool("use-custom-verify-callback", false, "")
|
||||
)
|
||||
|
||||
type stringSlice []string
|
||||
@ -168,11 +112,23 @@ func bogoShim() {
|
||||
|
||||
ClientSessionCache: NewLRUClientSessionCache(0),
|
||||
}
|
||||
|
||||
if *noTLS13 && cfg.MaxVersion == VersionTLS13 {
|
||||
cfg.MaxVersion = VersionTLS12
|
||||
}
|
||||
|
||||
if *advertiseALPN != "" {
|
||||
alpns := *advertiseALPN
|
||||
for len(alpns) > 0 {
|
||||
alpnLen := int(alpns[0])
|
||||
cfg.NextProtos = append(cfg.NextProtos, alpns[1:1+alpnLen])
|
||||
alpns = alpns[alpnLen+1:]
|
||||
}
|
||||
}
|
||||
|
||||
if *hostName != "" {
|
||||
cfg.ServerName = *hostName
|
||||
}
|
||||
|
||||
if *keyfile != "" || *certfile != "" {
|
||||
pair, err := LoadX509KeyPair(*certfile, *keyfile)
|
||||
if err != nil {
|
||||
@ -198,6 +154,18 @@ func bogoShim() {
|
||||
if *requireAnyClientCertificate {
|
||||
cfg.ClientAuth = RequireAnyClientCert
|
||||
}
|
||||
if *verifyPeer {
|
||||
cfg.ClientAuth = VerifyClientCertIfGiven
|
||||
}
|
||||
|
||||
if *echConfigListB64 != "" {
|
||||
echConfigList, err := base64.StdEncoding.DecodeString(*echConfigListB64)
|
||||
if err != nil {
|
||||
log.Fatalf("parse ech-config-list err: %s", err)
|
||||
}
|
||||
cfg.EncryptedClientHelloConfigList = echConfigList
|
||||
cfg.MinVersion = VersionTLS13
|
||||
}
|
||||
|
||||
if len(*curves) != 0 {
|
||||
for _, curveStr := range *curves {
|
||||
@ -210,6 +178,14 @@ func bogoShim() {
|
||||
}
|
||||
|
||||
for i := 0; i < *resumeCount+1; i++ {
|
||||
if i > 0 && (*onResumeECHConfigListB64 != "") {
|
||||
echConfigList, err := base64.StdEncoding.DecodeString(*onResumeECHConfigListB64)
|
||||
if err != nil {
|
||||
log.Fatalf("parse ech-config-list err: %s", err)
|
||||
}
|
||||
cfg.EncryptedClientHelloConfigList = echConfigList
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", net.JoinHostPort("localhost", *port))
|
||||
if err != nil {
|
||||
log.Fatalf("dial err: %s", err)
|
||||
@ -230,7 +206,7 @@ func bogoShim() {
|
||||
tlsConn = Client(conn, cfg)
|
||||
}
|
||||
|
||||
if *shimWritesFirst {
|
||||
if i == 0 && *shimWritesFirst {
|
||||
if _, err := tlsConn.Write([]byte("hello")); err != nil {
|
||||
log.Fatalf("write err: %s", err)
|
||||
}
|
||||
@ -238,19 +214,65 @@ func bogoShim() {
|
||||
|
||||
for {
|
||||
buf := make([]byte, 500)
|
||||
n, err := tlsConn.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
var n int
|
||||
n, err = tlsConn.Read(buf)
|
||||
if err != nil {
|
||||
log.Fatalf("read err: %s", err)
|
||||
break
|
||||
}
|
||||
buf = buf[:n]
|
||||
for i := range buf {
|
||||
buf[i] ^= 0xff
|
||||
}
|
||||
if _, err := tlsConn.Write(buf); err != nil {
|
||||
log.Fatalf("write err: %s", err)
|
||||
if _, err = tlsConn.Write(buf); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
retryErr, ok := err.(*ECHRejectionError)
|
||||
if !ok {
|
||||
log.Fatalf("unexpected error type returned: %v", err)
|
||||
}
|
||||
if *expectNoECHRetryConfigs && len(retryErr.RetryConfigList) > 0 {
|
||||
log.Fatalf("expected no ECH retry configs, got some")
|
||||
}
|
||||
if *expectedECHRetryConfigs != "" {
|
||||
expectedRetryConfigs, err := base64.StdEncoding.DecodeString(*expectedECHRetryConfigs)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to decode expected retry configs: %s", err)
|
||||
}
|
||||
if !bytes.Equal(retryErr.RetryConfigList, expectedRetryConfigs) {
|
||||
log.Fatalf("unexpected retry list returned: got %x, want %x", retryErr.RetryConfigList, expectedRetryConfigs)
|
||||
}
|
||||
}
|
||||
log.Fatalf("conn error: %s", err)
|
||||
}
|
||||
|
||||
cs := tlsConn.ConnectionState()
|
||||
if cs.HandshakeComplete {
|
||||
if *expectALPN != "" && cs.NegotiatedProtocol != *expectALPN {
|
||||
log.Fatalf("unexpected protocol negotiated: want %q, got %q", *expectALPN, cs.NegotiatedProtocol)
|
||||
}
|
||||
|
||||
if *expectECHAccepted && !cs.ECHAccepted {
|
||||
log.Fatal("expected ECH to be accepted, but connection state shows it was not")
|
||||
} else if i == 0 && *onInitialExpectECHAccepted && !cs.ECHAccepted {
|
||||
log.Fatal("expected ECH to be accepted, but connection state shows it was not")
|
||||
} else if i > 0 && *onResumeExpectECHAccepted && !cs.ECHAccepted {
|
||||
log.Fatal("expected ECH to be accepted on resumption, but connection state shows it was not")
|
||||
} else if i == 0 && !*expectECHAccepted && cs.ECHAccepted {
|
||||
log.Fatal("did not expect ECH, but it was accepted")
|
||||
}
|
||||
|
||||
if *expectHRR && !cs.testingOnlyDidHRR {
|
||||
log.Fatal("expected HRR but did not do it")
|
||||
}
|
||||
|
||||
if *expectSessionMiss && cs.DidResume {
|
||||
log.Fatal("unexpected session resumption")
|
||||
}
|
||||
|
||||
if *expectedServerName != "" && cs.ServerName != *expectedServerName {
|
||||
log.Fatalf("unexpected server name: got %q, want %q", cs.ServerName, *expectedServerName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,21 +297,26 @@ func TestBogoSuite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode")
|
||||
}
|
||||
|
||||
if testenv.Builder() != "" && runtime.GOOS == "windows" {
|
||||
t.Skip("#66913: windows network connections are flakey on builders")
|
||||
}
|
||||
|
||||
const boringsslModVer = "v0.0.0-20240517213134-ba62c812f01f"
|
||||
output, err := exec.Command("go", "mod", "download", "-json", "github.com/google/boringssl@"+boringsslModVer).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to download boringssl: %s", err)
|
||||
}
|
||||
var j struct {
|
||||
Dir string
|
||||
}
|
||||
if err := json.Unmarshal(output, &j); err != nil {
|
||||
t.Fatalf("failed to parse 'go mod download' output: %s", err)
|
||||
var bogoDir string
|
||||
if *bogoLocalDir != "" {
|
||||
bogoDir = *bogoLocalDir
|
||||
} else {
|
||||
const boringsslModVer = "v0.0.0-20240517213134-ba62c812f01f"
|
||||
output, err := exec.Command("go", "mod", "download", "-json", "github.com/google/boringssl@"+boringsslModVer).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to download boringssl: %s", err)
|
||||
}
|
||||
var j struct {
|
||||
Dir string
|
||||
}
|
||||
if err := json.Unmarshal(output, &j); err != nil {
|
||||
t.Fatalf("failed to parse 'go mod download' output: %s", err)
|
||||
}
|
||||
bogoDir = j.Dir
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
@ -319,7 +346,7 @@ func TestBogoSuite(t *testing.T) {
|
||||
cmd := exec.Command(goCmd, args...)
|
||||
out := &strings.Builder{}
|
||||
cmd.Stdout, cmd.Stderr = io.MultiWriter(os.Stdout, out), os.Stderr
|
||||
cmd.Dir = filepath.Join(j.Dir, "ssl/test/runner")
|
||||
cmd.Dir = filepath.Join(bogoDir, "ssl/test/runner")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("bogo failed: %s", err)
|
||||
|
@ -125,6 +125,8 @@ const (
|
||||
extensionKeyShare uint16 = 51
|
||||
extensionQUICTransportParameters uint16 = 57
|
||||
extensionRenegotiationInfo uint16 = 0xff01
|
||||
extensionECHOuterExtensions uint16 = 0xfd00
|
||||
extensionEncryptedClientHello uint16 = 0xfe0d
|
||||
)
|
||||
|
||||
// TLS signaling cipher suite values
|
||||
@ -287,6 +289,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)
|
||||
|
||||
@ -777,6 +784,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
|
||||
@ -836,36 +878,38 @@ func (c *Config) Clone() *Config {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return &Config{
|
||||
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,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1052,6 +1096,9 @@ func (c *Config) supportedVersions(isClient bool) []uint16 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if isClient && c.EncryptedClientHelloConfigList != nil && v < VersionTLS13 {
|
||||
continue
|
||||
}
|
||||
if c != nil && c.MinVersion != 0 && v < c.MinVersion {
|
||||
continue
|
||||
}
|
||||
|
@ -71,6 +71,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
|
||||
@ -1652,6 +1653,7 @@ func (c *Conn) connectionStateLocked() ConnectionState {
|
||||
} else {
|
||||
state.ekm = c.ekm
|
||||
}
|
||||
state.ECHAccepted = c.echAccepted
|
||||
return state
|
||||
}
|
||||
|
||||
|
283
src/crypto/tls/ech.go
Normal file
283
src/crypto/tls/ech.go
Normal file
@ -0,0 +1,283 @@
|
||||
// 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 tls
|
||||
|
||||
import (
|
||||
"crypto/internal/hpke"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
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"
|
||||
}
|
48
src/crypto/tls/ech_test.go
Normal file
48
src/crypto/tls/ech_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
// 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 tls
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeECHConfigLists(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
list string
|
||||
numConfigs int
|
||||
}{
|
||||
{"0045fe0d0041590020002092a01233db2218518ccbbbbc24df20686af417b37388de6460e94011974777090004000100010012636c6f7564666c6172652d6563682e636f6d0000", 1},
|
||||
{"0105badd00050504030201fe0d0066000010004104e62b69e2bf659f97be2f1e0d948a4cd5976bb7a91e0d46fbdda9a91e9ddcba5a01e7d697a80a18f9c3c4a31e56e27c8348db161a1cf51d7ef1942d4bcf7222c1000c000100010001000200010003400e7075626c69632e6578616d706c650000fe0d003d00002000207d661615730214aeee70533366f36a609ead65c0c208e62322346ab5bcd8de1c000411112222400e7075626c69632e6578616d706c650000fe0d004d000020002085bd6a03277c25427b52e269e0c77a8eb524ba1eb3d2f132662d4b0ac6cb7357000c000100010001000200010003400e7075626c69632e6578616d706c650008aaaa000474657374", 3},
|
||||
} {
|
||||
b, err := hex.DecodeString(tc.list)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configs, err := parseECHConfigList(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(configs) != tc.numConfigs {
|
||||
t.Fatalf("unexpected number of configs parsed: got %d want %d", len(configs), tc.numConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSkipBadConfigs(t *testing.T) {
|
||||
b, err := hex.DecodeString("00c8badd00050504030201fe0d0029006666000401020304000c000100010001000200010003400e7075626c69632e6578616d706c650000fe0d003d000020002072e8a23b7aef67832bcc89d652e3870a60f88ca684ec65d6eace6b61f136064c000411112222400e7075626c69632e6578616d706c650000fe0d004d00002000200ce95810a81d8023f41e83679bc92701b2acd46c75869f95c72bc61c6b12297c000c000100010001000200010003400e7075626c69632e6578616d706c650008aaaa000474657374")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configs, err := parseECHConfigList(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config := pickECHConfig(configs)
|
||||
if config != nil {
|
||||
t.Fatal("pickECHConfig picked an invalid config")
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/internal/hpke"
|
||||
"crypto/internal/mlkem768"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
@ -40,27 +41,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,11 +259,10 @@ 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
|
||||
}
|
||||
c.serverName = hello.serverName
|
||||
|
||||
session, earlySecret, binderKey, err := c.loadSession(hello)
|
||||
if err != nil {
|
||||
@ -231,6 +284,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 +361,7 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
|
||||
session: session,
|
||||
earlySecret: earlySecret,
|
||||
binderKey: binderKey,
|
||||
echContext: ech,
|
||||
}
|
||||
return hs.handshake()
|
||||
}
|
||||
@ -303,7 +382,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
|
||||
@ -422,13 +505,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
|
||||
}
|
||||
|
||||
@ -1009,7 +1086,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(),
|
||||
@ -1039,14 +1141,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
|
||||
@ -1169,3 +1271,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)
|
||||
}
|
||||
|
@ -7,9 +7,14 @@ package tls
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -2809,3 +2814,123 @@ func TestHandshakeRSATooBig(t *testing.T) {
|
||||
t.Errorf("Conn.processCertsFromClient unexpected error: want %q, got %q", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLS13ECHRejectionCallbacks(t *testing.T) {
|
||||
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "test"},
|
||||
DNSNames: []string{"example.golang"},
|
||||
NotBefore: testConfig.Time().Add(-time.Hour),
|
||||
NotAfter: testConfig.Time().Add(time.Hour),
|
||||
}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, k.Public(), k)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientConfig, serverConfig := testConfig.Clone(), testConfig.Clone()
|
||||
serverConfig.Certificates = []Certificate{
|
||||
{
|
||||
Certificate: [][]byte{certDER},
|
||||
PrivateKey: k,
|
||||
},
|
||||
}
|
||||
serverConfig.MinVersion = VersionTLS13
|
||||
clientConfig.RootCAs = x509.NewCertPool()
|
||||
clientConfig.RootCAs.AddCert(cert)
|
||||
clientConfig.MinVersion = VersionTLS13
|
||||
clientConfig.EncryptedClientHelloConfigList, _ = hex.DecodeString("0041fe0d003d0100200020204bed0a11fc0dde595a9b78d966b0011128eb83f65d3c91c1cc5ac786cd246f000400010001ff0e6578616d706c652e676f6c616e670000")
|
||||
clientConfig.ServerName = "example.golang"
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
expectedErr string
|
||||
|
||||
verifyConnection func(ConnectionState) error
|
||||
verifyPeerCertificate func([][]byte, [][]*x509.Certificate) error
|
||||
encryptedClientHelloRejectionVerify func(ConnectionState) error
|
||||
}{
|
||||
{
|
||||
name: "no callbacks",
|
||||
expectedErr: "tls: server rejected ECH",
|
||||
},
|
||||
{
|
||||
name: "EncryptedClientHelloRejectionVerify, no err",
|
||||
encryptedClientHelloRejectionVerify: func(ConnectionState) error {
|
||||
return nil
|
||||
},
|
||||
expectedErr: "tls: server rejected ECH",
|
||||
},
|
||||
{
|
||||
name: "EncryptedClientHelloRejectionVerify, err",
|
||||
encryptedClientHelloRejectionVerify: func(ConnectionState) error {
|
||||
return errors.New("callback err")
|
||||
},
|
||||
// testHandshake returns the server side error, so we just need to
|
||||
// check alertBadCertificate was sent
|
||||
expectedErr: "callback err",
|
||||
},
|
||||
{
|
||||
name: "VerifyConnection, err",
|
||||
verifyConnection: func(ConnectionState) error {
|
||||
return errors.New("callback err")
|
||||
},
|
||||
expectedErr: "tls: server rejected ECH",
|
||||
},
|
||||
{
|
||||
name: "VerifyPeerCertificate, err",
|
||||
verifyPeerCertificate: func([][]byte, [][]*x509.Certificate) error {
|
||||
return errors.New("callback err")
|
||||
},
|
||||
expectedErr: "tls: server rejected ECH",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, s := localPipe(t)
|
||||
done := make(chan error)
|
||||
|
||||
go func() {
|
||||
serverErr := Server(s, serverConfig).Handshake()
|
||||
s.Close()
|
||||
done <- serverErr
|
||||
}()
|
||||
|
||||
cConfig := clientConfig.Clone()
|
||||
cConfig.VerifyConnection = tc.verifyConnection
|
||||
cConfig.VerifyPeerCertificate = tc.verifyPeerCertificate
|
||||
cConfig.EncryptedClientHelloRejectionVerify = tc.encryptedClientHelloRejectionVerify
|
||||
|
||||
clientErr := Client(c, cConfig).Handshake()
|
||||
c.Close()
|
||||
|
||||
if tc.expectedErr == "" && clientErr != nil {
|
||||
t.Fatalf("unexpected err: %s", clientErr)
|
||||
} else if clientErr != nil && tc.expectedErr != clientErr.Error() {
|
||||
t.Fatalf("unexpected err: got %q, want %q", clientErr, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestECHTLS12Server(t *testing.T) {
|
||||
clientConfig, serverConfig := testConfig.Clone(), testConfig.Clone()
|
||||
|
||||
serverConfig.MaxVersion = VersionTLS12
|
||||
clientConfig.MinVersion = 0
|
||||
|
||||
clientConfig.EncryptedClientHelloConfigList, _ = hex.DecodeString("0041fe0d003d0100200020204bed0a11fc0dde595a9b78d966b0011128eb83f65d3c91c1cc5ac786cd246f000400010001ff0e6578616d706c652e676f6c616e670000")
|
||||
|
||||
expectedErr := "server: tls: client offered only unsupported versions: [304]\nclient: remote error: tls: protocol version not supported"
|
||||
_, _, err := testHandshake(t, clientConfig, serverConfig)
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Fatalf("unexpected handshake error: got %q, want %q", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/internal/mlkem768"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"hash"
|
||||
"slices"
|
||||
@ -35,6 +36,8 @@ 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,
|
||||
@ -68,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
|
||||
@ -77,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
|
||||
}
|
||||
@ -110,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
|
||||
@ -201,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.
|
||||
@ -210,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 {
|
||||
@ -222,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")
|
||||
}
|
||||
@ -248,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)
|
||||
@ -259,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))})
|
||||
@ -267,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
|
||||
}
|
||||
@ -503,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
|
||||
}
|
||||
@ -656,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,
|
||||
|
@ -7,6 +7,7 @@ package tls
|
||||
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
|
||||
|
@ -272,6 +272,12 @@ func (*serverHelloMsg) Generate(rand *rand.Rand, size int) reflect.Value {
|
||||
m.selectedIdentityPresent = true
|
||||
m.selectedIdentity = uint16(rand.Intn(0xffff))
|
||||
}
|
||||
if rand.Intn(10) > 5 {
|
||||
m.encryptedClientHello = randomBytes(rand.Intn(50)+1, rand)
|
||||
}
|
||||
if rand.Intn(10) > 5 {
|
||||
m.serverNameAck = rand.Intn(2) == 1
|
||||
}
|
||||
|
||||
return reflect.ValueOf(m)
|
||||
}
|
||||
|
@ -41,11 +41,12 @@ import (
|
||||
// reference connection will always change.
|
||||
|
||||
var (
|
||||
update = flag.Bool("update", false, "update golden files on failure")
|
||||
fast = flag.Bool("fast", false, "impose a quick, possibly flaky timeout on recorded tests")
|
||||
keyFile = flag.String("keylog", "", "destination file for KeyLogWriter")
|
||||
bogoMode = flag.Bool("bogo-mode", false, "Enabled bogo shim mode, ignore everything else")
|
||||
bogoFilter = flag.String("bogo-filter", "", "BoGo test filter")
|
||||
update = flag.Bool("update", false, "update golden files on failure")
|
||||
fast = flag.Bool("fast", false, "impose a quick, possibly flaky timeout on recorded tests")
|
||||
keyFile = flag.String("keylog", "", "destination file for KeyLogWriter")
|
||||
bogoMode = flag.Bool("bogo-mode", false, "Enabled bogo shim mode, ignore everything else")
|
||||
bogoFilter = flag.String("bogo-filter", "", "BoGo test filter")
|
||||
bogoLocalDir = flag.String("bogo-local-dir", "", "Local BoGo to use, instead of fetching from source")
|
||||
)
|
||||
|
||||
func runTestAndUpdateIfNeeded(t *testing.T, name string, run func(t *testing.T, update bool), wait bool) {
|
||||
|
@ -771,7 +771,7 @@ func TestWarningAlertFlood(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCloneFuncFields(t *testing.T) {
|
||||
const expectedCount = 8
|
||||
const expectedCount = 9
|
||||
called := 0
|
||||
|
||||
c1 := Config{
|
||||
@ -807,6 +807,10 @@ func TestCloneFuncFields(t *testing.T) {
|
||||
called |= 1 << 7
|
||||
return nil, nil
|
||||
},
|
||||
EncryptedClientHelloRejectionVerify: func(ConnectionState) error {
|
||||
called |= 1 << 8
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
c2 := c1.Clone()
|
||||
@ -819,6 +823,7 @@ func TestCloneFuncFields(t *testing.T) {
|
||||
c2.VerifyConnection(ConnectionState{})
|
||||
c2.UnwrapSession(nil, ConnectionState{})
|
||||
c2.WrapSession(ConnectionState{}, nil)
|
||||
c2.EncryptedClientHelloRejectionVerify(ConnectionState{})
|
||||
|
||||
if called != (1<<expectedCount)-1 {
|
||||
t.Fatalf("expected %d calls but saw calls %b", expectedCount, called)
|
||||
@ -837,7 +842,7 @@ func TestCloneNonFuncFields(t *testing.T) {
|
||||
switch fn := typ.Field(i).Name; fn {
|
||||
case "Rand":
|
||||
f.Set(reflect.ValueOf(io.Reader(os.Stdin)))
|
||||
case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "WrapSession", "UnwrapSession":
|
||||
case "Time", "GetCertificate", "GetConfigForClient", "VerifyPeerCertificate", "VerifyConnection", "GetClientCertificate", "WrapSession", "UnwrapSession", "EncryptedClientHelloRejectionVerify":
|
||||
// DeepEqual can't compare functions. If you add a
|
||||
// function field to this list, you must also change
|
||||
// TestCloneFuncFields to ensure that the func field is
|
||||
@ -872,6 +877,8 @@ func TestCloneNonFuncFields(t *testing.T) {
|
||||
f.Set(reflect.ValueOf([]CurveID{CurveP256}))
|
||||
case "Renegotiation":
|
||||
f.Set(reflect.ValueOf(RenegotiateOnceAsClient))
|
||||
case "EncryptedClientHelloConfigList":
|
||||
f.Set(reflect.ValueOf([]byte{'x'}))
|
||||
case "mutex", "autoSessionTicketKeys", "sessionTicketKeys":
|
||||
continue // these are unexported fields that are handled separately
|
||||
default:
|
||||
|
Loading…
Reference in New Issue
Block a user