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:
parent
e6eabfc665
commit
aa68126eeb
2
conn.go
2
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
|
||||
|
@ -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{
|
||||
|
@ -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 != "" {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
42
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.
|
||||
//
|
||||
|
40
ticket.go
40
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,
|
||||
|
Loading…
Reference in New Issue
Block a user