Skip to content

Commit 0295761

Browse files
chris-woodbwesterb
andauthored
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 <[email protected]>
1 parent 05760f7 commit 0295761

File tree

10 files changed

+377
-55
lines changed

10 files changed

+377
-55
lines changed

src/circl/kem/hybrid/hybrid.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ var ErrUninitialized = errors.New("public or private key not initialized")
4646
// Returns the hybrid KEM of Kyber512 and X25519.
4747
func Kyber512X25519() kem.Scheme { return kyber512X }
4848

49+
// Returns the hybrid KEM of Kyber768 and X25519.
50+
func Kyber768X25519() kem.Scheme { return kyber768X }
51+
4952
// Returns the hybrid KEM of Kyber768 and X448.
50-
func Kyber768X448() kem.Scheme { return kyber768X }
53+
func Kyber768X448() kem.Scheme { return kyber768X4 }
5154

5255
// Returns the hybrid KEM of Kyber1024 and X448.
5356
func Kyber1024X448() kem.Scheme { return kyber1024X }
@@ -59,6 +62,12 @@ var kyber512X kem.Scheme = &scheme{
5962
}
6063

6164
var kyber768X kem.Scheme = &scheme{
65+
"Kyber768-X25519",
66+
kyber768.Scheme(),
67+
hpke.KEM_X25519_HKDF_SHA256.Scheme(),
68+
}
69+
70+
var kyber768X4 kem.Scheme = &scheme{
6271
"Kyber768-X448",
6372
kyber768.Scheme(),
6473
hpke.KEM_X448_HKDF_SHA512.Scheme(),

src/circl/kem/schemes/schemes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var allSchemes = [...]kem.Scheme{
4141
sikep503.Scheme(),
4242
sikep751.Scheme(),
4343
hybrid.Kyber512X25519(),
44+
hybrid.Kyber768X25519(),
4445
hybrid.Kyber768X448(),
4546
hybrid.Kyber1024X448(),
4647
}

src/circl/kem/schemes/schemes_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ func Example_schemes() {
159159
// SIKEp503
160160
// SIKEp751
161161
// Kyber512-X25519
162+
// Kyber768-X25519
162163
// Kyber768-X448
163164
// Kyber1024-X448
164165
}

src/crypto/tls/cfkem.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.Kyber512X25519().(tls.TLSScheme).TLSCurveID(),
16+
// tls.X25519,
17+
// tls.P256,
18+
// }
19+
20+
package tls
21+
22+
import (
23+
"fmt"
24+
"io"
25+
26+
"circl/kem"
27+
"circl/kem/hybrid"
28+
)
29+
30+
// Either ecdheParameters or kem.PrivateKey
31+
type clientKeySharePrivate interface{}
32+
33+
var (
34+
kyber512X25519CurveID = CurveID(0xfe30)
35+
kyber768X25519CurveID = CurveID(0xfe31)
36+
invalidCurveID = CurveID(0)
37+
)
38+
39+
func kemSchemeKeyToCurveID(s kem.Scheme) CurveID {
40+
switch s.Name() {
41+
case "Kyber512-X25519":
42+
return kyber512X25519CurveID
43+
case "Kyber768-X25519":
44+
return kyber768X25519CurveID
45+
default:
46+
return invalidCurveID
47+
}
48+
}
49+
50+
// Extract CurveID from clientKeySharePrivate
51+
func clientKeySharePrivateCurveID(ks clientKeySharePrivate) CurveID {
52+
switch v := ks.(type) {
53+
case kem.PrivateKey:
54+
ret := kemSchemeKeyToCurveID(v.Scheme())
55+
if ret == invalidCurveID {
56+
panic("cfkem: internal error: don't know CurveID for this KEM")
57+
}
58+
return ret
59+
case ecdheParameters:
60+
return v.CurveID()
61+
default:
62+
panic("cfkem: internal error: unknown clientKeySharePrivate")
63+
}
64+
}
65+
66+
// Returns scheme by CurveID if supported by Circl
67+
func curveIdToCirclScheme(id CurveID) kem.Scheme {
68+
switch id {
69+
case kyber512X25519CurveID:
70+
return hybrid.Kyber512X25519()
71+
case kyber768X25519CurveID:
72+
return hybrid.Kyber768X25519()
73+
}
74+
return nil
75+
}
76+
77+
// Generate a new shared secret and encapsulates it for the packed
78+
// public key in ppk using randomness from rnd.
79+
func encapsulateForKem(scheme kem.Scheme, rnd io.Reader, ppk []byte) (
80+
ct, ss []byte, alert alert, err error) {
81+
pk, err := scheme.UnmarshalBinaryPublicKey(ppk)
82+
if err != nil {
83+
return nil, nil, alertIllegalParameter, fmt.Errorf("unpack pk: %w", err)
84+
}
85+
seed := make([]byte, scheme.EncapsulationSeedSize())
86+
if _, err := io.ReadFull(rnd, seed); err != nil {
87+
return nil, nil, alertInternalError, fmt.Errorf("random: %w", err)
88+
}
89+
ct, ss, err = scheme.EncapsulateDeterministically(pk, seed)
90+
return ct, ss, alertIllegalParameter, err
91+
}
92+
93+
// Generate a new keypair using randomness from rnd.
94+
func generateKemKeyPair(scheme kem.Scheme, rnd io.Reader) (
95+
kem.PublicKey, kem.PrivateKey, error) {
96+
seed := make([]byte, scheme.SeedSize())
97+
if _, err := io.ReadFull(rnd, seed); err != nil {
98+
return nil, nil, err
99+
}
100+
pk, sk := scheme.DeriveKeyPair(seed)
101+
return pk, sk, nil
102+
}

src/crypto/tls/cfkem_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
"circl/kem"
11+
"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 CFEventTLS13NegotiatedKEX:
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 {
79+
expectedKEX = kemSchemeKeyToCurveID(scheme)
80+
} else {
81+
expectedKEX = X25519
82+
}
83+
if clientPQ && !serverPQ {
84+
expectedRetry = true
85+
}
86+
87+
if !serverTLS12 && !clientTLS12 {
88+
if clientSelectedKEX == nil {
89+
t.Error("No TLS 1.3 KEX happened?")
90+
}
91+
92+
if *clientSelectedKEX != expectedKEX {
93+
t.Errorf("failed to negotiate: expected %d, got %d",
94+
expectedKEX, *clientSelectedKEX)
95+
}
96+
if expectedRetry != retry {
97+
t.Errorf("Expected retry=%v, got retry=%v", expectedRetry, retry)
98+
}
99+
} else {
100+
if clientSelectedKEX != nil {
101+
t.Error("TLS 1.3 KEX happened?")
102+
}
103+
}
104+
}
105+
106+
func TestHybridKEX(t *testing.T) {
107+
run := func(scheme kem.Scheme, clientPQ, serverPQ, clientTLS12, serverTLS12 bool) {
108+
t.Run(fmt.Sprintf("%s serverPQ:%v clientPQ:%v serverTLS12:%v clientTLS12:%v", scheme.Name(),
109+
serverPQ, clientPQ, serverTLS12, clientTLS12), func(t *testing.T) {
110+
testHybridKEX(t, scheme, clientPQ, serverPQ, clientTLS12, serverTLS12)
111+
})
112+
}
113+
for _, scheme := range []kem.Scheme{
114+
hybrid.Kyber512X25519(),
115+
hybrid.Kyber768X25519(),
116+
} {
117+
run(scheme, true, true, false, false)
118+
run(scheme, true, false, false, false)
119+
run(scheme, false, true, false, false)
120+
run(scheme, true, true, true, false)
121+
run(scheme, true, true, false, true)
122+
run(scheme, true, true, true, true)
123+
}
124+
}

src/crypto/tls/handshake_client.go

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type clientHandshakeState struct {
3636
session *ClientSessionState
3737
}
3838

39-
func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, ecdheParameters, error) {
39+
func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, clientKeySharePrivate, error) {
4040
config := c.config
4141
if len(config.ServerName) == 0 && !config.InsecureSkipVerify {
4242
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
122122
hello.supportedSignatureAlgorithms = config.supportedSignatureAlgorithms()
123123
}
124124

125-
var params ecdheParameters
125+
var secret clientKeySharePrivate
126126
if hello.supportedVersions[0] == VersionTLS13 {
127127
if hasAESGCMHardwareSupport {
128128
hello.cipherSuites = append(hello.cipherSuites, defaultCipherSuitesTLS13...)
@@ -131,19 +131,36 @@ func (c *Conn) makeClientHello(minVersion uint16) (*clientHelloMsg, ecdheParamet
131131
}
132132

133133
curveID := config.curvePreferences()[0]
134-
if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok {
135-
return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
136-
}
137-
params, err = generateECDHEParameters(config.rand(), curveID)
138-
if err != nil {
139-
return nil, nil, err
134+
if scheme := curveIdToCirclScheme(curveID); scheme != nil {
135+
pk, sk, err := generateKemKeyPair(scheme, config.rand())
136+
if err != nil {
137+
return nil, nil, fmt.Errorf("generateKemKeyPair %s: %w",
138+
scheme.Name(), err)
139+
}
140+
packedPk, err := pk.MarshalBinary()
141+
if err != nil {
142+
return nil, nil, fmt.Errorf("pack circl public key %s: %w",
143+
scheme.Name(), err)
144+
}
145+
hello.keyShares = []keyShare{{group: curveID, data: packedPk}}
146+
secret = sk
147+
} else {
148+
if _, ok := curveForCurveID(curveID); curveID != X25519 && !ok {
149+
return nil, nil, errors.New("tls: CurvePreferences includes unsupported curve")
150+
}
151+
params, err := generateECDHEParameters(config.rand(), curveID)
152+
if err != nil {
153+
return nil, nil, err
154+
}
155+
hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}}
156+
secret = params
140157
}
141-
hello.keyShares = []keyShare{{group: curveID, data: params.PublicKey()}}
158+
142159
hello.delegatedCredentialSupported = config.SupportDelegatedCredential
143160
hello.supportedSignatureAlgorithmsDC = supportedSignatureAlgorithmsDC
144161
}
145162

146-
return hello, params, nil
163+
return hello, secret, nil
147164
}
148165

149166
func (c *Conn) clientHandshake(ctx context.Context) (err error) {
@@ -230,16 +247,16 @@ func (c *Conn) clientHandshake(ctx context.Context) (err error) {
230247

231248
if c.vers == VersionTLS13 {
232249
hs := &clientHandshakeStateTLS13{
233-
c: c,
234-
ctx: ctx,
235-
serverHello: serverHello,
236-
hello: hello,
237-
helloInner: helloInner,
238-
ecdheParams: ecdheParams,
239-
session: session,
240-
earlySecret: earlySecret,
241-
binderKey: binderKey,
242-
hsTimings: hsTimings,
250+
c: c,
251+
ctx: ctx,
252+
serverHello: serverHello,
253+
hello: hello,
254+
helloInner: helloInner,
255+
keySharePrivate: ecdheParams,
256+
session: session,
257+
earlySecret: earlySecret,
258+
binderKey: binderKey,
259+
hsTimings: hsTimings,
243260
}
244261

245262
// In TLS 1.3, session tickets are delivered after the handshake.

0 commit comments

Comments
 (0)