Skip to content

Commit 3df0d74

Browse files
bwesterbchris-wood
andcommitted
Add hybrid post-quantum key agreement.
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]>
1 parent 4eb06c2 commit 3df0d74

File tree

13 files changed

+605
-65
lines changed

13 files changed

+605
-65
lines changed

src/circl/kem/hybrid/hybrid.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ package hybrid
3333
import (
3434
"errors"
3535

36-
"circl/hpke"
3736
"circl/internal/sha3"
3837
"circl/kem"
3938
"circl/kem/kyber/kyber1024"
@@ -46,28 +45,37 @@ var ErrUninitialized = errors.New("public or private key not initialized")
4645
// Returns the hybrid KEM of Kyber512 and X25519.
4746
func Kyber512X25519() kem.Scheme { return kyber512X }
4847

48+
// Returns the hybrid KEM of Kyber768 and X25519.
49+
func Kyber768X25519() kem.Scheme { return kyber768X }
50+
4951
// Returns the hybrid KEM of Kyber768 and X448.
50-
func Kyber768X448() kem.Scheme { return kyber768X }
52+
func Kyber768X448() kem.Scheme { return kyber768X4 }
5153

5254
// Returns the hybrid KEM of Kyber1024 and X448.
5355
func Kyber1024X448() kem.Scheme { return kyber1024X }
5456

5557
var kyber512X kem.Scheme = &scheme{
5658
"Kyber512-X25519",
59+
x25519Kem,
5760
kyber512.Scheme(),
58-
hpke.KEM_X25519_HKDF_SHA256.Scheme(),
5961
}
6062

6163
var kyber768X kem.Scheme = &scheme{
64+
"Kyber768-X25519",
65+
x25519Kem,
66+
kyber768.Scheme(),
67+
}
68+
69+
var kyber768X4 kem.Scheme = &scheme{
6270
"Kyber768-X448",
71+
x448Kem,
6372
kyber768.Scheme(),
64-
hpke.KEM_X448_HKDF_SHA512.Scheme(),
6573
}
6674

6775
var kyber1024X kem.Scheme = &scheme{
6876
"Kyber1024-X448",
77+
x448Kem,
6978
kyber1024.Scheme(),
70-
hpke.KEM_X448_HKDF_SHA512.Scheme(),
7179
}
7280

7381
// Public key of a hybrid KEM.

src/circl/kem/hybrid/xkem.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package hybrid
2+
3+
import (
4+
"bytes"
5+
cryptoRand "crypto/rand"
6+
"crypto/subtle"
7+
8+
"circl/dh/x25519"
9+
"circl/dh/x448"
10+
"circl/internal/sha3"
11+
"circl/kem"
12+
)
13+
14+
type xPublicKey struct {
15+
scheme *xScheme
16+
key []byte
17+
}
18+
type xPrivateKey struct {
19+
scheme *xScheme
20+
key []byte
21+
}
22+
type xScheme struct {
23+
size int
24+
}
25+
26+
var (
27+
x25519Kem = &xScheme{x25519.Size}
28+
x448Kem = &xScheme{x448.Size}
29+
)
30+
31+
func (sch *xScheme) Name() string {
32+
switch sch.size {
33+
case x25519.Size:
34+
return "X25519"
35+
case x448.Size:
36+
return "X448"
37+
}
38+
panic(kem.ErrTypeMismatch)
39+
}
40+
41+
func (sch *xScheme) PublicKeySize() int { return sch.size }
42+
func (sch *xScheme) PrivateKeySize() int { return sch.size }
43+
func (sch *xScheme) SeedSize() int { return sch.size }
44+
func (sch *xScheme) SharedKeySize() int { return sch.size }
45+
func (sch *xScheme) CiphertextSize() int { return sch.size }
46+
func (sch *xScheme) EncapsulationSeedSize() int { return sch.size }
47+
48+
func (sk *xPrivateKey) Scheme() kem.Scheme { return sk.scheme }
49+
func (pk *xPublicKey) Scheme() kem.Scheme { return pk.scheme }
50+
51+
func (sk *xPrivateKey) MarshalBinary() ([]byte, error) {
52+
ret := make([]byte, len(sk.key))
53+
copy(ret, sk.key)
54+
return ret, nil
55+
}
56+
57+
func (sk *xPrivateKey) Equal(other kem.PrivateKey) bool {
58+
oth, ok := other.(*xPrivateKey)
59+
if !ok {
60+
return false
61+
}
62+
if oth.scheme != sk.scheme {
63+
return false
64+
}
65+
return subtle.ConstantTimeCompare(oth.key, sk.key) == 1
66+
}
67+
68+
func (sk *xPrivateKey) Public() kem.PublicKey {
69+
pk := xPublicKey{sk.scheme, make([]byte, sk.scheme.size)}
70+
switch sk.scheme.size {
71+
case x25519.Size:
72+
var sk2, pk2 x25519.Key
73+
copy(sk2[:], sk.key)
74+
x25519.KeyGen(&pk2, &sk2)
75+
copy(pk.key, pk2[:])
76+
case x448.Size:
77+
var sk2, pk2 x448.Key
78+
copy(sk2[:], sk.key)
79+
x448.KeyGen(&pk2, &sk2)
80+
copy(pk.key, pk2[:])
81+
}
82+
return &pk
83+
}
84+
85+
func (pk *xPublicKey) Equal(other kem.PublicKey) bool {
86+
oth, ok := other.(*xPublicKey)
87+
if !ok {
88+
return false
89+
}
90+
if oth.scheme != pk.scheme {
91+
return false
92+
}
93+
return bytes.Equal(oth.key, pk.key)
94+
}
95+
96+
func (pk *xPublicKey) MarshalBinary() ([]byte, error) {
97+
ret := make([]byte, pk.scheme.size)
98+
copy(ret, pk.key)
99+
return ret, nil
100+
}
101+
102+
func (sch *xScheme) GenerateKeyPair() (kem.PublicKey, kem.PrivateKey, error) {
103+
seed := make([]byte, sch.SeedSize())
104+
_, err := cryptoRand.Read(seed)
105+
if err != nil {
106+
return nil, nil, err
107+
}
108+
pk, sk := sch.DeriveKeyPair(seed)
109+
return pk, sk, nil
110+
}
111+
112+
func (sch *xScheme) DeriveKeyPair(seed []byte) (kem.PublicKey, kem.PrivateKey) {
113+
if len(seed) != sch.SeedSize() {
114+
panic(kem.ErrSeedSize)
115+
}
116+
sk := xPrivateKey{scheme: sch, key: make([]byte, sch.size)}
117+
118+
h := sha3.NewShake256()
119+
_, _ = h.Write(seed)
120+
_, _ = h.Read(sk.key)
121+
122+
return sk.Public(), &sk
123+
}
124+
125+
func (sch *xScheme) Encapsulate(pk kem.PublicKey) (ct, ss []byte, err error) {
126+
seed := make([]byte, sch.EncapsulationSeedSize())
127+
_, err = cryptoRand.Read(seed)
128+
if err != nil {
129+
return
130+
}
131+
return sch.EncapsulateDeterministically(pk, seed)
132+
}
133+
134+
func (pk *xPublicKey) X(sk *xPrivateKey) []byte {
135+
if pk.scheme != sk.scheme {
136+
panic(kem.ErrTypeMismatch)
137+
}
138+
139+
switch pk.scheme.size {
140+
case x25519.Size:
141+
var ss2, pk2, sk2 x25519.Key
142+
copy(pk2[:], pk.key)
143+
copy(sk2[:], sk.key)
144+
x25519.Shared(&ss2, &sk2, &pk2)
145+
return ss2[:]
146+
case x448.Size:
147+
var ss2, pk2, sk2 x448.Key
148+
copy(pk2[:], pk.key)
149+
copy(sk2[:], sk.key)
150+
x448.Shared(&ss2, &sk2, &pk2)
151+
return ss2[:]
152+
}
153+
panic(kem.ErrTypeMismatch)
154+
}
155+
156+
func (sch *xScheme) EncapsulateDeterministically(
157+
pk kem.PublicKey, seed []byte,
158+
) (ct, ss []byte, err error) {
159+
if len(seed) != sch.EncapsulationSeedSize() {
160+
return nil, nil, kem.ErrSeedSize
161+
}
162+
pub, ok := pk.(*xPublicKey)
163+
if !ok || pub.scheme != sch {
164+
return nil, nil, kem.ErrTypeMismatch
165+
}
166+
167+
pk2, sk2 := sch.DeriveKeyPair(seed)
168+
ss = pub.X(sk2.(*xPrivateKey))
169+
ct, _ = pk2.MarshalBinary()
170+
return
171+
}
172+
173+
func (sch *xScheme) Decapsulate(sk kem.PrivateKey, ct []byte) ([]byte, error) {
174+
if len(ct) != sch.CiphertextSize() {
175+
return nil, kem.ErrCiphertextSize
176+
}
177+
178+
priv, ok := sk.(*xPrivateKey)
179+
if !ok || priv.scheme != sch {
180+
return nil, kem.ErrTypeMismatch
181+
}
182+
183+
pk, err := sch.UnmarshalBinaryPublicKey(ct)
184+
if err != nil {
185+
return nil, err
186+
}
187+
188+
ss := pk.(*xPublicKey).X(priv)
189+
return ss, nil
190+
}
191+
192+
func (sch *xScheme) UnmarshalBinaryPublicKey(buf []byte) (kem.PublicKey, error) {
193+
if len(buf) != sch.PublicKeySize() {
194+
return nil, kem.ErrPubKeySize
195+
}
196+
ret := xPublicKey{sch, make([]byte, sch.size)}
197+
copy(ret.key, buf)
198+
return &ret, nil
199+
}
200+
201+
func (sch *xScheme) UnmarshalBinaryPrivateKey(buf []byte) (kem.PrivateKey, error) {
202+
if len(buf) != sch.PrivateKeySize() {
203+
return nil, kem.ErrPrivKeySize
204+
}
205+
ret := xPrivateKey{sch, make([]byte, sch.size)}
206+
copy(ret.key, buf)
207+
return &ret, nil
208+
}

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/circl/pke/kyber/internal/common/ntt.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var InvNTTReductions = [...]int{
5959
// their proper order by calling Detangle().
6060
func (p *Poly) nttGeneric() {
6161
// Note that ℤ_q does not have a primitive 512ᵗʰ root of unity (as 512
62-
// does not divide into q) and so we cannot do a regular NTT. ℤ_q
62+
// does not divide into q-1) and so we cannot do a regular NTT. ℤ_q
6363
// does have a primitive 256ᵗʰ root of unity, the smallest of which
6464
// is ζ := 17.
6565
//
@@ -73,12 +73,12 @@ func (p *Poly) nttGeneric() {
7373
// ⋮
7474
// = (x² - ζ)(x² + ζ)(x² - ζ⁶⁵)(x² + ζ⁶⁵) … (x² + ζ¹²⁷)
7575
//
76-
// Note that the powers of ζ that appear (from th second line down) are
76+
// Note that the powers of ζ that appear (from the second line down) are
7777
// in binary
7878
//
79-
// 010000 110000
80-
// 001000 101000 011000 111000
81-
// 000100 100100 010100 110100 001100 101100 011100 111100
79+
// 0100000 1100000
80+
// 0010000 1010000 0110000 1110000
81+
// 0001000 1001000 0101000 1101000 0011000 1011000 0111000 1111000
8282
// …
8383
//
8484
// That is: brv(2), brv(3), brv(4), …, where brv(x) denotes the 7-bit
@@ -89,7 +89,7 @@ func (p *Poly) nttGeneric() {
8989
//
9090
// ℤ_q[x]/(x²⁵⁶+1) → ℤ_q[x]/(x²-ζ) x … x ℤ_q[x]/(x²+ζ¹²⁷)
9191
//
92-
// given by a ↦ ( a mod x²-z, …, a mod x²+z¹²⁷ )
92+
// given by a ↦ ( a mod x²-ζ, …, a mod x²+ζ¹²⁷ )
9393
// is an isomorphism, which is the "NTT". It can be efficiently computed by
9494
//
9595
//
@@ -105,7 +105,7 @@ func (p *Poly) nttGeneric() {
105105
//
106106
// Each cross is a Cooley-Tukey butterfly: it's the map
107107
//
108-
// (a, b) ↦ (a + ζ, a - ζ)
108+
// (a, b) ↦ (a + ζb, a - ζb)
109109
//
110110
// for the appropriate power ζ for that column and row group.
111111

0 commit comments

Comments
 (0)