Skip to content

Commit

Permalink
Add hybrid post-quantum key agreement.
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
chris-wood and bwesterb authored Jul 15, 2022
1 parent 05760f7 commit 0295761
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 55 deletions.
11 changes: 10 additions & 1 deletion src/circl/kem/hybrid/hybrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions src/circl/kem/schemes/schemes.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var allSchemes = [...]kem.Scheme{
sikep503.Scheme(),
sikep751.Scheme(),
hybrid.Kyber512X25519(),
hybrid.Kyber768X25519(),
hybrid.Kyber768X448(),
hybrid.Kyber1024X448(),
}
Expand Down
1 change: 1 addition & 0 deletions src/circl/kem/schemes/schemes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func Example_schemes() {
// SIKEp503
// SIKEp751
// Kyber512-X25519
// Kyber768-X25519
// Kyber768-X448
// Kyber1024-X448
}
102 changes: 102 additions & 0 deletions src/crypto/tls/cfkem.go
Original file line number Diff line number Diff line change
@@ -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
}
124 changes: 124 additions & 0 deletions src/crypto/tls/cfkem_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
57 changes: 37 additions & 20 deletions src/crypto/tls/handshake_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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...)
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 0295761

Please sign in to comment.