diff --git a/conn.go b/conn.go index cbebb7d..7260563 100644 --- a/conn.go +++ b/conn.go @@ -70,7 +70,7 @@ type Conn struct { // ekm is a closure for exporting keying material. ekm func(label string, context []byte, length int) ([]byte, error) // resumptionSecret is the resumption_master_secret for handling - // NewSessionTicket messages. nil if config.SessionTicketsDisabled. + // or sending NewSessionTicket messages. resumptionSecret []byte // ticketKeys is the set of active session ticket keys for this diff --git a/handshake_client.go b/handshake_client.go index bdc605e..cd4982e 100644 --- a/handshake_client.go +++ b/handshake_client.go @@ -202,6 +202,16 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { 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 { @@ -359,6 +369,19 @@ func (c *Conn) loadSession(hello *clientHelloMsg) ( return nil, nil, nil, nil } + if c.quic != nil && session.EarlyData { + // For 0-RTT, the cipher suite has to match exactly, and we need to be + // offering the same ALPN. + if mutualCipherSuite(hello.cipherSuites, session.cipherSuite) != nil { + for _, alpn := range hello.alpnProtocols { + if alpn == session.alpnProtocol { + hello.earlyData = true + break + } + } + } + } + // Set the pre_shared_key extension. See RFC 8446, Section 4.2.11.1. ticketAge := c.config.time().Sub(time.Unix(int64(session.createdAt), 0)) identity := pskIdentity{ diff --git a/handshake_client_tls13.go b/handshake_client_tls13.go index d1ed4a4..a96698a 100644 --- a/handshake_client_tls13.go +++ b/handshake_client_tls13.go @@ -281,6 +281,11 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { } } + if hs.hello.earlyData { + hs.hello.earlyData = false + c.quicRejectedEarlyData() + } + if _, err := hs.c.writeHandshakeRecord(hs.hello, hs.transcript); err != nil { return err } @@ -455,6 +460,24 @@ func (hs *clientHandshakeStateTLS13) readServerParameters() error { } } + if !hs.hello.earlyData && encryptedExtensions.earlyData { + c.sendAlert(alertUnsupportedExtension) + return errors.New("tls: server sent an unexpected early_data extension") + } + if hs.hello.earlyData && !encryptedExtensions.earlyData { + c.quicRejectedEarlyData() + } + if encryptedExtensions.earlyData { + if hs.session.cipherSuite != c.cipherSuite { + c.sendAlert(alertHandshakeFailure) + return errors.New("tls: server accepted 0-RTT with the wrong cipher suite") + } + if hs.session.alpnProtocol != c.clientProtocol { + c.sendAlert(alertHandshakeFailure) + return errors.New("tls: server accepted 0-RTT with the wrong ALPN") + } + } + return nil } @@ -715,6 +738,12 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error { return errors.New("tls: received a session ticket with invalid lifetime") } + // RFC 9001, Section 4.6.1 + if c.quic != nil && msg.maxEarlyData != 0 && msg.maxEarlyData != 0xffffffff { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: invalid early data for QUIC connection") + } + cipherSuite := cipherSuiteTLS13ByID(c.cipherSuite) if cipherSuite == nil || c.resumptionSecret == nil { return c.sendAlert(alertInternalError) @@ -731,6 +760,7 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error { session.secret = psk session.useBy = uint64(c.config.time().Add(lifetime).Unix()) session.ageAdd = msg.ageAdd + session.EarlyData = c.quic != nil && msg.maxEarlyData == 0xffffffff // RFC 9001, Section 4.6.1 cs := &ClientSessionState{ticket: msg.label, session: session} if cacheKey := c.clientSessionCacheKey(); cacheKey != "" { diff --git a/handshake_messages.go b/handshake_messages.go index fde639d..ae2b892 100644 --- a/handshake_messages.go +++ b/handshake_messages.go @@ -876,6 +876,7 @@ type encryptedExtensionsMsg struct { raw []byte alpnProtocol string quicTransportParameters []byte + earlyData bool } func (m *encryptedExtensionsMsg) marshal() ([]byte, error) { @@ -904,6 +905,11 @@ func (m *encryptedExtensionsMsg) marshal() ([]byte, error) { b.AddBytes(m.quicTransportParameters) }) } + if m.earlyData { + // RFC 8446, Section 4.2.10 + b.AddUint16(extensionEarlyData) + b.AddUint16(0) // empty extension_data + } }) }) @@ -947,6 +953,9 @@ func (m *encryptedExtensionsMsg) unmarshal(data []byte) bool { if !extData.CopyBytes(m.quicTransportParameters) { return false } + case extensionEarlyData: + // RFC 8446, Section 4.2.10 + m.earlyData = true default: // Ignore unknown extensions. continue diff --git a/handshake_server_tls13.go b/handshake_server_tls13.go index 02d3705..1abf770 100644 --- a/handshake_server_tls13.go +++ b/handshake_server_tls13.go @@ -34,6 +34,7 @@ type serverHandshakeStateTLS13 struct { hello *serverHelloMsg sentDummyCCS bool usingPSK bool + earlyData bool suite *cipherSuiteTLS13 cert *Certificate sigAlg SignatureScheme @@ -191,7 +192,12 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error { return errors.New("tls: initial handshake had non-empty renegotiation extension") } - if hs.clientHello.earlyData { + if hs.clientHello.earlyData && c.quic != nil { + if len(hs.clientHello.pskIdentities) == 0 { + c.sendAlert(alertIllegalParameter) + return errors.New("tls: early_data without pre_shared_key") + } + } else if hs.clientHello.earlyData { // See RFC 8446, Section 4.2.10 for the complicated behavior required // here. The scenario is that a different server at our address offered // to accept early data in the past, which we can't handle. For now, all @@ -278,6 +284,13 @@ GroupSelection: return errors.New("tls: invalid client key share") } + selectedProto, err := negotiateALPN(c.config.NextProtos, hs.clientHello.alpnProtocols, c.quic != nil) + if err != nil { + c.sendAlert(alertNoApplicationProtocol) + return err + } + c.clientProtocol = selectedProto + if c.quic != nil { if hs.clientHello.quicTransportParameters == nil { // RFC 9001 Section 8.2. @@ -358,10 +371,6 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { continue } - // We don't check the obfuscated ticket age because it's affected by - // clock skew and it's only a freshness signal useful for shrinking the - // window for replay attacks, which don't affect us as we don't do 0-RTT. - pskSuite := cipherSuiteTLS13ByID(sessionState.cipherSuite) if pskSuite == nil || pskSuite.hash != hs.suite.hash { continue @@ -399,6 +408,19 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error { return errors.New("tls: invalid PSK binder") } + if c.quic != nil && hs.clientHello.earlyData && i == 0 && + sessionState.EarlyData && sessionState.cipherSuite == hs.suite.id && + sessionState.alpnProtocol == c.clientProtocol { + hs.earlyData = true + + transcript := hs.suite.hash.New() + if err := transcriptMsg(hs.clientHello, transcript); err != nil { + return err + } + earlyTrafficSecret := hs.suite.deriveSecret(hs.earlySecret, clientEarlyTrafficLabel, transcript) + c.quicSetReadSecret(QUICEncryptionLevelEarly, hs.suite.id, earlyTrafficSecret) + } + c.didResume = true if err := c.processCertsFromClient(sessionState.certificate()); err != nil { return err @@ -657,14 +679,7 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error { } encryptedExtensions := new(encryptedExtensionsMsg) - - selectedProto, err := negotiateALPN(c.config.NextProtos, hs.clientHello.alpnProtocols, c.quic != nil) - if err != nil { - c.sendAlert(alertNoApplicationProtocol) - return err - } - encryptedExtensions.alpnProtocol = selectedProto - c.clientProtocol = selectedProto + encryptedExtensions.alpnProtocol = c.clientProtocol if c.quic != nil { p, err := c.quicGetTransportParameters() @@ -672,6 +687,7 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error { return err } encryptedExtensions.quicTransportParameters = p + encryptedExtensions.earlyData = hs.earlyData } if _, err := hs.c.writeHandshakeRecord(encryptedExtensions, hs.transcript); err != nil { @@ -812,6 +828,11 @@ func (hs *serverHandshakeStateTLS13) shouldSendSessionTickets() bool { return false } + // QUIC tickets are sent by QUICConn.SendSessionTicket, not automatically. + if hs.c.quic != nil { + return false + } + // Don't send tickets the client wouldn't use. See RFC 8446, Section 4.2.9. for _, pskMode := range hs.clientHello.pskModes { if pskMode == pskModeDHE { @@ -832,16 +853,24 @@ func (hs *serverHandshakeStateTLS13) sendSessionTickets() error { return err } + c.resumptionSecret = hs.suite.deriveSecret(hs.masterSecret, + resumptionLabel, hs.transcript) + if !hs.shouldSendSessionTickets() { return nil } + return c.sendSessionTicket(false) +} - resumptionSecret := hs.suite.deriveSecret(hs.masterSecret, - resumptionLabel, hs.transcript) +func (c *Conn) sendSessionTicket(earlyData bool) error { + suite := cipherSuiteTLS13ByID(c.cipherSuite) + if suite == nil { + return errors.New("tls: internal error: unknown cipher suite") + } // ticket_nonce, which must be unique per connection, is always left at // zero because we only ever send one ticket per connection. - psk := hs.suite.expandLabel(resumptionSecret, "resumption", - nil, hs.suite.hash.Size()) + psk := suite.expandLabel(c.resumptionSecret, "resumption", + nil, suite.hash.Size()) m := new(newSessionTicketMsgTLS13) @@ -850,6 +879,7 @@ func (hs *serverHandshakeStateTLS13) sendSessionTickets() error { return err } state.secret = psk + state.EarlyData = earlyData if c.config.WrapSession != nil { m.label, err = c.config.WrapSession(c.connectionStateLocked(), state) if err != nil { @@ -872,12 +902,17 @@ func (hs *serverHandshakeStateTLS13) sendSessionTickets() error { // The value is not stored anywhere; we never need to check the ticket age // because 0-RTT is not supported. ageAdd := make([]byte, 4) - _, err = hs.c.config.rand().Read(ageAdd) + _, err = c.config.rand().Read(ageAdd) if err != nil { return err } m.ageAdd = binary.LittleEndian.Uint32(ageAdd) + if earlyData { + // RFC 9001, Section 4.6.1 + m.maxEarlyData = 0xffffffff + } + if _, err := c.writeHandshakeRecord(m, nil); err != nil { return err } diff --git a/key_schedule.go b/key_schedule.go index cd63c45..46c7d4a 100644 --- a/key_schedule.go +++ b/key_schedule.go @@ -21,6 +21,7 @@ import ( const ( resumptionBinderLabel = "res binder" + clientEarlyTrafficLabel = "c e traffic" clientHandshakeTrafficLabel = "c hs traffic" serverHandshakeTrafficLabel = "s hs traffic" clientApplicationTrafficLabel = "c ap traffic" diff --git a/quic.go b/quic.go index 40537d5..06c2e19 100644 --- a/quic.go +++ b/quic.go @@ -16,6 +16,7 @@ type QUICEncryptionLevel int const ( QUICEncryptionLevelInitial = QUICEncryptionLevel(iota) + QUICEncryptionLevelEarly QUICEncryptionLevelHandshake QUICEncryptionLevelApplication ) @@ -24,6 +25,8 @@ func (l QUICEncryptionLevel) String() string { switch l { case QUICEncryptionLevelInitial: return "Initial" + case QUICEncryptionLevelEarly: + return "Early" case QUICEncryptionLevelHandshake: return "Handshake" case QUICEncryptionLevelApplication: @@ -39,6 +42,8 @@ func (l QUICEncryptionLevel) String() string { // Methods of QUICConn are not safe for concurrent use. type QUICConn struct { conn *Conn + + sessionTicketSent bool } // A QUICConfig configures a QUICConn. @@ -79,6 +84,11 @@ const ( // connection will never generate a QUICTransportParametersRequired event. QUICTransportParametersRequired + // QUICRejectedEarlyData indicates that the server rejected 0-RTT data even + // if we offered it. It's returned before QUICEncryptionLevelApplication + // keys are returned. + QUICRejectedEarlyData + // QUICHandshakeDone indicates that the TLS handshake has completed. QUICHandshakeDone ) @@ -106,10 +116,10 @@ type quicState struct { nextEvent int // eventArr is a statically allocated event array, large enough to handle - // the usual maximum number of events resulting from a single call: - // transport parameters, Initial data, Handshake write and read secrets, - // Handshake data, Application write secret, Application data. - eventArr [7]QUICEvent + // the usual maximum number of events resulting from a single call: transport + // parameters, Initial data, Early read secret, Handshake write and read + // secrets, Handshake data, Application write secret, Application data. + eventArr [8]QUICEvent started bool signalc chan struct{} // handshake data is available to be read @@ -237,6 +247,24 @@ func (q *QUICConn) HandleData(level QUICEncryptionLevel, data []byte) error { return nil } +// SendSessionTicket sends a session ticket to the client. +// It produces connection events, which may be read with NextEvent. +// Currently, it can only be called once. +func (q *QUICConn) SendSessionTicket(earlyData bool) error { + c := q.conn + if !c.isHandshakeComplete.Load() { + return quicError(errors.New("tls: SendSessionTicket called before handshake completed")) + } + if c.isClient { + return quicError(errors.New("tls: SendSessionTicket called on the client")) + } + if q.sessionTicketSent { + return quicError(errors.New("tls: SendSessionTicket called multiple times")) + } + q.sessionTicketSent = true + return quicError(c.sendSessionTicket(earlyData)) +} + // ConnectionState returns basic TLS details about the connection. func (q *QUICConn) ConnectionState() ConnectionState { return q.conn.ConnectionState() @@ -345,6 +373,12 @@ func (c *Conn) quicHandshakeComplete() { }) } +func (c *Conn) quicRejectedEarlyData() { + c.quic.events = append(c.quic.events, QUICEvent{ + Kind: QUICRejectedEarlyData, + }) +} + // quicWaitForSignal notifies the QUICConn that handshake progress is blocked, // and waits for a signal that the handshake should proceed. // diff --git a/ticket.go b/ticket.go index ca8cc2d..52ef2cb 100644 --- a/ticket.go +++ b/ticket.go @@ -34,7 +34,12 @@ type SessionState struct { // uint64 created_at; // opaque secret<1..2^8-1>; // opaque extra<0..2^24-1>; + // uint8 early_data = { 0, 1 }; // CertificateEntry certificate_list<0..2^24-1>; + // select (SessionState.early_data) { + // case 0: Empty; + // case 1: opaque alpn<1..2^8-1>; + // }; // select (SessionState.type) { // case server: /* empty */; // case client: struct { @@ -63,6 +68,11 @@ type SessionState struct { // fixed-length suffix. Extra []byte + // EarlyData indicates whether the ticket can be used for 0-RTT in a QUIC + // connection. The application may set this to false if it is true to + // decline to offer 0-RTT even if supported. + EarlyData bool + version uint16 isClient bool cipherSuite uint16 @@ -75,6 +85,7 @@ type SessionState struct { activeCertHandles []*activeCert ocspResponse []byte scts [][]byte + alpnProtocol string // only set if EarlyData is true // Client-side fields. verifiedChains [][]*x509.Certificate @@ -106,7 +117,17 @@ func (s *SessionState) Bytes() ([]byte, error) { b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(s.Extra) }) + if s.EarlyData { + b.AddUint8(1) + } else { + b.AddUint8(0) + } marshalCertificate(&b, s.certificate()) + if s.EarlyData { + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes([]byte(s.alpnProtocol)) + }) + } if s.isClient { b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { for _, chain := range s.verifiedChains { @@ -152,7 +173,7 @@ func certificatesToBytesSlice(certs []*x509.Certificate) [][]byte { func ParseSessionState(data []byte) (*SessionState, error) { ss := &SessionState{} s := cryptobyte.String(data) - var typ uint8 + var typ, earlyData uint8 var cert Certificate if !s.ReadUint16(&ss.version) || !s.ReadUint8(&typ) || @@ -161,10 +182,19 @@ func ParseSessionState(data []byte) (*SessionState, error) { !readUint64(&s, &ss.createdAt) || !readUint8LengthPrefixed(&s, &ss.secret) || !readUint24LengthPrefixed(&s, &ss.Extra) || + !s.ReadUint8(&earlyData) || len(ss.secret) == 0 || !unmarshalCertificate(&s, &cert) { return nil, errors.New("tls: invalid session encoding") } + switch earlyData { + case 0: + ss.EarlyData = false + case 1: + ss.EarlyData = true + default: + return nil, errors.New("tls: invalid session encoding") + } for _, cert := range cert.Certificate { c, err := globalCertCache.newCert(cert) if err != nil { @@ -175,6 +205,13 @@ func ParseSessionState(data []byte) (*SessionState, error) { } ss.ocspResponse = cert.OCSPStaple ss.scts = cert.SignedCertificateTimestamps + if ss.EarlyData { + var alpn []byte + if !readUint8LengthPrefixed(&s, &alpn) { + return nil, errors.New("tls: invalid session encoding") + } + ss.alpnProtocol = string(alpn) + } if isClient := typ == 2; !isClient { if !s.Empty() { return nil, errors.New("tls: invalid session encoding") @@ -236,6 +273,7 @@ func (c *Conn) sessionState() (*SessionState, error) { version: c.vers, cipherSuite: c.cipherSuite, createdAt: uint64(c.config.time().Unix()), + alpnProtocol: c.clientProtocol, peerCertificates: c.peerCertificates, activeCertHandles: c.activeCertHandles, ocspResponse: c.ocspResponse,