Skip to content

Commit e315ae0

Browse files
committed
kem/hybrid: ensure X25519 hybrids fails with low order points
X25519 public keys with low order points result in an all-zeroes shared secret. Ensure that hybrids with X25519 and X448 fail during Encapsulate and Decapsulate when the peer provides such a garbage public key.
1 parent 89e658c commit e315ae0

File tree

4 files changed

+91
-8
lines changed

4 files changed

+91
-8
lines changed

kem/hybrid/ckem.go

+3
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ func (pk *cPublicKey) X(sk *cPrivateKey) []byte {
159159

160160
sharedKey, err := sk.key.ECDH(pk.key)
161161
if err != nil {
162+
// ECDH cannot fail for NIST curves as NewPublicKey rejects
163+
// invalid points and the point in infinity, and NewPrivateKey
164+
// rejects invalid scalars and the zero value.
162165
panic(err)
163166
}
164167
return sharedKey

kem/hybrid/xkem.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func (sch *xScheme) Encapsulate(pk kem.PublicKey) (ct, ss []byte, err error) {
131131
return sch.EncapsulateDeterministically(pk, seed)
132132
}
133133

134-
func (pk *xPublicKey) X(sk *xPrivateKey) []byte {
134+
func (pk *xPublicKey) X(sk *xPrivateKey) ([]byte, error) {
135135
if pk.scheme != sk.scheme {
136136
panic(kem.ErrTypeMismatch)
137137
}
@@ -141,14 +141,18 @@ func (pk *xPublicKey) X(sk *xPrivateKey) []byte {
141141
var ss2, pk2, sk2 x25519.Key
142142
copy(pk2[:], pk.key)
143143
copy(sk2[:], sk.key)
144-
x25519.Shared(&ss2, &sk2, &pk2)
145-
return ss2[:]
144+
if !x25519.Shared(&ss2, &sk2, &pk2) {
145+
return nil, kem.ErrPubKey
146+
}
147+
return ss2[:], nil
146148
case x448.Size:
147149
var ss2, pk2, sk2 x448.Key
148150
copy(pk2[:], pk.key)
149151
copy(sk2[:], sk.key)
150-
x448.Shared(&ss2, &sk2, &pk2)
151-
return ss2[:]
152+
if !x448.Shared(&ss2, &sk2, &pk2) {
153+
return nil, kem.ErrPubKey
154+
}
155+
return ss2[:], nil
152156
}
153157
panic(kem.ErrTypeMismatch)
154158
}
@@ -165,7 +169,10 @@ func (sch *xScheme) EncapsulateDeterministically(
165169
}
166170

167171
pk2, sk2 := sch.DeriveKeyPair(seed)
168-
ss = pub.X(sk2.(*xPrivateKey))
172+
ss, err = pub.X(sk2.(*xPrivateKey))
173+
if err != nil {
174+
return nil, nil, err
175+
}
169176
ct, _ = pk2.MarshalBinary()
170177
return
171178
}
@@ -185,8 +192,7 @@ func (sch *xScheme) Decapsulate(sk kem.PrivateKey, ct []byte) ([]byte, error) {
185192
return nil, err
186193
}
187194

188-
ss := pk.(*xPublicKey).X(priv)
189-
return ss, nil
195+
return pk.(*xPublicKey).X(priv)
190196
}
191197

192198
func (sch *xScheme) UnmarshalBinaryPublicKey(buf []byte) (kem.PublicKey, error) {

kem/hybrid/xkem_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package hybrid
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
7+
"github.com/cloudflare/circl/kem"
8+
)
9+
10+
func mustDecodeString(s string) []byte {
11+
b, err := hex.DecodeString(s)
12+
if err != nil {
13+
panic(err)
14+
}
15+
return b
16+
}
17+
18+
// patchHybridWithLowOrderX25519 replaces the last half of the given ciphertext
19+
// or public key by a 32-byte Curve25519 public key with a point of low order.
20+
func patchHybridWithLowOrderX25519(hybridKey []byte) {
21+
// order 8
22+
var xPub = mustDecodeString("e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800")
23+
copy(hybridKey[len(hybridKey)-len(xPub):], xPub)
24+
}
25+
26+
func patchHybridPublicKeyWithLowOrderX25519(pub kem.PublicKey) (kem.PublicKey, error) {
27+
packed, err := pub.MarshalBinary()
28+
if err != nil {
29+
return nil, err
30+
}
31+
patchHybridWithLowOrderX25519(packed)
32+
return pub.Scheme().UnmarshalBinaryPublicKey(packed)
33+
}
34+
35+
func TestLowOrderX25519PointEncapsulate(t *testing.T) {
36+
scheme := X25519MLKEM768()
37+
pk, _, err := scheme.GenerateKeyPair()
38+
if err != nil {
39+
t.Fatalf("X25519MLKEM768 keygen: %s", err)
40+
}
41+
badPk, err := patchHybridPublicKeyWithLowOrderX25519(pk)
42+
if err != nil {
43+
t.Fatalf("patching X25519 key failed: %s", err)
44+
}
45+
_, _, err = scheme.Encapsulate(badPk)
46+
want := kem.ErrPubKey
47+
if err != want {
48+
t.Fatalf("Encapsulate error: expected %v; got %v", want, err)
49+
}
50+
}
51+
52+
func TestLowOrderX25519PointDecapsulate(t *testing.T) {
53+
scheme := X25519MLKEM768()
54+
pk, sk, err := scheme.GenerateKeyPair()
55+
if err != nil {
56+
t.Fatalf("X25519MLKEM768 keygen: %s", err)
57+
}
58+
ct, _, err := scheme.Encapsulate(pk)
59+
if err != nil {
60+
t.Fatalf("Encapsulate failed: %s", err)
61+
}
62+
patchHybridWithLowOrderX25519(ct)
63+
_, err = scheme.Decapsulate(sk, ct)
64+
want := kem.ErrPubKey
65+
if err != want {
66+
t.Fatalf("Decapsulate error: expected %v; got %v", want, err)
67+
}
68+
}

kem/xwing/xwing.go

+6
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ func (pk *PublicKey) EncapsulateTo(ct, ss, seed []byte) {
252252
copy(ekx[:], seed[32:])
253253

254254
x25519.KeyGen(&ctx, &ekx)
255+
// A peer public key with low order points results in an all-zeroes
256+
// shared secret. Ignored for now pending clarification in the spec,
257+
// https://github.com/dconnolly/draft-connolly-cfrg-xwing-kem/issues/28
255258
x25519.Shared(&ssx, &ekx, &pk.x)
256259
pk.m.EncapsulateTo(ct[:mlkem768.CiphertextSize], ssm[:], seedm[:])
257260

@@ -283,6 +286,9 @@ func (sk *PrivateKey) DecapsulateTo(ss, ct []byte) {
283286
copy(ctx[:], ct[mlkem768.CiphertextSize:])
284287

285288
sk.m.DecapsulateTo(ssm[:], ctm)
289+
// A peer public key with low order points results in an all-zeroes
290+
// shared secret. Ignored for now pending clarification in the spec,
291+
// https://github.com/dconnolly/draft-connolly-cfrg-xwing-kem/issues/28
286292
x25519.Shared(&ssx, &sk.x, &ctx)
287293
combiner(ss, &ssm, &ssx, &ctx, &sk.xpk)
288294
}

0 commit comments

Comments
 (0)