0
0
mirror of https://github.com/XTLS/REALITY.git synced 2025-08-22 14:38:35 +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 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

View File

@ -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{

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 {
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 != "" {

View File

@ -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

View File

@ -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
}

View File

@ -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"

42
quic.go
View File

@ -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.
//

View File

@ -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,