0
0
mirror of https://github.com/XTLS/REALITY.git synced 2025-08-23 15:08:37 +00:00

crypto/tls: add QUIC 0-RTT APIs

Fixes #60107

Change-Id: I158b1c2d80d8ebb5ed7a8e6f313f69060754e220
Reviewed-on: https://go-review.googlesource.com/c/go/+/496995
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
yuhan6665 2023-10-14 08:55:34 -04:00
parent e6eabfc665
commit aa68126eeb
8 changed files with 194 additions and 24 deletions

View File

@ -70,7 +70,7 @@ type Conn struct {
// ekm is a closure for exporting keying material. // ekm is a closure for exporting keying material.
ekm func(label string, context []byte, length int) ([]byte, error) ekm func(label string, context []byte, length int) ([]byte, error)
// resumptionSecret is the resumption_master_secret for handling // resumptionSecret is the resumption_master_secret for handling
// NewSessionTicket messages. nil if config.SessionTicketsDisabled. // or sending NewSessionTicket messages.
resumptionSecret []byte resumptionSecret []byte
// ticketKeys is the set of active session ticket keys for this // ticketKeys is the set of active session ticket keys for this

View File

@ -202,6 +202,16 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
return err 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 // serverHelloMsg is not included in the transcript
msg, err := c.readHandshake(nil) msg, err := c.readHandshake(nil)
if err != nil { if err != nil {
@ -359,6 +369,19 @@ func (c *Conn) loadSession(hello *clientHelloMsg) (
return nil, nil, nil, nil 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. // 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)) ticketAge := c.config.time().Sub(time.Unix(int64(session.createdAt), 0))
identity := pskIdentity{ identity := pskIdentity{

View File

@ -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 { if _, err := hs.c.writeHandshakeRecord(hs.hello, hs.transcript); err != nil {
return err 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 return nil
} }
@ -715,6 +738,12 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error {
return errors.New("tls: received a session ticket with invalid lifetime") 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) cipherSuite := cipherSuiteTLS13ByID(c.cipherSuite)
if cipherSuite == nil || c.resumptionSecret == nil { if cipherSuite == nil || c.resumptionSecret == nil {
return c.sendAlert(alertInternalError) return c.sendAlert(alertInternalError)
@ -731,6 +760,7 @@ func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error {
session.secret = psk session.secret = psk
session.useBy = uint64(c.config.time().Add(lifetime).Unix()) session.useBy = uint64(c.config.time().Add(lifetime).Unix())
session.ageAdd = msg.ageAdd 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} cs := &ClientSessionState{ticket: msg.label, session: session}
if cacheKey := c.clientSessionCacheKey(); cacheKey != "" { if cacheKey := c.clientSessionCacheKey(); cacheKey != "" {

View File

@ -876,6 +876,7 @@ type encryptedExtensionsMsg struct {
raw []byte raw []byte
alpnProtocol string alpnProtocol string
quicTransportParameters []byte quicTransportParameters []byte
earlyData bool
} }
func (m *encryptedExtensionsMsg) marshal() ([]byte, error) { func (m *encryptedExtensionsMsg) marshal() ([]byte, error) {
@ -904,6 +905,11 @@ func (m *encryptedExtensionsMsg) marshal() ([]byte, error) {
b.AddBytes(m.quicTransportParameters) 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) { if !extData.CopyBytes(m.quicTransportParameters) {
return false return false
} }
case extensionEarlyData:
// RFC 8446, Section 4.2.10
m.earlyData = true
default: default:
// Ignore unknown extensions. // Ignore unknown extensions.
continue continue

View File

@ -34,6 +34,7 @@ type serverHandshakeStateTLS13 struct {
hello *serverHelloMsg hello *serverHelloMsg
sentDummyCCS bool sentDummyCCS bool
usingPSK bool usingPSK bool
earlyData bool
suite *cipherSuiteTLS13 suite *cipherSuiteTLS13
cert *Certificate cert *Certificate
sigAlg SignatureScheme sigAlg SignatureScheme
@ -191,7 +192,12 @@ func (hs *serverHandshakeStateTLS13) processClientHello() error {
return errors.New("tls: initial handshake had non-empty renegotiation extension") 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 // See RFC 8446, Section 4.2.10 for the complicated behavior required
// here. The scenario is that a different server at our address offered // 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 // 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") 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 c.quic != nil {
if hs.clientHello.quicTransportParameters == nil { if hs.clientHello.quicTransportParameters == nil {
// RFC 9001 Section 8.2. // RFC 9001 Section 8.2.
@ -358,10 +371,6 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error {
continue 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) pskSuite := cipherSuiteTLS13ByID(sessionState.cipherSuite)
if pskSuite == nil || pskSuite.hash != hs.suite.hash { if pskSuite == nil || pskSuite.hash != hs.suite.hash {
continue continue
@ -399,6 +408,19 @@ func (hs *serverHandshakeStateTLS13) checkForResumption() error {
return errors.New("tls: invalid PSK binder") 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 c.didResume = true
if err := c.processCertsFromClient(sessionState.certificate()); err != nil { if err := c.processCertsFromClient(sessionState.certificate()); err != nil {
return err return err
@ -657,14 +679,7 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
} }
encryptedExtensions := new(encryptedExtensionsMsg) encryptedExtensions := new(encryptedExtensionsMsg)
encryptedExtensions.alpnProtocol = c.clientProtocol
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
if c.quic != nil { if c.quic != nil {
p, err := c.quicGetTransportParameters() p, err := c.quicGetTransportParameters()
@ -672,6 +687,7 @@ func (hs *serverHandshakeStateTLS13) sendServerParameters() error {
return err return err
} }
encryptedExtensions.quicTransportParameters = p encryptedExtensions.quicTransportParameters = p
encryptedExtensions.earlyData = hs.earlyData
} }
if _, err := hs.c.writeHandshakeRecord(encryptedExtensions, hs.transcript); err != nil { if _, err := hs.c.writeHandshakeRecord(encryptedExtensions, hs.transcript); err != nil {
@ -812,6 +828,11 @@ func (hs *serverHandshakeStateTLS13) shouldSendSessionTickets() bool {
return false 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. // Don't send tickets the client wouldn't use. See RFC 8446, Section 4.2.9.
for _, pskMode := range hs.clientHello.pskModes { for _, pskMode := range hs.clientHello.pskModes {
if pskMode == pskModeDHE { if pskMode == pskModeDHE {
@ -832,16 +853,24 @@ func (hs *serverHandshakeStateTLS13) sendSessionTickets() error {
return err return err
} }
c.resumptionSecret = hs.suite.deriveSecret(hs.masterSecret,
resumptionLabel, hs.transcript)
if !hs.shouldSendSessionTickets() { if !hs.shouldSendSessionTickets() {
return nil return nil
} }
return c.sendSessionTicket(false)
}
resumptionSecret := hs.suite.deriveSecret(hs.masterSecret, func (c *Conn) sendSessionTicket(earlyData bool) error {
resumptionLabel, hs.transcript) 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 // ticket_nonce, which must be unique per connection, is always left at
// zero because we only ever send one ticket per connection. // zero because we only ever send one ticket per connection.
psk := hs.suite.expandLabel(resumptionSecret, "resumption", psk := suite.expandLabel(c.resumptionSecret, "resumption",
nil, hs.suite.hash.Size()) nil, suite.hash.Size())
m := new(newSessionTicketMsgTLS13) m := new(newSessionTicketMsgTLS13)
@ -850,6 +879,7 @@ func (hs *serverHandshakeStateTLS13) sendSessionTickets() error {
return err return err
} }
state.secret = psk state.secret = psk
state.EarlyData = earlyData
if c.config.WrapSession != nil { if c.config.WrapSession != nil {
m.label, err = c.config.WrapSession(c.connectionStateLocked(), state) m.label, err = c.config.WrapSession(c.connectionStateLocked(), state)
if err != nil { 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 // The value is not stored anywhere; we never need to check the ticket age
// because 0-RTT is not supported. // because 0-RTT is not supported.
ageAdd := make([]byte, 4) ageAdd := make([]byte, 4)
_, err = hs.c.config.rand().Read(ageAdd) _, err = c.config.rand().Read(ageAdd)
if err != nil { if err != nil {
return err return err
} }
m.ageAdd = binary.LittleEndian.Uint32(ageAdd) 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 { if _, err := c.writeHandshakeRecord(m, nil); err != nil {
return err return err
} }

View File

@ -21,6 +21,7 @@ import (
const ( const (
resumptionBinderLabel = "res binder" resumptionBinderLabel = "res binder"
clientEarlyTrafficLabel = "c e traffic"
clientHandshakeTrafficLabel = "c hs traffic" clientHandshakeTrafficLabel = "c hs traffic"
serverHandshakeTrafficLabel = "s hs traffic" serverHandshakeTrafficLabel = "s hs traffic"
clientApplicationTrafficLabel = "c ap traffic" clientApplicationTrafficLabel = "c ap traffic"

42
quic.go
View File

@ -16,6 +16,7 @@ type QUICEncryptionLevel int
const ( const (
QUICEncryptionLevelInitial = QUICEncryptionLevel(iota) QUICEncryptionLevelInitial = QUICEncryptionLevel(iota)
QUICEncryptionLevelEarly
QUICEncryptionLevelHandshake QUICEncryptionLevelHandshake
QUICEncryptionLevelApplication QUICEncryptionLevelApplication
) )
@ -24,6 +25,8 @@ func (l QUICEncryptionLevel) String() string {
switch l { switch l {
case QUICEncryptionLevelInitial: case QUICEncryptionLevelInitial:
return "Initial" return "Initial"
case QUICEncryptionLevelEarly:
return "Early"
case QUICEncryptionLevelHandshake: case QUICEncryptionLevelHandshake:
return "Handshake" return "Handshake"
case QUICEncryptionLevelApplication: case QUICEncryptionLevelApplication:
@ -39,6 +42,8 @@ func (l QUICEncryptionLevel) String() string {
// Methods of QUICConn are not safe for concurrent use. // Methods of QUICConn are not safe for concurrent use.
type QUICConn struct { type QUICConn struct {
conn *Conn conn *Conn
sessionTicketSent bool
} }
// A QUICConfig configures a QUICConn. // A QUICConfig configures a QUICConn.
@ -79,6 +84,11 @@ const (
// connection will never generate a QUICTransportParametersRequired event. // connection will never generate a QUICTransportParametersRequired event.
QUICTransportParametersRequired 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 indicates that the TLS handshake has completed.
QUICHandshakeDone QUICHandshakeDone
) )
@ -106,10 +116,10 @@ type quicState struct {
nextEvent int nextEvent int
// eventArr is a statically allocated event array, large enough to handle // eventArr is a statically allocated event array, large enough to handle
// the usual maximum number of events resulting from a single call: // the usual maximum number of events resulting from a single call: transport
// transport parameters, Initial data, Handshake write and read secrets, // parameters, Initial data, Early read secret, Handshake write and read
// Handshake data, Application write secret, Application data. // secrets, Handshake data, Application write secret, Application data.
eventArr [7]QUICEvent eventArr [8]QUICEvent
started bool started bool
signalc chan struct{} // handshake data is available to be read 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 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. // ConnectionState returns basic TLS details about the connection.
func (q *QUICConn) ConnectionState() ConnectionState { func (q *QUICConn) ConnectionState() ConnectionState {
return q.conn.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, // quicWaitForSignal notifies the QUICConn that handshake progress is blocked,
// and waits for a signal that the handshake should proceed. // and waits for a signal that the handshake should proceed.
// //

View File

@ -34,7 +34,12 @@ type SessionState struct {
// uint64 created_at; // uint64 created_at;
// opaque secret<1..2^8-1>; // opaque secret<1..2^8-1>;
// opaque extra<0..2^24-1>; // opaque extra<0..2^24-1>;
// uint8 early_data = { 0, 1 };
// CertificateEntry certificate_list<0..2^24-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) { // select (SessionState.type) {
// case server: /* empty */; // case server: /* empty */;
// case client: struct { // case client: struct {
@ -63,6 +68,11 @@ type SessionState struct {
// fixed-length suffix. // fixed-length suffix.
Extra []byte 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 version uint16
isClient bool isClient bool
cipherSuite uint16 cipherSuite uint16
@ -75,6 +85,7 @@ type SessionState struct {
activeCertHandles []*activeCert activeCertHandles []*activeCert
ocspResponse []byte ocspResponse []byte
scts [][]byte scts [][]byte
alpnProtocol string // only set if EarlyData is true
// Client-side fields. // Client-side fields.
verifiedChains [][]*x509.Certificate verifiedChains [][]*x509.Certificate
@ -106,7 +117,17 @@ func (s *SessionState) Bytes() ([]byte, error) {
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(s.Extra) b.AddBytes(s.Extra)
}) })
if s.EarlyData {
b.AddUint8(1)
} else {
b.AddUint8(0)
}
marshalCertificate(&b, s.certificate()) marshalCertificate(&b, s.certificate())
if s.EarlyData {
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes([]byte(s.alpnProtocol))
})
}
if s.isClient { if s.isClient {
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
for _, chain := range s.verifiedChains { for _, chain := range s.verifiedChains {
@ -152,7 +173,7 @@ func certificatesToBytesSlice(certs []*x509.Certificate) [][]byte {
func ParseSessionState(data []byte) (*SessionState, error) { func ParseSessionState(data []byte) (*SessionState, error) {
ss := &SessionState{} ss := &SessionState{}
s := cryptobyte.String(data) s := cryptobyte.String(data)
var typ uint8 var typ, earlyData uint8
var cert Certificate var cert Certificate
if !s.ReadUint16(&ss.version) || if !s.ReadUint16(&ss.version) ||
!s.ReadUint8(&typ) || !s.ReadUint8(&typ) ||
@ -161,10 +182,19 @@ func ParseSessionState(data []byte) (*SessionState, error) {
!readUint64(&s, &ss.createdAt) || !readUint64(&s, &ss.createdAt) ||
!readUint8LengthPrefixed(&s, &ss.secret) || !readUint8LengthPrefixed(&s, &ss.secret) ||
!readUint24LengthPrefixed(&s, &ss.Extra) || !readUint24LengthPrefixed(&s, &ss.Extra) ||
!s.ReadUint8(&earlyData) ||
len(ss.secret) == 0 || len(ss.secret) == 0 ||
!unmarshalCertificate(&s, &cert) { !unmarshalCertificate(&s, &cert) {
return nil, errors.New("tls: invalid session encoding") 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 { for _, cert := range cert.Certificate {
c, err := globalCertCache.newCert(cert) c, err := globalCertCache.newCert(cert)
if err != nil { if err != nil {
@ -175,6 +205,13 @@ func ParseSessionState(data []byte) (*SessionState, error) {
} }
ss.ocspResponse = cert.OCSPStaple ss.ocspResponse = cert.OCSPStaple
ss.scts = cert.SignedCertificateTimestamps 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 isClient := typ == 2; !isClient {
if !s.Empty() { if !s.Empty() {
return nil, errors.New("tls: invalid session encoding") return nil, errors.New("tls: invalid session encoding")
@ -236,6 +273,7 @@ func (c *Conn) sessionState() (*SessionState, error) {
version: c.vers, version: c.vers,
cipherSuite: c.cipherSuite, cipherSuite: c.cipherSuite,
createdAt: uint64(c.config.time().Unix()), createdAt: uint64(c.config.time().Unix()),
alpnProtocol: c.clientProtocol,
peerCertificates: c.peerCertificates, peerCertificates: c.peerCertificates,
activeCertHandles: c.activeCertHandles, activeCertHandles: c.activeCertHandles,
ocspResponse: c.ocspResponse, ocspResponse: c.ocspResponse,