Skip to content

Commit

Permalink
X-Wing PQ/T hybrid
Browse files Browse the repository at this point in the history
Implements final version (-05)

https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/

Also includes HPKE integration with final IANA codepoint (which is different
from the one requested in -05.)
  • Loading branch information
bwesterb committed Jan 15, 2025
1 parent 91946a3 commit 678678e
Show file tree
Hide file tree
Showing 8 changed files with 624 additions and 1 deletion.
12 changes: 11 additions & 1 deletion hpke/algs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/cloudflare/circl/ecc/p384"
"github.com/cloudflare/circl/kem"
"github.com/cloudflare/circl/kem/kyber/kyber768"
"github.com/cloudflare/circl/kem/xwing"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
Expand All @@ -39,6 +40,8 @@ const (
// KEM_X25519_KYBER768_DRAFT00 is a hybrid KEM built on DHKEM(X25519, HKDF-SHA256)
// and Kyber768Draft00
KEM_X25519_KYBER768_DRAFT00 KEM = 0x30
// KEM_XWING is a hybrid KEM using X25519 and ML-KEM-768.
KEM_XWING KEM = 0x647a
)

// IsValid returns true if the KEM identifier is supported by the HPKE package.
Expand All @@ -49,7 +52,8 @@ func (k KEM) IsValid() bool {
KEM_P521_HKDF_SHA512,
KEM_X25519_HKDF_SHA256,
KEM_X448_HKDF_SHA512,
KEM_X25519_KYBER768_DRAFT00:
KEM_X25519_KYBER768_DRAFT00,
KEM_XWING:
return true
default:
return false
Expand All @@ -72,6 +76,8 @@ func (k KEM) Scheme() kem.AuthScheme {
return dhkemx448hkdfsha512
case KEM_X25519_KYBER768_DRAFT00:
return hybridkemX25519Kyber768
case KEM_XWING:
return kemXwing
default:
panic(ErrInvalidKEM)
}
Expand Down Expand Up @@ -237,6 +243,7 @@ var (
dhkemp256hkdfsha256, dhkemp384hkdfsha384, dhkemp521hkdfsha512 shortKEM
dhkemx25519hkdfsha256, dhkemx448hkdfsha512 xKEM
hybridkemX25519Kyber768 hybridKEM
kemXwing genericNoAuthKEM
)

func init() {
Expand Down Expand Up @@ -275,4 +282,7 @@ func init() {
hybridkemX25519Kyber768.kemBase.Hash = crypto.SHA256
hybridkemX25519Kyber768.kemA = dhkemx25519hkdfsha256
hybridkemX25519Kyber768.kemB = kyber768.Scheme()

kemXwing.kem = xwing.Scheme()
kemXwing.name = "HPKE_KEM_XWING"
}
78 changes: 78 additions & 0 deletions hpke/genericnoauthkem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package hpke

// Shim to use generic KEM (kem.Scheme) as HPKE KEM.

import (
"github.com/cloudflare/circl/internal/sha3"
"github.com/cloudflare/circl/kem"
)

// genericNoAuthKEM wraps a generic KEM (kem.Scheme) to be used as a HPKE KEM.
type genericNoAuthKEM struct {
kem kem.Scheme
name string
}

func (h genericNoAuthKEM) PrivateKeySize() int { return h.kem.PrivateKeySize() }
func (h genericNoAuthKEM) SeedSize() int { return h.kem.SeedSize() }
func (h genericNoAuthKEM) CiphertextSize() int { return h.kem.CiphertextSize() }
func (h genericNoAuthKEM) PublicKeySize() int { return h.kem.PublicKeySize() }
func (h genericNoAuthKEM) EncapsulationSeedSize() int { return h.kem.EncapsulationSeedSize() }
func (h genericNoAuthKEM) SharedKeySize() int { return h.kem.SharedKeySize() }
func (h genericNoAuthKEM) Name() string { return h.name }

func (h genericNoAuthKEM) AuthDecapsulate(skR kem.PrivateKey,
ct []byte,
pkS kem.PublicKey,
) ([]byte, error) {
panic("AuthDecapsulate is not supported for this KEM")
}

func (h genericNoAuthKEM) AuthEncapsulate(pkr kem.PublicKey, sks kem.PrivateKey) (
ct []byte, ss []byte, err error,
) {
panic("AuthEncapsulate is not supported for this KEM")
}

func (h genericNoAuthKEM) AuthEncapsulateDeterministically(pkr kem.PublicKey, sks kem.PrivateKey, seed []byte) (ct, ss []byte, err error) {
panic("AuthEncapsulateDeterministically is not supported for this KEM")
}

func (h genericNoAuthKEM) Encapsulate(pkr kem.PublicKey) (
ct []byte, ss []byte, err error,
) {
return h.kem.Encapsulate(pkr)
}

func (h genericNoAuthKEM) Decapsulate(skr kem.PrivateKey, ct []byte) ([]byte, error) {
return h.kem.Decapsulate(skr, ct)
}

func (h genericNoAuthKEM) EncapsulateDeterministically(
pkr kem.PublicKey, seed []byte,
) (ct, ss []byte, err error) {
return h.kem.EncapsulateDeterministically(pkr, seed)
}

// HPKE requires DeriveKeyPair() to take any seed larger than the private key
// size, whereas typical KEMs expect a specific seed size. We'll just use
// SHAKE256 to hash it to the right size as in X-Wing.
func (h genericNoAuthKEM) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
seed2 := make([]byte, h.kem.SeedSize())
hh := sha3.NewShake256()
_, _ = hh.Write(seed)
_, _ = hh.Read(seed2)
return h.kem.DeriveKeyPair(seed2)
}

func (h genericNoAuthKEM) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
return h.kem.GenerateKeyPair()
}

func (h genericNoAuthKEM) UnmarshalBinaryPrivateKey(data []byte) (kem.PrivateKey, error) {
return h.kem.UnmarshalBinaryPrivateKey(data)
}

func (h genericNoAuthKEM) UnmarshalBinaryPublicKey(data []byte) (kem.PublicKey, error) {
return h.kem.UnmarshalBinaryPublicKey(data)
}
1 change: 1 addition & 0 deletions hpke/hpke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ func BenchmarkHpkeRoundTrip(b *testing.B) {
}{
{hpke.KEM_X25519_HKDF_SHA256, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM},
{hpke.KEM_X25519_KYBER768_DRAFT00, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM},
{hpke.KEM_XWING, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM},
}
for _, test := range tests {
runHpkeBenchmark(b, test.kem, test.kdf, test.aead)
Expand Down
2 changes: 2 additions & 0 deletions kem/schemes/schemes.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/cloudflare/circl/kem/mlkem/mlkem1024"
"github.com/cloudflare/circl/kem/mlkem/mlkem512"
"github.com/cloudflare/circl/kem/mlkem/mlkem768"
"github.com/cloudflare/circl/kem/xwing"
)

var allSchemes = [...]kem.Scheme{
Expand All @@ -50,6 +51,7 @@ var allSchemes = [...]kem.Scheme{
hybrid.Kyber1024X448(),
hybrid.P256Kyber768Draft00(),
hybrid.X25519MLKEM768(),
xwing.Scheme(),
}

var allSchemeNames map[string]kem.Scheme
Expand Down
1 change: 1 addition & 0 deletions kem/schemes/schemes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,5 @@ func Example_schemes() {
// Kyber1024-X448
// P256Kyber768Draft00
// X25519MLKEM768
// X-Wing
}
142 changes: 142 additions & 0 deletions kem/xwing/scheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package xwing

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

"github.com/cloudflare/circl/kem"
"github.com/cloudflare/circl/kem/mlkem/mlkem768"
)

// This file contains the boilerplate code to connect X-Wing to the
// generic KEM API.

// Returns the generic KEM interface for X-Wing PQ/T hybrid KEM.
func Scheme() kem.Scheme { return &xwing }

type scheme struct{}

var xwing scheme

func (*scheme) Name() string { return "X-Wing" }
func (*scheme) PublicKeySize() int { return PublicKeySize }
func (*scheme) PrivateKeySize() int { return PrivateKeySize }
func (*scheme) SeedSize() int { return SeedSize }
func (*scheme) EncapsulationSeedSize() int { return EncapsulationSeedSize }
func (*scheme) SharedKeySize() int { return SharedKeySize }
func (*scheme) CiphertextSize() int { return CiphertextSize }
func (*PrivateKey) Scheme() kem.Scheme { return &xwing }
func (*PublicKey) Scheme() kem.Scheme { return &xwing }

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

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

func (*scheme) UnmarshalBinaryPublicKey(buf []byte) (kem.PublicKey, error) {
var pk PublicKey
if len(buf) != PublicKeySize {
return nil, kem.ErrPubKeySize
}

if err := pk.Unpack(buf); err != nil {
return nil, err
}
return &pk, nil
}

func (*scheme) UnmarshalBinaryPrivateKey(buf []byte) (kem.PrivateKey, error) {
var sk PrivateKey
if len(buf) != PrivateKeySize {
return nil, kem.ErrPrivKeySize
}

sk.Unpack(buf)
return &sk, nil
}

func (sk *PrivateKey) MarshalBinary() ([]byte, error) {
var ret [PrivateKeySize]byte
sk.Pack(ret[:])
return ret[:], nil
}

func (sk *PrivateKey) Equal(other kem.PrivateKey) bool {
oth, ok := other.(*PrivateKey)
if !ok {
return false
}
return sk.m.Equal(&oth.m) &&
subtle.ConstantTimeCompare(oth.x[:], sk.x[:]) == 1
}

func (sk *PrivateKey) Public() kem.PublicKey {
var pk PublicKey
pk.m = *(sk.m.Public().(*mlkem768.PublicKey))
pk.x = sk.xpk
return &pk
}

func (pk *PublicKey) Equal(other kem.PublicKey) bool {
oth, ok := other.(*PublicKey)
if !ok {
return false
}
return pk.m.Equal(&oth.m) && bytes.Equal(pk.x[:], oth.x[:])
}

func (pk *PublicKey) MarshalBinary() ([]byte, error) {
var ret [PublicKeySize]byte
pk.Pack(ret[:])
return ret[:], nil
}

func (*scheme) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
sk, pk := DeriveKeyPair(seed)
return pk, sk
}

func (sch *scheme) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
sk, pk, err := GenerateKeyPair(nil)
return pk, sk, err
}

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

var ss [SharedKeySize]byte

priv, ok := sk.(*PrivateKey)
if !ok {
return nil, kem.ErrTypeMismatch
}

priv.DecapsulateTo(ss[:], ct[:])

return ss[:], nil
}
Loading

0 comments on commit 678678e

Please sign in to comment.