Skip to content

Commit 678678e

Browse files
committed
X-Wing PQ/T hybrid
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.)
1 parent 91946a3 commit 678678e

File tree

8 files changed

+624
-1
lines changed

8 files changed

+624
-1
lines changed

hpke/algs.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/cloudflare/circl/ecc/p384"
1717
"github.com/cloudflare/circl/kem"
1818
"github.com/cloudflare/circl/kem/kyber/kyber768"
19+
"github.com/cloudflare/circl/kem/xwing"
1920
"golang.org/x/crypto/chacha20poly1305"
2021
"golang.org/x/crypto/hkdf"
2122
)
@@ -39,6 +40,8 @@ const (
3940
// KEM_X25519_KYBER768_DRAFT00 is a hybrid KEM built on DHKEM(X25519, HKDF-SHA256)
4041
// and Kyber768Draft00
4142
KEM_X25519_KYBER768_DRAFT00 KEM = 0x30
43+
// KEM_XWING is a hybrid KEM using X25519 and ML-KEM-768.
44+
KEM_XWING KEM = 0x647a
4245
)
4346

4447
// IsValid returns true if the KEM identifier is supported by the HPKE package.
@@ -49,7 +52,8 @@ func (k KEM) IsValid() bool {
4952
KEM_P521_HKDF_SHA512,
5053
KEM_X25519_HKDF_SHA256,
5154
KEM_X448_HKDF_SHA512,
52-
KEM_X25519_KYBER768_DRAFT00:
55+
KEM_X25519_KYBER768_DRAFT00,
56+
KEM_XWING:
5357
return true
5458
default:
5559
return false
@@ -72,6 +76,8 @@ func (k KEM) Scheme() kem.AuthScheme {
7276
return dhkemx448hkdfsha512
7377
case KEM_X25519_KYBER768_DRAFT00:
7478
return hybridkemX25519Kyber768
79+
case KEM_XWING:
80+
return kemXwing
7581
default:
7682
panic(ErrInvalidKEM)
7783
}
@@ -237,6 +243,7 @@ var (
237243
dhkemp256hkdfsha256, dhkemp384hkdfsha384, dhkemp521hkdfsha512 shortKEM
238244
dhkemx25519hkdfsha256, dhkemx448hkdfsha512 xKEM
239245
hybridkemX25519Kyber768 hybridKEM
246+
kemXwing genericNoAuthKEM
240247
)
241248

242249
func init() {
@@ -275,4 +282,7 @@ func init() {
275282
hybridkemX25519Kyber768.kemBase.Hash = crypto.SHA256
276283
hybridkemX25519Kyber768.kemA = dhkemx25519hkdfsha256
277284
hybridkemX25519Kyber768.kemB = kyber768.Scheme()
285+
286+
kemXwing.kem = xwing.Scheme()
287+
kemXwing.name = "HPKE_KEM_XWING"
278288
}

hpke/genericnoauthkem.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package hpke
2+
3+
// Shim to use generic KEM (kem.Scheme) as HPKE KEM.
4+
5+
import (
6+
"github.com/cloudflare/circl/internal/sha3"
7+
"github.com/cloudflare/circl/kem"
8+
)
9+
10+
// genericNoAuthKEM wraps a generic KEM (kem.Scheme) to be used as a HPKE KEM.
11+
type genericNoAuthKEM struct {
12+
kem kem.Scheme
13+
name string
14+
}
15+
16+
func (h genericNoAuthKEM) PrivateKeySize() int { return h.kem.PrivateKeySize() }
17+
func (h genericNoAuthKEM) SeedSize() int { return h.kem.SeedSize() }
18+
func (h genericNoAuthKEM) CiphertextSize() int { return h.kem.CiphertextSize() }
19+
func (h genericNoAuthKEM) PublicKeySize() int { return h.kem.PublicKeySize() }
20+
func (h genericNoAuthKEM) EncapsulationSeedSize() int { return h.kem.EncapsulationSeedSize() }
21+
func (h genericNoAuthKEM) SharedKeySize() int { return h.kem.SharedKeySize() }
22+
func (h genericNoAuthKEM) Name() string { return h.name }
23+
24+
func (h genericNoAuthKEM) AuthDecapsulate(skR kem.PrivateKey,
25+
ct []byte,
26+
pkS kem.PublicKey,
27+
) ([]byte, error) {
28+
panic("AuthDecapsulate is not supported for this KEM")
29+
}
30+
31+
func (h genericNoAuthKEM) AuthEncapsulate(pkr kem.PublicKey, sks kem.PrivateKey) (
32+
ct []byte, ss []byte, err error,
33+
) {
34+
panic("AuthEncapsulate is not supported for this KEM")
35+
}
36+
37+
func (h genericNoAuthKEM) AuthEncapsulateDeterministically(pkr kem.PublicKey, sks kem.PrivateKey, seed []byte) (ct, ss []byte, err error) {
38+
panic("AuthEncapsulateDeterministically is not supported for this KEM")
39+
}
40+
41+
func (h genericNoAuthKEM) Encapsulate(pkr kem.PublicKey) (
42+
ct []byte, ss []byte, err error,
43+
) {
44+
return h.kem.Encapsulate(pkr)
45+
}
46+
47+
func (h genericNoAuthKEM) Decapsulate(skr kem.PrivateKey, ct []byte) ([]byte, error) {
48+
return h.kem.Decapsulate(skr, ct)
49+
}
50+
51+
func (h genericNoAuthKEM) EncapsulateDeterministically(
52+
pkr kem.PublicKey, seed []byte,
53+
) (ct, ss []byte, err error) {
54+
return h.kem.EncapsulateDeterministically(pkr, seed)
55+
}
56+
57+
// HPKE requires DeriveKeyPair() to take any seed larger than the private key
58+
// size, whereas typical KEMs expect a specific seed size. We'll just use
59+
// SHAKE256 to hash it to the right size as in X-Wing.
60+
func (h genericNoAuthKEM) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
61+
seed2 := make([]byte, h.kem.SeedSize())
62+
hh := sha3.NewShake256()
63+
_, _ = hh.Write(seed)
64+
_, _ = hh.Read(seed2)
65+
return h.kem.DeriveKeyPair(seed2)
66+
}
67+
68+
func (h genericNoAuthKEM) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
69+
return h.kem.GenerateKeyPair()
70+
}
71+
72+
func (h genericNoAuthKEM) UnmarshalBinaryPrivateKey(data []byte) (kem.PrivateKey, error) {
73+
return h.kem.UnmarshalBinaryPrivateKey(data)
74+
}
75+
76+
func (h genericNoAuthKEM) UnmarshalBinaryPublicKey(data []byte) (kem.PublicKey, error) {
77+
return h.kem.UnmarshalBinaryPublicKey(data)
78+
}

hpke/hpke_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func BenchmarkHpkeRoundTrip(b *testing.B) {
160160
}{
161161
{hpke.KEM_X25519_HKDF_SHA256, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM},
162162
{hpke.KEM_X25519_KYBER768_DRAFT00, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM},
163+
{hpke.KEM_XWING, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM},
163164
}
164165
for _, test := range tests {
165166
runHpkeBenchmark(b, test.kem, test.kdf, test.aead)

kem/schemes/schemes.go

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/cloudflare/circl/kem/mlkem/mlkem1024"
3030
"github.com/cloudflare/circl/kem/mlkem/mlkem512"
3131
"github.com/cloudflare/circl/kem/mlkem/mlkem768"
32+
"github.com/cloudflare/circl/kem/xwing"
3233
)
3334

3435
var allSchemes = [...]kem.Scheme{
@@ -50,6 +51,7 @@ var allSchemes = [...]kem.Scheme{
5051
hybrid.Kyber1024X448(),
5152
hybrid.P256Kyber768Draft00(),
5253
hybrid.X25519MLKEM768(),
54+
xwing.Scheme(),
5355
}
5456

5557
var allSchemeNames map[string]kem.Scheme

kem/schemes/schemes_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,5 @@ func Example_schemes() {
160160
// Kyber1024-X448
161161
// P256Kyber768Draft00
162162
// X25519MLKEM768
163+
// X-Wing
163164
}

kem/xwing/scheme.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package xwing
2+
3+
import (
4+
"bytes"
5+
cryptoRand "crypto/rand"
6+
"crypto/subtle"
7+
8+
"github.com/cloudflare/circl/kem"
9+
"github.com/cloudflare/circl/kem/mlkem/mlkem768"
10+
)
11+
12+
// This file contains the boilerplate code to connect X-Wing to the
13+
// generic KEM API.
14+
15+
// Returns the generic KEM interface for X-Wing PQ/T hybrid KEM.
16+
func Scheme() kem.Scheme { return &xwing }
17+
18+
type scheme struct{}
19+
20+
var xwing scheme
21+
22+
func (*scheme) Name() string { return "X-Wing" }
23+
func (*scheme) PublicKeySize() int { return PublicKeySize }
24+
func (*scheme) PrivateKeySize() int { return PrivateKeySize }
25+
func (*scheme) SeedSize() int { return SeedSize }
26+
func (*scheme) EncapsulationSeedSize() int { return EncapsulationSeedSize }
27+
func (*scheme) SharedKeySize() int { return SharedKeySize }
28+
func (*scheme) CiphertextSize() int { return CiphertextSize }
29+
func (*PrivateKey) Scheme() kem.Scheme { return &xwing }
30+
func (*PublicKey) Scheme() kem.Scheme { return &xwing }
31+
32+
func (sch *scheme) Encapsulate(pk kem.PublicKey) (ct, ss []byte, err error) {
33+
var seed [EncapsulationSeedSize]byte
34+
_, err = cryptoRand.Read(seed[:])
35+
if err != nil {
36+
return
37+
}
38+
return sch.EncapsulateDeterministically(pk, seed[:])
39+
}
40+
41+
func (sch *scheme) EncapsulateDeterministically(
42+
pk kem.PublicKey, seed []byte,
43+
) ([]byte, []byte, error) {
44+
if len(seed) != EncapsulationSeedSize {
45+
return nil, nil, kem.ErrSeedSize
46+
}
47+
pub, ok := pk.(*PublicKey)
48+
if !ok {
49+
return nil, nil, kem.ErrTypeMismatch
50+
}
51+
var (
52+
ct [CiphertextSize]byte
53+
ss [SharedKeySize]byte
54+
)
55+
pub.EncapsulateTo(ct[:], ss[:], seed)
56+
return ct[:], ss[:], nil
57+
}
58+
59+
func (*scheme) UnmarshalBinaryPublicKey(buf []byte) (kem.PublicKey, error) {
60+
var pk PublicKey
61+
if len(buf) != PublicKeySize {
62+
return nil, kem.ErrPubKeySize
63+
}
64+
65+
if err := pk.Unpack(buf); err != nil {
66+
return nil, err
67+
}
68+
return &pk, nil
69+
}
70+
71+
func (*scheme) UnmarshalBinaryPrivateKey(buf []byte) (kem.PrivateKey, error) {
72+
var sk PrivateKey
73+
if len(buf) != PrivateKeySize {
74+
return nil, kem.ErrPrivKeySize
75+
}
76+
77+
sk.Unpack(buf)
78+
return &sk, nil
79+
}
80+
81+
func (sk *PrivateKey) MarshalBinary() ([]byte, error) {
82+
var ret [PrivateKeySize]byte
83+
sk.Pack(ret[:])
84+
return ret[:], nil
85+
}
86+
87+
func (sk *PrivateKey) Equal(other kem.PrivateKey) bool {
88+
oth, ok := other.(*PrivateKey)
89+
if !ok {
90+
return false
91+
}
92+
return sk.m.Equal(&oth.m) &&
93+
subtle.ConstantTimeCompare(oth.x[:], sk.x[:]) == 1
94+
}
95+
96+
func (sk *PrivateKey) Public() kem.PublicKey {
97+
var pk PublicKey
98+
pk.m = *(sk.m.Public().(*mlkem768.PublicKey))
99+
pk.x = sk.xpk
100+
return &pk
101+
}
102+
103+
func (pk *PublicKey) Equal(other kem.PublicKey) bool {
104+
oth, ok := other.(*PublicKey)
105+
if !ok {
106+
return false
107+
}
108+
return pk.m.Equal(&oth.m) && bytes.Equal(pk.x[:], oth.x[:])
109+
}
110+
111+
func (pk *PublicKey) MarshalBinary() ([]byte, error) {
112+
var ret [PublicKeySize]byte
113+
pk.Pack(ret[:])
114+
return ret[:], nil
115+
}
116+
117+
func (*scheme) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
118+
sk, pk := DeriveKeyPair(seed)
119+
return pk, sk
120+
}
121+
122+
func (sch *scheme) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
123+
sk, pk, err := GenerateKeyPair(nil)
124+
return pk, sk, err
125+
}
126+
127+
func (*scheme) Decapsulate(sk kem.PrivateKey, ct []byte) ([]byte, error) {
128+
if len(ct) != CiphertextSize {
129+
return nil, kem.ErrCiphertextSize
130+
}
131+
132+
var ss [SharedKeySize]byte
133+
134+
priv, ok := sk.(*PrivateKey)
135+
if !ok {
136+
return nil, kem.ErrTypeMismatch
137+
}
138+
139+
priv.DecapsulateTo(ss[:], ct[:])
140+
141+
return ss[:], nil
142+
}

0 commit comments

Comments
 (0)