diff --git a/examples/ech/main.go b/examples/ech/main.go index 3b9e205..f307202 100644 --- a/examples/ech/main.go +++ b/examples/ech/main.go @@ -2,6 +2,9 @@ package main import ( "bufio" + // "crypto/tls" + "encoding/base64" + "errors" "fmt" "io" @@ -24,13 +27,13 @@ var ( // var requestAddr = "crypto.cloudflare.com:443" // var requestPath = "/cdn-cgi/trace" -// var requestHostname = "tls-ech.dev" // speaks http2 and TLS 1.3 and ECH and PQ -// var requestAddr = "tls-ech.dev:443" -// var requestPath = "/" +var requestHostname = "tls-ech.dev" // speaks http2 and TLS 1.3 and ECH and PQ +var requestAddr = "tls-ech.dev:443" +var requestPath = "/" -var requestHostname = "defo.ie" // speaks http2 and TLS 1.3 and ECH and PQ -var requestAddr = "defo.ie:443" -var requestPath = "/ech-check.php" +// var requestHostname = "defo.ie" // speaks http2 and TLS 1.3 and ECH and PQ +// var requestAddr = "defo.ie:443" +// var requestPath = "/ech-check.php" // var requestHostname = "client.tlsfingerprint.io" // speaks http2 and TLS 1.3 and ECH and PQ // var requestAddr = "client.tlsfingerprint.io:443" @@ -41,29 +44,37 @@ func HttpGetCustom(hostname string, addr string) (*http.Response, error) { if err != nil { return nil, fmt.Errorf("os.OpenFile error: %+v", err) } + + echConf, err := base64.RawStdEncoding.DecodeString("AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA") + if err != nil { + return nil, err + } + config := tls.Config{ - ServerName: hostname, - KeyLogWriter: klw, + ServerName: hostname, + KeyLogWriter: klw, + EncryptedClientHelloConfigList: echConf, } dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } - uTlsConn := tls.UClient(dialConn, &config, tls.HelloCustom) + uTlsConn := tls.UClient(dialConn, &config, tls.HelloGolang) + // uTlsConn := tls.Client(dialConn, &config) defer uTlsConn.Close() // do not use this particular spec in production // make sure to generate a separate copy of ClientHelloSpec for every connection - spec, err := tls.UTLSIdToSpec(tls.HelloChrome_120) - // spec, err := tls.UTLSIdToSpec(tls.HelloFirefox_120) - if err != nil { - return nil, fmt.Errorf("tls.UTLSIdToSpec error: %+v", err) - } + // spec, err := tls.UTLSIdToSpec(tls.HelloChrome_120) + // // spec, err := tls.UTLSIdToSpec(tls.HelloFirefox_120) + // if err != nil { + // return nil, fmt.Errorf("tls.UTLSIdToSpec error: %+v", err) + // } - err = uTlsConn.ApplyPreset(&spec) - if err != nil { - return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) - } + // err = uTlsConn.ApplyPreset(&spec) + // if err != nil { + // return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + // } err = uTlsConn.Handshake() if err != nil { diff --git a/u_conn.go b/u_conn.go index d39fca8..a696c89 100644 --- a/u_conn.go +++ b/u_conn.go @@ -51,6 +51,9 @@ type UConn struct { // ech extension is a shortcut to the ECH extension in the Extensions slice if there is one. ech ECHExtension + + // echCtx is the echContex returned by makeClientHello() + echCtx *echContext } // UClient returns a new uTLS client, with behavior depending on clientHelloID. @@ -107,7 +110,7 @@ func (uconn *UConn) buildHandshakeState(loadSession bool) error { uAssert(uconn.clientHelloBuildStatus == NotBuilt, "BuildHandshakeState failed: invalid call, client hello has already been built by utls") // use default Golang ClientHello. - hello, keySharePrivate, _, err := uconn.makeClientHello() + hello, keySharePrivate, ech, err := uconn.makeClientHello() if err != nil { return err } @@ -115,6 +118,7 @@ func (uconn *UConn) buildHandshakeState(loadSession bool) error { uconn.HandshakeState.Hello = hello.getPublicPtr() uconn.HandshakeState.State13.KeyShareKeys = keySharePrivate.ToPublic() uconn.HandshakeState.C = uconn.Conn + uconn.echCtx = ech uconn.clientHelloBuildStatus = BuildByGoTLS } else { uAssert(uconn.clientHelloBuildStatus == BuildByUtls || uconn.clientHelloBuildStatus == NotBuilt, "BuildHandshakeState failed: invalid call, client hello has already been built by go-tls") @@ -473,153 +477,6 @@ func (c *UConn) Write(b []byte) (int, error) { return n + m, c.out.setErrorLocked(err) } -// clientHandshakeWithOneState checks that exactly one expected state is set (1.2 or 1.3) -// and performs client TLS handshake with that state -func (c *UConn) clientHandshake(ctx context.Context) (err error) { - // [uTLS section begins] - hello := c.HandshakeState.Hello.getPrivatePtr() - defer func() { c.HandshakeState.Hello = hello.getPublicPtr() }() - - sessionIsLocked := c.utls.sessionController.isSessionLocked() - - // after this point exactly 1 out of 2 HandshakeState pointers is non-nil, - // useTLS13 variable tells which pointer - // [uTLS section ends] - - if c.config == nil { - c.config = defaultConfig() - } - - // This may be a renegotiation handshake, in which case some fields - // need to be reset. - c.didResume = false - - // [uTLS section begins] - // don't make new ClientHello, use hs.hello - // preserve the checks from beginning and end of makeClientHello() - if len(c.config.ServerName) == 0 && !c.config.InsecureSkipVerify && len(c.config.InsecureServerNameToVerify) == 0 { - return errors.New("tls: at least one of ServerName, InsecureSkipVerify or InsecureServerNameToVerify must be specified in the tls.Config") - } - - nextProtosLength := 0 - for _, proto := range c.config.NextProtos { - if l := len(proto); l == 0 || l > 255 { - return errors.New("tls: invalid NextProtos value") - } else { - nextProtosLength += 1 + l - } - } - - if nextProtosLength > 0xffff { - return errors.New("tls: NextProtos values too large") - } - - if c.handshakes > 0 { - hello.secureRenegotiation = c.clientFinished[:] - } - - var ( - session *SessionState - earlySecret []byte - binderKey []byte - ) - if !sessionIsLocked { - // [uTLS section ends] - - session, earlySecret, binderKey, err = c.loadSession(hello) - - // [uTLS section start] - } else { - session = c.HandshakeState.Session - earlySecret = c.HandshakeState.State13.EarlySecret - binderKey = c.HandshakeState.State13.BinderKey - } - // [uTLS section ends] - if err != nil { - return err - } - if session != nil { - defer func() { - // If we got a handshake failure when resuming a session, throw away - // the session ticket. See RFC 5077, Section 3.2. - // - // RFC 8446 makes no mention of dropping tickets on failure, but it - // does require servers to abort on invalid binders, so we need to - // delete tickets to recover from a corrupted PSK. - if err != nil { - if cacheKey := c.clientSessionCacheKey(); cacheKey != "" { - c.config.ClientSessionCache.Put(cacheKey, nil) - } - } - }() - } - - if _, err := c.writeHandshakeRecord(hello, nil); err != nil { - return err - } - - if hello.earlyData { - suite := cipherSuiteTLS13ByID(session.cipherSuite) - transcript := suite.hash.New() - if err := transcriptMsg(hello, transcript); err != nil { - return err - } - earlyTrafficSecret := suite.deriveSecret(earlySecret, clientEarlyTrafficLabel, transcript) - c.quicSetWriteSecret(QUICEncryptionLevelEarly, suite.id, earlyTrafficSecret) - } - - msg, err := c.readHandshake(nil) - if err != nil { - return err - } - - serverHello, ok := msg.(*serverHelloMsg) - if !ok { - c.sendAlert(alertUnexpectedMessage) - return unexpectedMessageError(serverHello, msg) - } - - if err := c.pickTLSVersion(serverHello); err != nil { - return err - } - - // uTLS: do not create new handshakeState, use existing one - if c.vers == VersionTLS13 { - hs13 := c.HandshakeState.toPrivate13() - hs13.serverHello = serverHello - hs13.hello = hello - if hs13.keySharesParams == nil { - hs13.keySharesParams = NewKeySharesParameters() - } - if !sessionIsLocked { - hs13.earlySecret = earlySecret - hs13.binderKey = binderKey - hs13.session = session - } - hs13.ctx = ctx - // In TLS 1.3, session tickets are delivered after the handshake. - err = hs13.handshake() - if handshakeState := hs13.toPublic13(); handshakeState != nil { - c.HandshakeState = *handshakeState - } - return err - } - - hs12 := c.HandshakeState.toPrivate12() - hs12.serverHello = serverHello - hs12.hello = hello - hs12.ctx = ctx - hs12.session = session - err = hs12.handshake() - if handshakeState := hs12.toPublic12(); handshakeState != nil { - c.HandshakeState = *handshakeState - } - if err != nil { - return err - } - return nil -} - func (uconn *UConn) ApplyConfig() error { for _, ext := range uconn.Extensions { err := ext.writeToUConn(uconn) diff --git a/u_handshake_client.go b/u_handshake_client.go index 0301ed7..33543dc 100644 --- a/u_handshake_client.go +++ b/u_handshake_client.go @@ -7,6 +7,7 @@ package tls import ( "bytes" "compress/zlib" + "context" "errors" "fmt" "io" @@ -383,3 +384,175 @@ func (c *Conn) makeClientHelloForApplyPreset() (*clientHelloMsg, *keySharePrivat return hello, keyShareKeys, ech, nil } + +// clientHandshakeWithOneState checks that exactly one expected state is set (1.2 or 1.3) +// and performs client TLS handshake with that state +func (c *UConn) clientHandshake(ctx context.Context) (err error) { + // [uTLS section begins] + hello := c.HandshakeState.Hello.getPrivatePtr() + ech := c.echCtx + defer func() { c.HandshakeState.Hello = hello.getPublicPtr() }() + + sessionIsLocked := c.utls.sessionController.isSessionLocked() + + // after this point exactly 1 out of 2 HandshakeState pointers is non-nil, + // useTLS13 variable tells which pointer + // [uTLS section ends] + + if c.config == nil { + c.config = defaultConfig() + } + + // This may be a renegotiation handshake, in which case some fields + // need to be reset. + c.didResume = false + + // [uTLS section begins] + // don't make new ClientHello, use hs.hello + // preserve the checks from beginning and end of makeClientHello() + if len(c.config.ServerName) == 0 && !c.config.InsecureSkipVerify && len(c.config.InsecureServerNameToVerify) == 0 { + return errors.New("tls: at least one of ServerName, InsecureSkipVerify or InsecureServerNameToVerify must be specified in the tls.Config") + } + + nextProtosLength := 0 + for _, proto := range c.config.NextProtos { + if l := len(proto); l == 0 || l > 255 { + return errors.New("tls: invalid NextProtos value") + } else { + nextProtosLength += 1 + l + } + } + + if nextProtosLength > 0xffff { + return errors.New("tls: NextProtos values too large") + } + + if c.handshakes > 0 { + hello.secureRenegotiation = c.clientFinished[:] + } + + var ( + session *SessionState + earlySecret []byte + binderKey []byte + ) + if !sessionIsLocked { + // [uTLS section ends] + + session, earlySecret, binderKey, err = c.loadSession(hello) + + // [uTLS section start] + } else { + session = c.HandshakeState.Session + earlySecret = c.HandshakeState.State13.EarlySecret + binderKey = c.HandshakeState.State13.BinderKey + } + // [uTLS section ends] + if err != nil { + return err + } + if session != nil { + defer func() { + // If we got a handshake failure when resuming a session, throw away + // the session ticket. See RFC 5077, Section 3.2. + // + // RFC 8446 makes no mention of dropping tickets on failure, but it + // does require servers to abort on invalid binders, so we need to + // delete tickets to recover from a corrupted PSK. + if err != nil { + if cacheKey := c.clientSessionCacheKey(); cacheKey != "" { + c.config.ClientSessionCache.Put(cacheKey, nil) + } + } + }() + } + + 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 + } + + if hello.earlyData { + suite := cipherSuiteTLS13ByID(session.cipherSuite) + transcript := suite.hash.New() + if err := transcriptMsg(hello, transcript); err != nil { + return err + } + earlyTrafficSecret := suite.deriveSecret(earlySecret, clientEarlyTrafficLabel, transcript) + c.quicSetWriteSecret(QUICEncryptionLevelEarly, suite.id, earlyTrafficSecret) + } + + // serverHelloMsg is not included in the transcript + msg, err := c.readHandshake(nil) + if err != nil { + return err + } + + serverHello, ok := msg.(*serverHelloMsg) + if !ok { + c.sendAlert(alertUnexpectedMessage) + return unexpectedMessageError(serverHello, msg) + } + + if err := c.pickTLSVersion(serverHello); err != nil { + return err + } + + // uTLS: do not create new handshakeState, use existing one + if c.vers == VersionTLS13 { + hs13 := c.HandshakeState.toPrivate13() + hs13.serverHello = serverHello + hs13.hello = hello + hs13.echContext = ech + if !sessionIsLocked { + hs13.earlySecret = earlySecret + hs13.binderKey = binderKey + hs13.session = session + } + hs13.ctx = ctx + // In TLS 1.3, session tickets are delivered after the handshake. + err = hs13.handshake() + if handshakeState := hs13.toPublic13(); handshakeState != nil { + c.HandshakeState = *handshakeState + } + return err + } + + hs12 := c.HandshakeState.toPrivate12() + hs12.serverHello = serverHello + hs12.hello = hello + hs12.ctx = ctx + hs12.session = session + err = hs12.handshake() + if handshakeState := hs12.toPublic12(); handshakeState != nil { + c.HandshakeState = *handshakeState + } + if err != nil { + return err + } + return nil +} diff --git a/u_parrots.go b/u_parrots.go index 48a9244..7e3e576 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -2626,7 +2626,7 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { return err } - privateHello, clientKeySharePrivate, _, err := uconn.makeClientHelloForApplyPreset() + privateHello, clientKeySharePrivate, ech, err := uconn.makeClientHelloForApplyPreset() if err != nil { return err } @@ -2637,6 +2637,7 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { uconn.HandshakeState.State13.KeyShareKeys = &KeySharePrivateKeys{} } uconn.HandshakeState.State13.KeySharesParams = NewKeySharesParameters() + uconn.echCtx = ech hello := uconn.HandshakeState.Hello switch len(hello.Random) { diff --git a/u_public.go b/u_public.go index 58fd85c..735f26d 100644 --- a/u_public.go +++ b/u_public.go @@ -436,7 +436,8 @@ type PubClientHelloMsg struct { PskBinders [][]byte QuicTransportParameters []byte - cachedPrivateHello *clientHelloMsg // todo: further optimize to reduce clientHelloMsg construction + cachedPrivateHello *clientHelloMsg // todo: further optimize to reduce clientHelloMsg construction + encryptedClientHello []byte } func (chm *PubClientHelloMsg) getPrivatePtr() *clientHelloMsg { @@ -472,6 +473,7 @@ func (chm *PubClientHelloMsg) getPrivatePtr() *clientHelloMsg { pskIdentities: PskIdentities(chm.PskIdentities).ToPrivate(), pskBinders: chm.PskBinders, quicTransportParameters: chm.QuicTransportParameters, + encryptedClientHello: chm.encryptedClientHello, nextProtoNeg: chm.NextProtoNeg, } @@ -523,6 +525,7 @@ func (chm *clientHelloMsg) getPublicPtr() *PubClientHelloMsg { PskBinders: chm.pskBinders, QuicTransportParameters: chm.quicTransportParameters, cachedPrivateHello: chm, + encryptedClientHello: chm.encryptedClientHello, } } }