Skip to content

Commit

Permalink
Add hybrid post-quantum key agreement.
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
bwesterb and chris-wood committed Dec 7, 2022
1 parent 08bcce7 commit c1f2e83
Show file tree
Hide file tree
Showing 13 changed files with 605 additions and 65 deletions.
18 changes: 13 additions & 5 deletions src/circl/kem/hybrid/hybrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ package hybrid
import (
"errors"

"circl/hpke"
"circl/internal/sha3"
"circl/kem"
"circl/kem/kyber/kyber1024"
Expand All @@ -46,28 +45,37 @@ 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 }

var kyber512X kem.Scheme = &scheme{
"Kyber512-X25519",
x25519Kem,
kyber512.Scheme(),
hpke.KEM_X25519_HKDF_SHA256.Scheme(),
}

var kyber768X kem.Scheme = &scheme{
"Kyber768-X25519",
x25519Kem,
kyber768.Scheme(),
}

var kyber768X4 kem.Scheme = &scheme{
"Kyber768-X448",
x448Kem,
kyber768.Scheme(),
hpke.KEM_X448_HKDF_SHA512.Scheme(),
}

var kyber1024X kem.Scheme = &scheme{
"Kyber1024-X448",
x448Kem,
kyber1024.Scheme(),
hpke.KEM_X448_HKDF_SHA512.Scheme(),
}

// Public key of a hybrid KEM.
Expand Down
208 changes: 208 additions & 0 deletions src/circl/kem/hybrid/xkem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package hybrid

import (
"bytes"
cryptoRand "crypto/rand"
"crypto/subtle"

"circl/dh/x25519"
"circl/dh/x448"
"circl/internal/sha3"
"circl/kem"
)

type xPublicKey struct {
scheme *xScheme
key []byte
}
type xPrivateKey struct {
scheme *xScheme
key []byte
}
type xScheme struct {
size int
}

var (
x25519Kem = &xScheme{x25519.Size}
x448Kem = &xScheme{x448.Size}
)

func (sch *xScheme) Name() string {
switch sch.size {
case x25519.Size:
return "X25519"
case x448.Size:
return "X448"
}
panic(kem.ErrTypeMismatch)
}

func (sch *xScheme) PublicKeySize() int { return sch.size }
func (sch *xScheme) PrivateKeySize() int { return sch.size }
func (sch *xScheme) SeedSize() int { return sch.size }
func (sch *xScheme) SharedKeySize() int { return sch.size }
func (sch *xScheme) CiphertextSize() int { return sch.size }
func (sch *xScheme) EncapsulationSeedSize() int { return sch.size }

func (sk *xPrivateKey) Scheme() kem.Scheme { return sk.scheme }
func (pk *xPublicKey) Scheme() kem.Scheme { return pk.scheme }

func (sk *xPrivateKey) MarshalBinary() ([]byte, error) {
ret := make([]byte, len(sk.key))
copy(ret, sk.key)
return ret, nil
}

func (sk *xPrivateKey) Equal(other kem.PrivateKey) bool {
oth, ok := other.(*xPrivateKey)
if !ok {
return false
}
if oth.scheme != sk.scheme {
return false
}
return subtle.ConstantTimeCompare(oth.key, sk.key) == 1
}

func (sk *xPrivateKey) Public() kem.PublicKey {
pk := xPublicKey{sk.scheme, make([]byte, sk.scheme.size)}
switch sk.scheme.size {
case x25519.Size:
var sk2, pk2 x25519.Key
copy(sk2[:], sk.key)
x25519.KeyGen(&pk2, &sk2)
copy(pk.key, pk2[:])
case x448.Size:
var sk2, pk2 x448.Key
copy(sk2[:], sk.key)
x448.KeyGen(&pk2, &sk2)
copy(pk.key, pk2[:])
}
return &pk
}

func (pk *xPublicKey) Equal(other kem.PublicKey) bool {
oth, ok := other.(*xPublicKey)
if !ok {
return false
}
if oth.scheme != pk.scheme {
return false
}
return bytes.Equal(oth.key, pk.key)
}

func (pk *xPublicKey) MarshalBinary() ([]byte, error) {
ret := make([]byte, pk.scheme.size)
copy(ret, pk.key)
return ret, nil
}

func (sch *xScheme) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
seed := make([]byte, sch.SeedSize())
_, err := cryptoRand.Read(seed)
if err != nil {
return nil, nil, err
}
pk, sk := sch.DeriveKeyPair(seed)
return pk, sk, nil
}

func (sch *xScheme) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
if len(seed) != sch.SeedSize() {
panic(kem.ErrSeedSize)
}
sk := xPrivateKey{scheme: sch, key: make([]byte, sch.size)}

h := sha3.NewShake256()
_, _ = h.Write(seed)
_, _ = h.Read(sk.key)

return sk.Public(), &sk
}

func (sch *xScheme) Encapsulate(pk kem.PublicKey) (ct, ss []byte, err error) {
seed := make([]byte, sch.EncapsulationSeedSize())
_, err = cryptoRand.Read(seed)
if err != nil {
return
}
return sch.EncapsulateDeterministically(pk, seed)
}

func (pk *xPublicKey) X(sk *xPrivateKey) []byte {
if pk.scheme != sk.scheme {
panic(kem.ErrTypeMismatch)
}

switch pk.scheme.size {
case x25519.Size:
var ss2, pk2, sk2 x25519.Key
copy(pk2[:], pk.key)
copy(sk2[:], sk.key)
x25519.Shared(&ss2, &sk2, &pk2)
return ss2[:]
case x448.Size:
var ss2, pk2, sk2 x448.Key
copy(pk2[:], pk.key)
copy(sk2[:], sk.key)
x448.Shared(&ss2, &sk2, &pk2)
return ss2[:]
}
panic(kem.ErrTypeMismatch)
}

func (sch *xScheme) EncapsulateDeterministically(
pk kem.PublicKey, seed []byte,
) (ct, ss []byte, err error) {
if len(seed) != sch.EncapsulationSeedSize() {
return nil, nil, kem.ErrSeedSize
}
pub, ok := pk.(*xPublicKey)
if !ok || pub.scheme != sch {
return nil, nil, kem.ErrTypeMismatch
}

pk2, sk2 := sch.DeriveKeyPair(seed)
ss = pub.X(sk2.(*xPrivateKey))
ct, _ = pk2.MarshalBinary()
return
}

func (sch *xScheme) Decapsulate(sk kem.PrivateKey, ct []byte) ([]byte, error) {
if len(ct) != sch.CiphertextSize() {
return nil, kem.ErrCiphertextSize
}

priv, ok := sk.(*xPrivateKey)
if !ok || priv.scheme != sch {
return nil, kem.ErrTypeMismatch
}

pk, err := sch.UnmarshalBinaryPublicKey(ct)
if err != nil {
return nil, err
}

ss := pk.(*xPublicKey).X(priv)
return ss, nil
}

func (sch *xScheme) UnmarshalBinaryPublicKey(buf []byte) (kem.PublicKey, error) {
if len(buf) != sch.PublicKeySize() {
return nil, kem.ErrPubKeySize
}
ret := xPublicKey{sch, make([]byte, sch.size)}
copy(ret.key, buf)
return &ret, nil
}

func (sch *xScheme) UnmarshalBinaryPrivateKey(buf []byte) (kem.PrivateKey, error) {
if len(buf) != sch.PrivateKeySize() {
return nil, kem.ErrPrivKeySize
}
ret := xPrivateKey{sch, make([]byte, sch.size)}
copy(ret.key, buf)
return &ret, nil
}
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
}
14 changes: 7 additions & 7 deletions src/circl/pke/kyber/internal/common/ntt.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ var InvNTTReductions = [...]int{
// their proper order by calling Detangle().
func (p *Poly) nttGeneric() {
// Note that ℤ_q does not have a primitive 512ᵗʰ root of unity (as 512
// does not divide into q) and so we cannot do a regular NTT. ℤ_q
// does not divide into q-1) and so we cannot do a regular NTT. ℤ_q
// does have a primitive 256ᵗʰ root of unity, the smallest of which
// is ζ := 17.
//
Expand All @@ -73,12 +73,12 @@ func (p *Poly) nttGeneric() {
// ⋮
// = (x² - ζ)(x² + ζ)(x² - ζ⁶⁵)(x² + ζ⁶⁵) … (x² + ζ¹²⁷)
//
// Note that the powers of ζ that appear (from th second line down) are
// Note that the powers of ζ that appear (from the second line down) are
// in binary
//
// 010000 110000
// 001000 101000 011000 111000
// 000100 100100 010100 110100 001100 101100 011100 111100
// 0100000 1100000
// 0010000 1010000 0110000 1110000
// 0001000 1001000 0101000 1101000 0011000 1011000 0111000 1111000
// …
//
// That is: brv(2), brv(3), brv(4), …, where brv(x) denotes the 7-bit
Expand All @@ -89,7 +89,7 @@ func (p *Poly) nttGeneric() {
//
// ℤ_q[x]/(x²⁵⁶+1) → ℤ_q[x]/(x²-ζ) x … x ℤ_q[x]/(x²+ζ¹²⁷)
//
// given by a ↦ ( a mod x²-z, …, a mod x²+z¹²⁷ )
// given by a ↦ ( a mod x²-ζ, …, a mod x²+ζ¹²⁷ )
// is an isomorphism, which is the "NTT". It can be efficiently computed by
//
//
Expand All @@ -105,7 +105,7 @@ func (p *Poly) nttGeneric() {
//
// Each cross is a Cooley-Tukey butterfly: it's the map
//
// (a, b) ↦ (a + ζ, a - ζ)
// (a, b) ↦ (a + ζb, a - ζb)
//
// for the appropriate power ζ for that column and row group.

Expand Down
Loading

0 comments on commit c1f2e83

Please sign in to comment.