Skip to content

Commit 902e08e

Browse files
committed
Add hybrid post-quantum key agreement.
Adds X25519Kyber512Draft00 and X25519Kyber768Draft00 hybrid post-quantum key agreements with temporary group identifiers. The hybrid post-quantum key exchanges uses plain X{25519,448} instead of HPKE, which we assume will be more likely to be adopted. The order is chosen to match CECPQ2. Not enabled by default. Adds CFEvents to detect `HelloRetryRequest`s and to signal which key agreement was used. Cf #121 #122 #123 #132 Co-authored-by: Christopher Wood <[email protected]> [ bas, 1.20.1: also adds P256Kyber768Draft00 ]
1 parent 84905d1 commit 902e08e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+11098
-65
lines changed

src/crypto/tls/cfkem.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2022 Cloudflare, Inc. All rights reserved. Use of this source code
2+
// is governed by a BSD-style license that can be found in the LICENSE file.
3+
//
4+
// Glue to add Circl's (post-quantum) hybrid KEMs.
5+
//
6+
// To enable set CurvePreferences with the desired scheme as the first element:
7+
//
8+
// import (
9+
// "github.com/cloudflare/circl/kem/tls"
10+
// "github.com/cloudflare/circl/kem/hybrid"
11+
//
12+
// [...]
13+
//
14+
// config.CurvePreferences = []tls.CurveID{
15+
// hybrid.X25519Kyber512Draft00().(tls.TLSScheme).TLSCurveID(),
16+
// tls.X25519,
17+
// tls.P256,
18+
// }
19+
20+
package tls
21+
22+
import (
23+
"fmt"
24+
"io"
25+
26+
"crypto/ecdh"
27+
28+
"github.com/cloudflare/circl/kem"
29+
"github.com/cloudflare/circl/kem/hybrid"
30+
)
31+
32+
// Either ecdheParameters or kem.PrivateKey
33+
type clientKeySharePrivate interface{}
34+
35+
var (
36+
X25519Kyber512Draft00 = CurveID(0xfe30)
37+
X25519Kyber768Draft00 = CurveID(0xfe31)
38+
P256Kyber768Draft00 = CurveID(0xfe32)
39+
invalidCurveID = CurveID(0)
40+
)
41+
42+
func kemSchemeKeyToCurveID(s kem.Scheme) CurveID {
43+
switch s.Name() {
44+
case "Kyber512-X25519":
45+
return X25519Kyber512Draft00
46+
case "Kyber768-X25519":
47+
return X25519Kyber768Draft00
48+
case "P256Kyber768Draft00":
49+
return P256Kyber768Draft00
50+
default:
51+
return invalidCurveID
52+
}
53+
}
54+
55+
// Extract CurveID from clientKeySharePrivate
56+
func clientKeySharePrivateCurveID(ks clientKeySharePrivate) CurveID {
57+
switch v := ks.(type) {
58+
case kem.PrivateKey:
59+
ret := kemSchemeKeyToCurveID(v.Scheme())
60+
if ret == invalidCurveID {
61+
panic("cfkem: internal error: don't know CurveID for this KEM")
62+
}
63+
return ret
64+
case *ecdh.PrivateKey:
65+
ret, ok := curveIDForCurve(v.Curve())
66+
if !ok {
67+
panic("cfkem: internal error: unknown curve")
68+
}
69+
return ret
70+
default:
71+
panic("cfkem: internal error: unknown clientKeySharePrivate")
72+
}
73+
}
74+
75+
// Returns scheme by CurveID if supported by Circl
76+
func curveIdToCirclScheme(id CurveID) kem.Scheme {
77+
switch id {
78+
case X25519Kyber512Draft00:
79+
return hybrid.Kyber512X25519()
80+
case X25519Kyber768Draft00:
81+
return hybrid.Kyber768X25519()
82+
case P256Kyber768Draft00:
83+
return hybrid.P256Kyber768Draft00()
84+
}
85+
return nil
86+
}
87+
88+
// Generate a new shared secret and encapsulates it for the packed
89+
// public key in ppk using randomness from rnd.
90+
func encapsulateForKem(scheme kem.Scheme, rnd io.Reader, ppk []byte) (
91+
ct, ss []byte, alert alert, err error) {
92+
pk, err := scheme.UnmarshalBinaryPublicKey(ppk)
93+
if err != nil {
94+
return nil, nil, alertIllegalParameter, fmt.Errorf("unpack pk: %w", err)
95+
}
96+
seed := make([]byte, scheme.EncapsulationSeedSize())
97+
if _, err := io.ReadFull(rnd, seed); err != nil {
98+
return nil, nil, alertInternalError, fmt.Errorf("random: %w", err)
99+
}
100+
ct, ss, err = scheme.EncapsulateDeterministically(pk, seed)
101+
return ct, ss, alertIllegalParameter, err
102+
}
103+
104+
// Generate a new keypair using randomness from rnd.
105+
func generateKemKeyPair(scheme kem.Scheme, rnd io.Reader) (
106+
kem.PublicKey, kem.PrivateKey, error) {
107+
seed := make([]byte, scheme.SeedSize())
108+
if _, err := io.ReadFull(rnd, seed); err != nil {
109+
return nil, nil, err
110+
}
111+
pk, sk := scheme.DeriveKeyPair(seed)
112+
return pk, sk, nil
113+
}

src/crypto/tls/cfkem_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2022 Cloudflare, Inc. All rights reserved. Use of this source code
2+
// is governed by a BSD-style license that can be found in the LICENSE file.
3+
4+
package tls
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/cloudflare/circl/kem"
11+
"github.com/cloudflare/circl/kem/hybrid"
12+
)
13+
14+
func testHybridKEX(t *testing.T, scheme kem.Scheme, clientPQ, serverPQ,
15+
clientTLS12, serverTLS12 bool) {
16+
var clientSelectedKEX *CurveID
17+
var retry bool
18+
19+
rsaCert := Certificate{
20+
Certificate: [][]byte{testRSACertificate},
21+
PrivateKey: testRSAPrivateKey,
22+
}
23+
serverCerts := []Certificate{rsaCert}
24+
25+
clientConfig := testConfig.Clone()
26+
if clientPQ {
27+
clientConfig.CurvePreferences = []CurveID{
28+
kemSchemeKeyToCurveID(scheme),
29+
X25519,
30+
}
31+
}
32+
clientConfig.CFEventHandler = func(ev CFEvent) {
33+
switch e := ev.(type) {
34+
case CFEventTLSNegotiatedNamedKEX:
35+
clientSelectedKEX = &e.KEX
36+
case CFEventTLS13HRR:
37+
retry = true
38+
}
39+
}
40+
if clientTLS12 {
41+
clientConfig.MaxVersion = VersionTLS12
42+
}
43+
44+
serverConfig := testConfig.Clone()
45+
if serverPQ {
46+
serverConfig.CurvePreferences = []CurveID{
47+
kemSchemeKeyToCurveID(scheme),
48+
X25519,
49+
}
50+
}
51+
if serverTLS12 {
52+
serverConfig.MaxVersion = VersionTLS12
53+
}
54+
serverConfig.Certificates = serverCerts
55+
56+
c, s := localPipe(t)
57+
done := make(chan error)
58+
defer c.Close()
59+
60+
go func() {
61+
defer s.Close()
62+
done <- Server(s, serverConfig).Handshake()
63+
}()
64+
65+
cli := Client(c, clientConfig)
66+
clientErr := cli.Handshake()
67+
serverErr := <-done
68+
if clientErr != nil {
69+
t.Errorf("client error: %s", clientErr)
70+
}
71+
if serverErr != nil {
72+
t.Errorf("server error: %s", serverErr)
73+
}
74+
75+
var expectedKEX CurveID
76+
var expectedRetry bool
77+
78+
if clientPQ && serverPQ && !clientTLS12 && !serverTLS12 {
79+
expectedKEX = kemSchemeKeyToCurveID(scheme)
80+
} else {
81+
expectedKEX = X25519
82+
}
83+
if !clientTLS12 && clientPQ && !serverPQ {
84+
expectedRetry = true
85+
}
86+
87+
if clientSelectedKEX == nil {
88+
t.Error("No KEX happened?")
89+
}
90+
91+
if *clientSelectedKEX != expectedKEX {
92+
t.Errorf("failed to negotiate: expected %d, got %d",
93+
expectedKEX, *clientSelectedKEX)
94+
}
95+
if expectedRetry != retry {
96+
t.Errorf("Expected retry=%v, got retry=%v", expectedRetry, retry)
97+
}
98+
}
99+
100+
func TestHybridKEX(t *testing.T) {
101+
run := func(scheme kem.Scheme, clientPQ, serverPQ, clientTLS12, serverTLS12 bool) {
102+
t.Run(fmt.Sprintf("%s serverPQ:%v clientPQ:%v serverTLS12:%v clientTLS12:%v", scheme.Name(),
103+
serverPQ, clientPQ, serverTLS12, clientTLS12), func(t *testing.T) {
104+
testHybridKEX(t, scheme, clientPQ, serverPQ, clientTLS12, serverTLS12)
105+
})
106+
}
107+
for _, scheme := range []kem.Scheme{
108+
hybrid.Kyber512X25519(),
109+
hybrid.Kyber768X25519(),
110+
hybrid.P256Kyber768Draft00(),
111+
} {
112+
run(scheme, true, true, false, false)
113+
run(scheme, true, false, false, false)
114+
run(scheme, false, true, false, false)
115+
run(scheme, true, true, true, false)
116+
run(scheme, true, true, false, true)
117+
run(scheme, true, true, true, true)
118+
}
119+
}

src/crypto/tls/handshake_client.go

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"bytes"
99
"context"
1010
"crypto"
11-
"crypto/ecdh"
1211
"crypto/ecdsa"
1312
"crypto/ed25519"
1413
"crypto/rsa"
@@ -38,7 +37,7 @@ type clientHandshakeState struct {
3837

3938
var testingOnlyForceClientHelloSignatureAlgorithms []SignatureScheme
4039

41-
func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, *ecdh.PrivateKey, error) {
40+
func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, clientKeySharePrivate, error) {
4241
config := c.config
4342
if len(config.ServerName) == 0 && !config.InsecureSkipVerify {
4443
return nil, nil, errors.New("tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config")
@@ -127,7 +126,7 @@ func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, *ecdh.Privat
127126
hello.supportedSignatureAlgorithms = testingOnlyForceClientHelloSignatureAlgorithms
128127
}
129128

130-
var key *ecdh.PrivateKey
129+
var secret clientKeySharePrivate
131130
if hello.supportedVersions[0] == VersionTLS13 {
132131
if hasAESGCMHardwareSupport {
133132
hello.cipherSuites = append(hello.cipherSuites, defaultCipherSuitesTLS13...)
@@ -136,19 +135,36 @@ func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, *ecdh.Privat
136135
}
137136

138137
curveID := config.curvePreferences()[0]
139-
if _, ok := curveForCurveID(curveID); !ok {
140-
return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
141-
}
142-
key, err = generateECDHEKey(config.rand(), curveID)
143-
if err != nil {
144-
return nil, nil, err
138+
if scheme := curveIdToCirclScheme(curveID); scheme != nil {
139+
pk, sk, err := generateKemKeyPair(scheme, config.rand())
140+
if err != nil {
141+
return nil, nil, fmt.Errorf("generateKemKeyPair %s: %w",
142+
scheme.Name(), err)
143+
}
144+
packedPk, err := pk.MarshalBinary()
145+
if err != nil {
146+
return nil, nil, fmt.Errorf("pack circl public key %s: %w",
147+
scheme.Name(), err)
148+
}
149+
hello.keyShares = []keyShare{{group: curveID, data: packedPk}}
150+
secret = sk
151+
} else {
152+
if _, ok := curveForCurveID(curveID); !ok {
153+
return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
154+
}
155+
key, err := generateECDHEKey(config.rand(), curveID)
156+
if err != nil {
157+
return nil, nil, err
158+
}
159+
hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}}
160+
secret = key
145161
}
146-
hello.keyShares = []keyShare{{group: curveID, data: key.PublicKey().Bytes()}}
162+
147163
hello.delegatedCredentialSupported = config.SupportDelegatedCredential
148164
hello.supportedSignatureAlgorithmsDC = supportedSignatureAlgorithmsDC
149165
}
150166

151-
return hello, key, nil
167+
return hello, secret, nil
152168
}
153169

154170
func (c *Conn) clientHandshake(ctx context.Context) (err error) {
@@ -239,16 +255,16 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
239255

240256
if c.vers == VersionTLS13 {
241257
hs := &clientHandshakeStateTLS13{
242-
c: c,
243-
ctx: ctx,
244-
serverHello: serverHello,
245-
hello: hello,
246-
ecdheKey: ecdheKey,
247-
helloInner: helloInner,
248-
session: session,
249-
earlySecret: earlySecret,
250-
binderKey: binderKey,
251-
hsTimings: hsTimings,
258+
c: c,
259+
ctx: ctx,
260+
serverHello: serverHello,
261+
hello: hello,
262+
helloInner: helloInner,
263+
keySharePrivate: ecdheKey,
264+
session: session,
265+
earlySecret: earlySecret,
266+
binderKey: binderKey,
267+
hsTimings: hsTimings,
252268
}
253269

254270
// In TLS 1.3, session tickets are delivered after the handshake.
@@ -581,6 +597,16 @@ func (hs *clientHandshakeState) doFullHandshake() error {
581597
return err
582598
}
583599

600+
if eccKex, ok := keyAgreement.(*ecdheKeyAgreement); ok {
601+
curveId, ok := curveIDForCurve(eccKex.key.Curve())
602+
if !ok {
603+
panic("internal error: unknown curve")
604+
}
605+
c.handleCFEvent(CFEventTLSNegotiatedNamedKEX{
606+
KEX: curveId,
607+
})
608+
}
609+
584610
msg, err = c.readHandshake(&hs.finishedHash)
585611
if err != nil {
586612
return err

0 commit comments

Comments
 (0)