From 0295761c3b043e0d51bcd42b2e3d449e7d0ee61a Mon Sep 17 00:00:00 2001 From: Christopher Wood Date: Fri, 15 Jul 2022 07:08:03 -0700 Subject: [PATCH] Add hybrid post-quantum key agreement. Adds Kyber512X25519 and Kyber768X25519 hybrid post-quantum key agreements with temporary group identifiers. Not enabled by default. Adds CFEvents to detect `HelloRetryRequest`s and to signal which key agreement was used. Co-authored-by: Bas Westerbaan --- src/circl/kem/hybrid/hybrid.go | 11 +- src/circl/kem/schemes/schemes.go | 1 + src/circl/kem/schemes/schemes_test.go | 1 + src/crypto/tls/cfkem.go | 102 +++++++++++++++++++ src/crypto/tls/cfkem_test.go | 124 +++++++++++++++++++++++ src/crypto/tls/handshake_client.go | 57 +++++++---- src/crypto/tls/handshake_client_tls13.go | 87 +++++++++++----- src/crypto/tls/handshake_server_tls13.go | 30 ++++-- src/crypto/tls/key_agreement.go | 2 +- src/crypto/tls/tls_cf.go | 17 ++++ 10 files changed, 377 insertions(+), 55 deletions(-) create mode 100644 src/crypto/tls/cfkem.go create mode 100644 src/crypto/tls/cfkem_test.go diff --git a/src/circl/kem/hybrid/hybrid.go b/src/circl/kem/hybrid/hybrid.go index 44e312747d7..fd422bdf907 100644 --- a/src/circl/kem/hybrid/hybrid.go +++ b/src/circl/kem/hybrid/hybrid.go @@ -46,8 +46,11 @@ var ErrUninitialized = errors.New("public or private key not initialized") // Returns the hybrid KEM of Kyber512 and X25519. func Kyber512X25519() kem.Scheme { return kyber512X } +// Returns the hybrid KEM of Kyber768 and X25519. +func Kyber768X25519() kem.Scheme { return kyber768X } + // Returns the hybrid KEM of Kyber768 and X448. -func Kyber768X448() kem.Scheme { return kyber768X } +func Kyber768X448() kem.Scheme { return kyber768X4 } // Returns the hybrid KEM of Kyber1024 and X448. func Kyber1024X448() kem.Scheme { return kyber1024X } @@ -59,6 +62,12 @@ var kyber512X kem.Scheme = &scheme{ } var kyber768X kem.Scheme = &scheme{ + "Kyber768-X25519", + kyber768.Scheme(), + hpke.KEM_X25519_HKDF_SHA256.Scheme(), +} + +var kyber768X4 kem.Scheme = &scheme{ "Kyber768-X448", kyber768.Scheme(), hpke.KEM_X448_HKDF_SHA512.Scheme(), diff --git a/src/circl/kem/schemes/schemes.go b/src/circl/kem/schemes/schemes.go index 59b5d8906fa..6872d9c7137 100644 --- a/src/circl/kem/schemes/schemes.go +++ b/src/circl/kem/schemes/schemes.go @@ -41,6 +41,7 @@ var allSchemes = [...]kem.Scheme{ sikep503.Scheme(), sikep751.Scheme(), hybrid.Kyber512X25519(), + hybrid.Kyber768X25519(), hybrid.Kyber768X448(), hybrid.Kyber1024X448(), } diff --git a/src/circl/kem/schemes/schemes_test.go b/src/circl/kem/schemes/schemes_test.go index 78210549e12..36333a96f64 100644 --- a/src/circl/kem/schemes/schemes_test.go +++ b/src/circl/kem/schemes/schemes_test.go @@ -159,6 +159,7 @@ func Example_schemes() { // SIKEp503 // SIKEp751 // Kyber512-X25519 + // Kyber768-X25519 // Kyber768-X448 // Kyber1024-X448 } diff --git a/src/crypto/tls/cfkem.go b/src/crypto/tls/cfkem.go new file mode 100644 index 00000000000..2cffc932b74 --- /dev/null +++ b/src/crypto/tls/cfkem.go @@ -0,0 +1,102 @@ +// Copyright 2022 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. +// +// Glue to add Circl's (post-quantum) hybrid KEMs. +// +// To enable set CurvePreferences with the desired scheme as the first element: +// +// import ( +// "github.com/cloudflare/circl/kem/tls" +// "github.com/cloudflare/circl/kem/hybrid" +// +// [...] +// +// config.CurvePreferences = []tls.CurveID{ +// hybrid.Kyber512X25519().(tls.TLSScheme).TLSCurveID(), +// tls.X25519, +// tls.P256, +// } + +package tls + +import ( + "fmt" + "io" + + "circl/kem" + "circl/kem/hybrid" +) + +// Either ecdheParameters or kem.PrivateKey +type clientKeySharePrivate interface{} + +var ( + kyber512X25519CurveID = CurveID(0xfe30) + kyber768X25519CurveID = CurveID(0xfe31) + invalidCurveID = CurveID(0) +) + +func kemSchemeKeyToCurveID(s kem.Scheme) CurveID { + switch s.Name() { + case "Kyber512-X25519": + return kyber512X25519CurveID + case "Kyber768-X25519": + return kyber768X25519CurveID + default: + return invalidCurveID + } +} + +// Extract CurveID from clientKeySharePrivate +func clientKeySharePrivateCurveID(ks clientKeySharePrivate) CurveID { + switch v := ks.(type) { + case kem.PrivateKey: + ret := kemSchemeKeyToCurveID(v.Scheme()) + if ret == invalidCurveID { + panic("cfkem: internal error: don't know CurveID for this KEM") + } + return ret + case ecdheParameters: + return v.CurveID() + default: + panic("cfkem: internal error: unknown clientKeySharePrivate") + } +} + +// Returns scheme by CurveID if supported by Circl +func curveIdToCirclScheme(id CurveID) kem.Scheme { + switch id { + case kyber512X25519CurveID: + return hybrid.Kyber512X25519() + case kyber768X25519CurveID: + return hybrid.Kyber768X25519() + } + return nil +} + +// Generate a new shared secret and encapsulates it for the packed +// public key in ppk using randomness from rnd. +func encapsulateForKem(scheme kem.Scheme, rnd io.Reader, ppk []byte) ( + ct, ss []byte, alert alert, err error) { + pk, err := scheme.UnmarshalBinaryPublicKey(ppk) + if err != nil { + return nil, nil, alertIllegalParameter, fmt.Errorf("unpack pk: %w", err) + } + seed := make([]byte, scheme.EncapsulationSeedSize()) + if _, err := io.ReadFull(rnd, seed); err != nil { + return nil, nil, alertInternalError, fmt.Errorf("random: %w", err) + } + ct, ss, err = scheme.EncapsulateDeterministically(pk, seed) + return ct, ss, alertIllegalParameter, err +} + +// Generate a new keypair using randomness from rnd. +func generateKemKeyPair(scheme kem.Scheme, rnd io.Reader) ( + kem.PublicKey, kem.PrivateKey, error) { + seed := make([]byte, scheme.SeedSize()) + if _, err := io.ReadFull(rnd, seed); err != nil { + return nil, nil, err + } + pk, sk := scheme.DeriveKeyPair(seed) + return pk, sk, nil +} diff --git a/src/crypto/tls/cfkem_test.go b/src/crypto/tls/cfkem_test.go new file mode 100644 index 00000000000..4416143039b --- /dev/null +++ b/src/crypto/tls/cfkem_test.go @@ -0,0 +1,124 @@ +// Copyright 2022 Cloudflare, Inc. All rights reserved. Use of this source code +// is governed by a BSD-style license that can be found in the LICENSE file. + +package tls + +import ( + "fmt" + "testing" + + "circl/kem" + "circl/kem/hybrid" +) + +func testHybridKEX(t *testing.T, scheme kem.Scheme, clientPQ, serverPQ, + clientTLS12, serverTLS12 bool) { + var clientSelectedKEX *CurveID + var retry bool + + rsaCert := Certificate{ + Certificate: [][]byte{testRSACertificate}, + PrivateKey: testRSAPrivateKey, + } + serverCerts := []Certificate{rsaCert} + + clientConfig := testConfig.Clone() + if clientPQ { + clientConfig.CurvePreferences = []CurveID{ + kemSchemeKeyToCurveID(scheme), + X25519, + } + } + clientConfig.CFEventHandler = func(ev CFEvent) { + switch e := ev.(type) { + case CFEventTLS13NegotiatedKEX: + clientSelectedKEX = &e.KEX + case CFEventTLS13HRR: + retry = true + } + } + if clientTLS12 { + clientConfig.MaxVersion = VersionTLS12 + } + + serverConfig := testConfig.Clone() + if serverPQ { + serverConfig.CurvePreferences = []CurveID{ + kemSchemeKeyToCurveID(scheme), + X25519, + } + } + if serverTLS12 { + serverConfig.MaxVersion = VersionTLS12 + } + serverConfig.Certificates = serverCerts + + c, s := localPipe(t) + done := make(chan error) + defer c.Close() + + go func() { + defer s.Close() + done <- Server(s, serverConfig).Handshake() + }() + + cli := Client(c, clientConfig) + clientErr := cli.Handshake() + serverErr := <-done + if clientErr != nil { + t.Errorf("client error: %s", clientErr) + } + if serverErr != nil { + t.Errorf("server error: %s", serverErr) + } + + var expectedKEX CurveID + var expectedRetry bool + + if clientPQ && serverPQ { + expectedKEX = kemSchemeKeyToCurveID(scheme) + } else { + expectedKEX = X25519 + } + if clientPQ && !serverPQ { + expectedRetry = true + } + + if !serverTLS12 && !clientTLS12 { + if clientSelectedKEX == nil { + t.Error("No TLS 1.3 KEX happened?") + } + + if *clientSelectedKEX != expectedKEX { + t.Errorf("failed to negotiate: expected %d, got %d", + expectedKEX, *clientSelectedKEX) + } + if expectedRetry != retry { + t.Errorf("Expected retry=%v, got retry=%v", expectedRetry, retry) + } + } else { + if clientSelectedKEX != nil { + t.Error("TLS 1.3 KEX happened?") + } + } +} + +func TestHybridKEX(t *testing.T) { + run := func(scheme kem.Scheme, clientPQ, serverPQ, clientTLS12, serverTLS12 bool) { + t.Run(fmt.Sprintf("%s serverPQ:%v clientPQ:%v serverTLS12:%v clientTLS12:%v", scheme.Name(), + serverPQ, clientPQ, serverTLS12, clientTLS12), func(t *testing.T) { + testHybridKEX(t, scheme, clientPQ, serverPQ, clientTLS12, serverTLS12) + }) + } + for _, scheme := range []kem.Scheme{ + hybrid.Kyber512X25519(), + hybrid.Kyber768X25519(), + } { + run(scheme, true, true, false, false) + run(scheme, true, false, false, false) + run(scheme, false, true, false, false) + run(scheme, true, true, true, false) + run(scheme, true, true, false, true) + run(scheme, true, true, true, true) + } +} diff --git a/src/crypto/tls/handshake_client.go b/src/crypto/tls/handshake_client.go index b5b4ce9195b..55e4cf93226 100644 --- a/src/crypto/tls/handshake_client.go +++ b/src/crypto/tls/handshake_client.go @@ -36,7 +36,7 @@ type clientHandshakeState struct { session *ClientSessionState } -func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, ecdheParameters, error) { +func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, clientKeySharePrivate, error) { config := c.config if len(config.ServerName) == 0 && !config.InsecureSkipVerify { return nil, nil, errors.New("tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config") @@ -122,7 +122,7 @@ func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, ecdheParamet hello.supportedSignatureAlgorithms = config.supportedSignatureAlgorithms() } - var params ecdheParameters + var secret clientKeySharePrivate if hello.supportedVersions[0] == VersionTLS13 { if hasAESGCMHardwareSupport { hello.cipherSuites = append(hello.cipherSuites, defaultCipherSuitesTLS13...) @@ -131,19 +131,36 @@ func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, ecdheParamet } curveID := config.curvePreferences()[0] - if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok { - return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") - } - params, err = generateECDHEParameters(config.rand(), curveID) - if err != nil { - return nil, nil, err + if scheme := curveIdToCirclScheme(curveID); scheme != nil { + pk, sk, err := generateKemKeyPair(scheme, config.rand()) + if err != nil { + return nil, nil, fmt.Errorf("generateKemKeyPair %s: %w", + scheme.Name(), err) + } + packedPk, err := pk.MarshalBinary() + if err != nil { + return nil, nil, fmt.Errorf("pack circl public key %s: %w", + scheme.Name(), err) + } + hello.keyShares = []keyShare{{group: curveID, data: packedPk}} + secret = sk + } else { + if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok { + return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve") + } + params, err := generateECDHEParameters(config.rand(), curveID) + if err != nil { + return nil, nil, err + } + hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}} + secret = params } - hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}} + hello.delegatedCredentialSupported = config.SupportDelegatedCredential hello.supportedSignatureAlgorithmsDC = supportedSignatureAlgorithmsDC } - return hello, params, nil + return hello, secret, nil } func (c *Conn) clientHandshake(ctx context.Context) (err error) { @@ -230,16 +247,16 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) { if c.vers == VersionTLS13 { hs := &clientHandshakeStateTLS13{ - c: c, - ctx: ctx, - serverHello: serverHello, - hello: hello, - helloInner: helloInner, - ecdheParams: ecdheParams, - session: session, - earlySecret: earlySecret, - binderKey: binderKey, - hsTimings: hsTimings, + c: c, + ctx: ctx, + serverHello: serverHello, + hello: hello, + helloInner: helloInner, + keySharePrivate: ecdheParams, + session: session, + earlySecret: earlySecret, + binderKey: binderKey, + hsTimings: hsTimings, } // In TLS 1.3, session tickets are delivered after the handshake. diff --git a/src/crypto/tls/handshake_client_tls13.go b/src/crypto/tls/handshake_client_tls13.go index 6dfe75a9580..16c0e1743ca 100644 --- a/src/crypto/tls/handshake_client_tls13.go +++ b/src/crypto/tls/handshake_client_tls13.go @@ -16,19 +16,22 @@ import ( "hash" "sync/atomic" "time" + + circlKem "circl/kem" ) type clientHandshakeStateTLS13 struct { - c *Conn - ctx context.Context - serverHello *serverHelloMsg - hello *clientHelloMsg - helloInner *clientHelloMsg - ecdheParams ecdheParameters - - session *ClientSessionState - earlySecret []byte - binderKey []byte + c *Conn + ctx context.Context + serverHello *serverHelloMsg + hello *clientHelloMsg + helloInner *clientHelloMsg + keySharePrivate clientKeySharePrivate + + session *ClientSessionState + earlySecret []byte + binderKey []byte + selectedGroup CurveID certReq *certificateRequestMsgTLS13 usingPSK bool @@ -94,7 +97,7 @@ func (hs *clientHandshakeStateTLS13) handshake() error { } // Consistency check on the presence of a keyShare and its parameters. - if hs.ecdheParams == nil || len(hs.hello.keyShares) != 1 { + if hs.keySharePrivate == nil || len(hs.hello.keyShares) != 1 { return c.sendAlert(alertInternalError) } @@ -266,6 +269,8 @@ func (hs *clientHandshakeStateTLS13) sendDummyChangeCipherSpec() error { func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { c := hs.c + c.handleCFEvent(CFEventTLS13HRR{}) + // The first ClientHello gets double-hashed into the transcript upon a // HelloRetryRequest. (The idea is that the server might offload transcript // storage to the client in the cookie.) See RFC 8446, Section 4.4.1. @@ -347,21 +352,38 @@ func (hs *clientHandshakeStateTLS13) processHelloRetryRequest() error { c.sendAlert(alertIllegalParameter) return errors.New("tls: server selected unsupported group") } - if hs.ecdheParams.CurveID() == curveID { + if clientKeySharePrivateCurveID(hs.keySharePrivate) == curveID { c.sendAlert(alertIllegalParameter) return errors.New("tls: server sent an unnecessary HelloRetryRequest key_share") } - if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok { - c.sendAlert(alertInternalError) - return errors.New("tls: CurvePreferences includes unsupported curve") - } - params, err := generateECDHEParameters(c.config.rand(), curveID) - if err != nil { - c.sendAlert(alertInternalError) - return err + if scheme := curveIdToCirclScheme(curveID); scheme != nil { + pk, sk, err := generateKemKeyPair(scheme, c.config.rand()) + if err != nil { + c.sendAlert(alertInternalError) + return fmt.Errorf("HRR generateKemKeyPair %s: %w", + scheme.Name(), err) + } + packedPk, err := pk.MarshalBinary() + if err != nil { + c.sendAlert(alertInternalError) + return fmt.Errorf("HRR pack circl public key %s: %w", + scheme.Name(), err) + } + hs.keySharePrivate = sk + hello.keyShares = []keyShare{{group: curveID, data: packedPk}} + } else { + if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok { + c.sendAlert(alertInternalError) + return errors.New("tls: CurvePreferences includes unsupported curve") + } + params, err := generateECDHEParameters(c.config.rand(), curveID) + if err != nil { + c.sendAlert(alertInternalError) + return err + } + hs.keySharePrivate = params + hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}} } - hs.ecdheParams = params - hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}} } hello.raw = nil @@ -487,11 +509,15 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error { c.sendAlert(alertIllegalParameter) return errors.New("tls: server did not send a key share") } - if hs.serverHello.serverShare.group != hs.ecdheParams.CurveID() { + if hs.serverHello.serverShare.group != clientKeySharePrivateCurveID(hs.keySharePrivate) { c.sendAlert(alertIllegalParameter) return errors.New("tls: server selected unsupported group") } + c.handleCFEvent(CFEventTLS13NegotiatedKEX{ + KEX: hs.serverHello.serverShare.group, + }) + if !hs.serverHello.selectedIdentityPresent { return nil } @@ -535,10 +561,19 @@ func (hs *clientHandshakeStateTLS13) processServerHello() error { func (hs *clientHandshakeStateTLS13) establishHandshakeKeys() error { c := hs.c - sharedKey := hs.ecdheParams.SharedKey(hs.serverHello.serverShare.data) - if sharedKey == nil { + var sharedKey []byte + if params, ok := hs.keySharePrivate.(ecdheParameters); ok { + sharedKey = params.SharedKey(hs.serverHello.serverShare.data) + } else if sk, ok := hs.keySharePrivate.(circlKem.PrivateKey); ok { + var err error + sharedKey, err = sk.Scheme().Decapsulate(sk, hs.serverHello.serverShare.data) + if err != nil { + c.sendAlert(alertIllegalParameter) + return fmt.Errorf("%s decaps: %w", sk.Scheme().Name(), err) + } + } else { c.sendAlert(alertIllegalParameter) - return errors.New("tls: invalid server key share") + return fmt.Errorf("tls: invalid server key share") } earlySecret := hs.earlySecret diff --git a/src/crypto/tls/handshake_server_tls13.go b/src/crypto/tls/handshake_server_tls13.go index 926ef76726e..8dc9ddae101 100644 --- a/src/crypto/tls/handshake_server_tls13.go +++ b/src/crypto/tls/handshake_server_tls13.go @@ -33,6 +33,7 @@ type serverHandshakeStateTLS13 struct { suite *cipherSuiteTLS13 cert *Certificate sigAlg SignatureScheme + selectedGroup CurveID earlySecret []byte sharedKey []byte handshakeSecret []byte @@ -279,23 +280,36 @@ GroupSelection: clientKeyShare = &hs.clientHello.keyShares[0] } - if _, ok := curveForCurveID(selectedGroup); selectedGroup != X25519 && !ok { + if _, ok := curveForCurveID(selectedGroup); selectedGroup != X25519 && curveIdToCirclScheme(selectedGroup) == nil && !ok { c.sendAlert(alertInternalError) return errors.New("tls: CurvePreferences includes unsupported curve") } - params, err := generateECDHEParameters(c.config.rand(), selectedGroup) - if err != nil { - c.sendAlert(alertInternalError) - return err + if kem := curveIdToCirclScheme(selectedGroup); kem != nil { + ct, ss, alert, err := encapsulateForKem(kem, c.config.rand(), clientKeyShare.data) + if err != nil { + c.sendAlert(alert) + return fmt.Errorf("%s encap: %w", kem.Name(), err) + } + hs.hello.serverShare = keyShare{group: selectedGroup, data: ct} + hs.sharedKey = ss + } else { + params, err := generateECDHEParameters(c.config.rand(), selectedGroup) + if err != nil { + c.sendAlert(alertInternalError) + return err + } + hs.hello.serverShare = keyShare{group: selectedGroup, data: params.PublicKey()} + hs.sharedKey = params.SharedKey(clientKeyShare.data) } - hs.hello.serverShare = keyShare{group: selectedGroup, data: params.PublicKey()} - hs.sharedKey = params.SharedKey(clientKeyShare.data) if hs.sharedKey == nil { c.sendAlert(alertIllegalParameter) return errors.New("tls: invalid client key share") } c.serverName = hs.clientHello.serverName + c.handleCFEvent(CFEventTLS13NegotiatedKEX{ + KEX: selectedGroup, + }) hs.hsTimings.ProcessClientHello = hs.hsTimings.elapsedTime() @@ -535,6 +549,8 @@ func (hs *serverHandshakeStateTLS13) sendDummyChangeCipherSpec() error { func (hs *serverHandshakeStateTLS13) doHelloRetryRequest(selectedGroup CurveID) error { c := hs.c + c.handleCFEvent(CFEventTLS13HRR{}) + // The first ClientHello gets double-hashed into the transcript upon a // HelloRetryRequest. See RFC 8446, Section 4.4.1. hs.transcript.Write(hs.clientHello.marshal()) diff --git a/src/crypto/tls/key_agreement.go b/src/crypto/tls/key_agreement.go index 630e3df5ae0..85789c9736a 100644 --- a/src/crypto/tls/key_agreement.go +++ b/src/crypto/tls/key_agreement.go @@ -168,7 +168,7 @@ type ecdheKeyAgreement struct { func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) { var curveID CurveID for _, c := range clientHello.supportedCurves { - if config.supportsCurve(c) { + if config.supportsCurve(c) && curveIdToCirclScheme(c) == nil { curveID = c break } diff --git a/src/crypto/tls/tls_cf.go b/src/crypto/tls/tls_cf.go index b7e36dc8e1e..688a87bf150 100644 --- a/src/crypto/tls/tls_cf.go +++ b/src/crypto/tls/tls_cf.go @@ -218,3 +218,20 @@ type CFEventECHPublicNameMismatch struct{} func (e CFEventECHPublicNameMismatch) Name() string { return "ech public name does not match outer sni" } + +// CFEventTLS13NegotiatedKEX is emitted when a key agreement mechanism has been +// established. +type CFEventTLS13NegotiatedKEX struct { + KEX CurveID +} + +func (e CFEventTLS13NegotiatedKEX) Name() string { + return "CFEventTLS13NegotiatedKEX" +} + +// CFEventTLS13HRR is emitted when a HRR is sent or received +type CFEventTLS13HRR struct{} + +func (e CFEventTLS13HRR) Name() string { + return "CFEventTLS13HRR" +}