From 8d0f1c40c755c9b68f3d8bc926d4934f8ac4286e Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Sun, 2 Oct 2022 16:49:53 -0400 Subject: [PATCH] feat: implement encrypted client extensions (alps) --- common.go | 5 +++ conn.go | 11 +++++- handshake_client_tls13.go | 29 ++++++++++++++ handshake_messages.go | 15 ++++++-- u_common.go | 7 ++-- u_parrots.go | 4 +- u_tls_extensions.go | 79 ++++++++++++++++++++++++++++++++++++--- 7 files changed, 135 insertions(+), 15 deletions(-) diff --git a/common.go b/common.go index 48cc7be0..c3fe9f8a 100644 --- a/common.go +++ b/common.go @@ -204,6 +204,7 @@ type ConnectionState struct { CipherSuite uint16 // cipher suite in use (TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, ...) NegotiatedProtocol string // negotiated next protocol (not guaranteed to be from Config.NextProtos) NegotiatedProtocolIsMutual bool // negotiated protocol was advertised by server (client side only) + PeerApplicationSettings []byte // application settings provided by peer ServerName string // server name requested by client, if any (server side only) PeerCertificates []*x509.Certificate // certificate chain presented by remote peer VerifiedChains [][]*x509.Certificate // verified chains built from PeerCertificates @@ -500,6 +501,10 @@ type Config struct { // order of preference. NextProtos []string + // ApplicationSettings is a set of application settings to use which each + // application protocol. + ApplicationSettings map[string][]byte + // ServerName is used to verify the hostname on the returned // certificates unless InsecureSkipVerify is given. It is also included // in the client's handshake to support virtual hosting unless it is diff --git a/conn.go b/conn.go index fed41032..7dfffd84 100644 --- a/conn.go +++ b/conn.go @@ -83,6 +83,10 @@ type Conn struct { clientProtocol string clientProtocolFallback bool + hasApplicationSettings bool + peerApplicationSettings []byte + localApplicationSettings []byte + // input/output in, out halfConn rawInput bytes.Buffer // raw input, starting with a record header @@ -1052,7 +1056,11 @@ func (c *Conn) readHandshake() (interface{}, error) { case typeFinished: m = new(finishedMsg) case typeEncryptedExtensions: - m = new(encryptedExtensionsMsg) + if c.isClient { + m = new(encryptedExtensionsMsg) + } else { + m = new(clientEncryptedExtensionsMsg) + } case typeEndOfEarlyData: m = new(endOfEarlyDataMsg) case typeKeyUpdate: @@ -1392,6 +1400,7 @@ func (c *Conn) ConnectionState() ConnectionState { if state.HandshakeComplete { state.Version = c.vers state.NegotiatedProtocol = c.clientProtocol + state.PeerApplicationSettings = c.peerApplicationSettings state.DidResume = c.didResume state.NegotiatedProtocolIsMutual = !c.clientProtocolFallback state.CipherSuite = c.cipherSuite diff --git a/handshake_client_tls13.go b/handshake_client_tls13.go index 549dafab..ed8bedd9 100644 --- a/handshake_client_tls13.go +++ b/handshake_client_tls13.go @@ -95,6 +95,9 @@ func (hs *clientHandshakeStateTLS13) handshake() error { if err := hs.readServerFinished(); err != nil { return err } + if err := hs.sendClientEncryptedExtensions(); err != nil { + return err + } if err := hs.sendClientCertificate(); err != nil { return err } @@ -459,7 +462,18 @@ func (hs *clientHandshakeStateTLS13) readServerParameters() error { return errors.New("tls: server advertised unrequested ALPN extension") } c.clientProtocol = encryptedExtensions.alpnProtocol + c.hasApplicationSettings = encryptedExtensions.hasApplicationSettings + c.peerApplicationSettings = encryptedExtensions.applicationSettings + if c.hasApplicationSettings { + if c.vers < VersionTLS13 { + return errors.New("tls: server sent application settings at invalid version") + } + if len(c.clientProtocol) == 0 { + return errors.New("tls: server sent application settings without ALPN") + } + c.localApplicationSettings = c.config.ApplicationSettings[hs.serverHello.alpnProtocol] + } return nil } @@ -721,6 +735,21 @@ func (hs *clientHandshakeStateTLS13) sendClientFinished() error { return nil } +func (hs *clientHandshakeStateTLS13) sendClientEncryptedExtensions() error { + c := hs.c + clientEncryptedExtensions := new(clientEncryptedExtensionsMsg) + if c.hasApplicationSettings { + clientEncryptedExtensions.hasApplicationSettings = true + clientEncryptedExtensions.applicationSettings = c.localApplicationSettings + hs.transcript.Write(clientEncryptedExtensions.marshal()) + if _, err := c.writeRecord(recordTypeHandshake, clientEncryptedExtensions.marshal()); err != nil { + return err + } + } + + return nil +} + func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error { if !c.isClient { c.sendAlert(alertUnexpectedMessage) diff --git a/handshake_messages.go b/handshake_messages.go index 3dc4b775..733886ee 100644 --- a/handshake_messages.go +++ b/handshake_messages.go @@ -6,8 +6,9 @@ package tls import ( "fmt" - "golang.org/x/crypto/cryptobyte" "strings" + + "golang.org/x/crypto/cryptobyte" ) // The marshalingFunction type is an adapter to allow the use of ordinary @@ -863,8 +864,11 @@ func (m *serverHelloMsg) unmarshal(data []byte) bool { } type encryptedExtensionsMsg struct { - raw []byte - alpnProtocol string + raw []byte + alpnProtocol string + hasApplicationSettings bool + applicationSettings []byte + customExtension []byte } func (m *encryptedExtensionsMsg) marshal() []byte { @@ -922,7 +926,12 @@ func (m *encryptedExtensionsMsg) unmarshal(data []byte) bool { proto.Empty() || !protoList.Empty() { return false } + m.alpnProtocol = string(proto) + case extensionApplicationSettings: + m.hasApplicationSettings = true + m.applicationSettings = []byte(extData) + continue default: // Ignore unknown extensions. continue diff --git a/u_common.go b/u_common.go index 2501df91..92329ebb 100644 --- a/u_common.go +++ b/u_common.go @@ -20,9 +20,10 @@ const ( utlsExtensionExtendedMasterSecret uint16 = 23 // https://tools.ietf.org/html/rfc7627 // extensions with 'fake' prefix break connection, if server echoes them back - fakeExtensionChannelIDOld uint16 = 30031 // not IANA assigned - fakeExtensionChannelID uint16 = 30032 // not IANA assigned - fakeExtensionALPS uint16 = 17513 // not IANA assigned + fakeExtensionChannelIDOld uint16 = 30031 // not IANA assigned + fakeExtensionChannelID uint16 = 30032 // not IANA assigned + extensionApplicationSettings uint16 = 17513 // not IANA assigned + extensionCustom uint16 = 1234 // not IANA assigned fakeRecordSizeLimit uint16 = 0x001c ) diff --git a/u_parrots.go b/u_parrots.go index 7763d3aa..75f5e7ee 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -354,7 +354,7 @@ func UtlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &CompressCertificateExtension{ Algorithms: []CertCompressionAlgo{CertCompressionBrotli}, }, - &FakeALPSExtension{SupportedProtocols: []string{"h2"}}, + &ALPSExtension{SupportedProtocols: []string{"h2"}}, &UtlsGREASEExtension{}, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, }, @@ -425,7 +425,7 @@ func UtlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) { &CompressCertificateExtension{ Algorithms: []CertCompressionAlgo{CertCompressionBrotli}, }, - &FakeALPSExtension{SupportedProtocols: []string{"h2"}}, + &ALPSExtension{SupportedProtocols: []string{"h2"}}, &UtlsGREASEExtension{}, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}, }, diff --git a/u_tls_extensions.go b/u_tls_extensions.go index 9bc3fb9c..7880b0c9 100644 --- a/u_tls_extensions.go +++ b/u_tls_extensions.go @@ -7,6 +7,8 @@ package tls import ( "errors" "io" + + "golang.org/x/crypto/cryptobyte" ) type TLSExtension interface { @@ -737,15 +739,15 @@ func (e *FakeRecordSizeLimitExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } -type FakeALPSExtension struct { +type ALPSExtension struct { SupportedProtocols []string } -func (e *FakeALPSExtension) writeToUConn(uc *UConn) error { +func (e *ALPSExtension) writeToUConn(uc *UConn) error { return nil } -func (e *FakeALPSExtension) Len() int { +func (e *ALPSExtension) Len() int { bLen := 2 + 2 + 2 for _, s := range e.SupportedProtocols { bLen += 1 + len(s) @@ -753,13 +755,13 @@ func (e *FakeALPSExtension) Len() int { return bLen } -func (e *FakeALPSExtension) Read(b []byte) (int, error) { +func (e *ALPSExtension) Read(b []byte) (int, error) { if len(b) < e.Len() { return 0, io.ErrShortBuffer } - b[0] = byte(fakeExtensionALPS >> 8) - b[1] = byte(fakeExtensionALPS & 0xff) + b[0] = byte(extensionApplicationSettings >> 8) + b[1] = byte(extensionApplicationSettings & 0xff) lengths := b[2:] b = b[6:] @@ -780,3 +782,68 @@ func (e *FakeALPSExtension) Read(b []byte) (int, error) { return e.Len(), io.EOF } + +type clientEncryptedExtensionsMsg struct { + raw []byte + applicationSettings []byte + hasApplicationSettings bool + customExtension []byte +} + +func (m *clientEncryptedExtensionsMsg) marshal() (x []byte) { + if m.raw != nil { + return m.raw + } + + var builder cryptobyte.Builder + builder.AddUint8(typeEncryptedExtensions) + builder.AddUint24LengthPrefixed(func(body *cryptobyte.Builder) { + body.AddUint16LengthPrefixed(func(extensions *cryptobyte.Builder) { + if m.hasApplicationSettings { + extensions.AddUint16(extensionApplicationSettings) + extensions.AddUint16LengthPrefixed(func(msg *cryptobyte.Builder) { + msg.AddBytes(m.applicationSettings) + }) + } + if len(m.customExtension) > 0 { + extensions.AddUint16(extensionCustom) + extensions.AddUint16LengthPrefixed(func(msg *cryptobyte.Builder) { + msg.AddBytes(m.customExtension) + }) + } + }) + }) + + m.raw = builder.BytesOrPanic() + return m.raw +} + +func (m *clientEncryptedExtensionsMsg) unmarshal(data []byte) bool { + *m = clientEncryptedExtensionsMsg{raw: data} + s := cryptobyte.String(data) + + var extensions cryptobyte.String + if !s.Skip(4) || // message type and uint24 length field + !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() { + return false + } + + for !extensions.Empty() { + var extension uint16 + var extData cryptobyte.String + if !extensions.ReadUint16(&extension) || + !extensions.ReadUint16LengthPrefixed(&extData) { + return false + } + + switch extension { + case extensionApplicationSettings: + m.hasApplicationSettings = true + m.applicationSettings = []byte(extData) + default: + // Unknown extensions are illegal in EncryptedExtensions. + return false + } + } + return true +}