Skip to content

Commit c350df6

Browse files
committed
[WIP] HPKE updates
From draft-ietf-hpke-pq-01 implements: - The one-stage SHAKE-based KDFs in the key schedule. - Hybrid QSF-X25519-MLKEM768 KEM (a.k.a. X-Wing) - Pure ML-KEM-{512,768,1024} KEMs. Does not include QSF-P384-MLKEM1024, QSF-P256-MLKEM768 (yet), nor support for the one-stage KDFs in DHKEM.
1 parent aa837fd commit c350df6

File tree

9 files changed

+771
-71
lines changed

9 files changed

+771
-71
lines changed

hpke/aead.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ func (c *encdecContext) Export(exporterContext []byte, length uint) []byte {
3636
if length > maxLength {
3737
panic(fmt.Errorf("output length must be lesser than %v bytes", maxLength))
3838
}
39-
return c.suite.labeledExpand(c.exporterSecret, []byte("sec"),
39+
if c.suite.kdfID.IsTwoStage() {
40+
return c.suite.labeledExpand(c.exporterSecret, []byte("sec"),
41+
exporterContext, uint16(length))
42+
}
43+
return c.suite.labeledDerive(c.exporterSecret, []byte("sec"),
4044
exporterContext, uint16(length))
4145
}
4246

hpke/algs.go

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ import (
1313

1414
"github.com/cloudflare/circl/dh/x25519"
1515
"github.com/cloudflare/circl/dh/x448"
16+
"github.com/cloudflare/circl/internal/sha3"
1617
"github.com/cloudflare/circl/kem"
1718
"github.com/cloudflare/circl/kem/kyber/kyber768"
19+
"github.com/cloudflare/circl/kem/mlkem/mlkem1024"
20+
"github.com/cloudflare/circl/kem/mlkem/mlkem512"
21+
"github.com/cloudflare/circl/kem/mlkem/mlkem768"
1822
"github.com/cloudflare/circl/kem/xwing"
1923
"golang.org/x/crypto/chacha20poly1305"
2024
"golang.org/x/crypto/hkdf"
@@ -41,6 +45,13 @@ const (
4145
KEM_X25519_KYBER768_DRAFT00 KEM = 0x30
4246
// KEM_XWING is a hybrid KEM using X25519 and ML-KEM-768.
4347
KEM_XWING KEM = 0x647a
48+
49+
KEM_MLKEM512 KEM = 0x0040
50+
KEM_MLKEM768 KEM = 0x0041
51+
KEM_MLKEM1024 KEM = 0x0042
52+
53+
KEM_QSF_P256_MLKEM768 = 0x0050
54+
KEM_QSF_P384_MLKEM1024 = 0x0052
4455
)
4556

4657
// IsValid returns true if the KEM identifier is supported by the HPKE package.
@@ -52,7 +63,10 @@ func (k KEM) IsValid() bool {
5263
KEM_X25519_HKDF_SHA256,
5364
KEM_X448_HKDF_SHA512,
5465
KEM_X25519_KYBER768_DRAFT00,
55-
KEM_XWING:
66+
KEM_XWING,
67+
KEM_MLKEM512,
68+
KEM_MLKEM768,
69+
KEM_MLKEM1024:
5670
return true
5771
default:
5872
return false
@@ -77,6 +91,12 @@ func (k KEM) Scheme() kem.Scheme {
7791
return hybridkemX25519Kyber768
7892
case KEM_XWING:
7993
return kemXwing
94+
case KEM_MLKEM512:
95+
return kemMLKEM512
96+
case KEM_MLKEM768:
97+
return kemMLKEM768
98+
case KEM_MLKEM1024:
99+
return kemMLKEM1024
80100
default:
81101
panic(ErrInvalidKEM)
82102
}
@@ -86,27 +106,57 @@ type KDF uint16
86106

87107
//nolint:golint,stylecheck
88108
const (
89-
// KDF_HKDF_SHA256 is a KDF using HKDF with SHA-256.
109+
// KDF_HKDF_SHA256 is a two-stage KDF using HKDF with SHA-256.
90110
KDF_HKDF_SHA256 KDF = 0x01
91-
// KDF_HKDF_SHA384 is a KDF using HKDF with SHA-384.
111+
// KDF_HKDF_SHA384 is a two-stage KDF using HKDF with SHA-384.
92112
KDF_HKDF_SHA384 KDF = 0x02
93-
// KDF_HKDF_SHA512 is a KDF using HKDF with SHA-512.
113+
// KDF_HKDF_SHA512 is a two-stage KDF using HKDF with SHA-512.
94114
KDF_HKDF_SHA512 KDF = 0x03
115+
116+
// KDF_SHAKE128 is a one-stage KDF using SHAKE-128.
117+
KDF_SHAKE128 = 0x10
118+
// KDF_SHAKE256 is a one-stage KDF using SHAKE-256.
119+
KDF_SHAKE256 = 0x11
120+
// KDF_TurboSHAKE128 is a one-stage KDF using TurboSHAKE-128.
121+
KDF_TurboSHAKE128 = 0x12
122+
// KDF_TurboSHAKE256 is a one-stage KDF using TurboSHAKE-256.
123+
KDF_TurboSHAKE256 = 0x13
95124
)
96125

97-
func (k KDF) IsValid() bool {
126+
func (k KDF) IsTwoStage() bool {
98127
switch k {
99128
case KDF_HKDF_SHA256,
100129
KDF_HKDF_SHA384,
101130
KDF_HKDF_SHA512:
102131
return true
132+
case KDF_SHAKE128,
133+
KDF_TurboSHAKE128,
134+
KDF_SHAKE256,
135+
KDF_TurboSHAKE256:
136+
return false
137+
default:
138+
panic(ErrInvalidKDF)
139+
}
140+
}
141+
142+
func (k KDF) IsValid() bool {
143+
switch k {
144+
case KDF_HKDF_SHA256,
145+
KDF_HKDF_SHA384,
146+
KDF_HKDF_SHA512,
147+
KDF_SHAKE128,
148+
KDF_TurboSHAKE128,
149+
KDF_SHAKE256,
150+
KDF_TurboSHAKE256:
151+
return true
103152
default:
104153
return false
105154
}
106155
}
107156

108157
// ExtractSize returns the size (in bytes) of the pseudorandom key produced
109-
// by KDF.Extract.
158+
// by KDF.Extract() for a two-stage KDF, and the minimum output length
159+
// for full security for KDF.Derive() for a one-stage KDF.
110160
func (k KDF) ExtractSize() int {
111161
switch k {
112162
case KDF_HKDF_SHA256:
@@ -115,13 +165,19 @@ func (k KDF) ExtractSize() int {
115165
return crypto.SHA384.Size()
116166
case KDF_HKDF_SHA512:
117167
return crypto.SHA512.Size()
168+
case KDF_SHAKE128, KDF_TurboSHAKE128:
169+
return 32
170+
case KDF_SHAKE256, KDF_TurboSHAKE256:
171+
return 64
118172
default:
119173
panic(ErrInvalidKDF)
120174
}
121175
}
122176

123177
// Extract derives a pseudorandom key from a high-entropy, secret input and a
124178
// salt. The size of the output is determined by KDF.ExtractSize.
179+
//
180+
// Panics when called on a one-stage KDF.
125181
func (k KDF) Extract(secret, salt []byte) (pseudorandomKey []byte) {
126182
return hkdf.Extract(k.hash(), secret, salt)
127183
}
@@ -130,6 +186,8 @@ func (k KDF) Extract(secret, salt []byte) (pseudorandomKey []byte) {
130186
// and an information string. Panics if the pseudorandom key is less
131187
// than N bytes, or if the output length is greater than 255*N bytes,
132188
// where N is the size returned by KDF.Extract function.
189+
//
190+
// Panics when called on a one-stage KDF.
133191
func (k KDF) Expand(pseudorandomKey, info []byte, outputLen uint) []byte {
134192
extractSize := k.ExtractSize()
135193
if len(pseudorandomKey) < extractSize {
@@ -148,6 +206,27 @@ func (k KDF) Expand(pseudorandomKey, info []byte, outputLen uint) []byte {
148206
return output
149207
}
150208

209+
// Derive derives a variable-length pseudorandom string from a
210+
// high-entropy, secret input.
211+
//
212+
// Panics when called on a two-stage KDF.
213+
func (k KDF) Derive(ikm []byte, l uint) []byte {
214+
ret := make([]byte, l)
215+
switch k {
216+
case KDF_SHAKE128:
217+
sha3.ShakeSum128(ret, ikm)
218+
case KDF_SHAKE256:
219+
sha3.ShakeSum256(ret, ikm)
220+
case KDF_TurboSHAKE128:
221+
sha3.TurboShakeSum128(ret, ikm, 0x1f)
222+
case KDF_TurboSHAKE256:
223+
sha3.TurboShakeSum256(ret, ikm, 0x1f)
224+
default:
225+
panic(ErrInvalidKDF)
226+
}
227+
return ret
228+
}
229+
151230
func (k KDF) hash() func() hash.Hash {
152231
switch k {
153232
case KDF_HKDF_SHA256:
@@ -243,6 +322,9 @@ var (
243322
dhkemx25519hkdfsha256, dhkemx448hkdfsha512 xKEM
244323
hybridkemX25519Kyber768 hybridKEM
245324
kemXwing genericNoAuthKEM
325+
kemMLKEM512 genericNoAuthKEM
326+
kemMLKEM768 genericNoAuthKEM
327+
kemMLKEM1024 genericNoAuthKEM
246328
)
247329

248330
func init() {
@@ -284,4 +366,11 @@ func init() {
284366

285367
kemXwing.Scheme = xwing.Scheme()
286368
kemXwing.name = "HPKE_KEM_XWING"
369+
370+
kemMLKEM512.Scheme = mlkem512.Scheme()
371+
kemMLKEM512.name = "HPKE_KEM_MLKEM512"
372+
kemMLKEM768.Scheme = mlkem768.Scheme()
373+
kemMLKEM768.name = "HPKE_KEM_MLKEM768"
374+
kemMLKEM1024.Scheme = mlkem1024.Scheme()
375+
kemMLKEM1024.name = "HPKE_KEM_MLKEM1024"
287376
}

hpke/hpke.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"crypto/rand"
1818
"encoding"
1919
"errors"
20+
"fmt"
2021
"io"
2122

2223
"github.com/cloudflare/circl/kem"
@@ -216,7 +217,7 @@ func (s *Sender) allSetup(rnd io.Reader) ([]byte, Sealer, error) {
216217
seed := make([]byte, scheme.EncapsulationSeedSize())
217218
_, err := io.ReadFull(rnd, seed)
218219
if err != nil {
219-
return nil, nil, err
220+
return nil, nil, fmt.Errorf("encapsulation seed: %w", err)
220221
}
221222

222223
var enc, ss []byte

hpke/hybridkem.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (h hybridKEM) Decapsulate(skr kem.PrivateKey, ct []byte) ([]byte, error) {
6363
return nil, err
6464
}
6565

66-
ss := append(ssA, ssB...)
66+
ss := concat(ssA, ssB)
6767

6868
return ss, nil
6969
}
@@ -81,8 +81,8 @@ func (h hybridKEM) EncapsulateDeterministically(
8181
return nil, nil, err
8282
}
8383

84-
ct = append(encA, encB...)
85-
ss = append(ssA, ssB...)
84+
ct = concat(encA, encB)
85+
ss = concat(ssA, ssB)
8686

8787
return ct, ss, nil
8888
}
@@ -106,7 +106,7 @@ func (k *hybridKEMPrivKey) MarshalBinary() ([]byte, error) {
106106
if err != nil {
107107
return nil, err
108108
}
109-
return append(skA, skB...), nil
109+
return concat(skA, skB), nil
110110
}
111111

112112
func (k *hybridKEMPrivKey) Equal(sk kem.PrivateKey) bool {
@@ -143,7 +143,7 @@ func (k hybridKEMPubKey) MarshalBinary() ([]byte, error) {
143143
if err != nil {
144144
return nil, err
145145
}
146-
return append(pkA, pkB...), nil
146+
return concat(pkA, pkB), nil
147147
}
148148

149149
func (k *hybridKEMPubKey) Equal(pk kem.PublicKey) bool {

hpke/kembase.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ func (k kemBase) extractExpand(dh, kemCtx []byte) []byte {
5151

5252
func (k kemBase) labeledExtract(salt, label, info []byte) []byte {
5353
suiteID := k.getSuiteID()
54-
labeledIKM := append(append(append(append(
55-
make([]byte, 0, len(versionLabel)+len(suiteID)+len(label)+len(info)),
56-
versionLabel...),
57-
suiteID[:]...),
58-
label...),
59-
info...)
54+
labeledIKM := concat(
55+
[]byte(versionLabel),
56+
suiteID[:],
57+
label,
58+
info,
59+
)
6060
return hkdf.Extract(k.New, labeledIKM, salt)
6161
}
6262

@@ -68,11 +68,13 @@ func (k kemBase) labeledExpand(prk, label, info []byte, l uint16) []byte {
6868
2+len(versionLabel)+len(suiteID)+len(label)+len(info),
6969
)
7070
binary.BigEndian.PutUint16(labeledInfo[0:2], l)
71-
labeledInfo = append(append(append(append(labeledInfo,
72-
versionLabel...),
73-
suiteID[:]...),
74-
label...),
75-
info...)
71+
labeledInfo = concat(
72+
labeledInfo,
73+
[]byte(versionLabel),
74+
suiteID[:],
75+
label,
76+
info,
77+
)
7678
b := make([]byte, l)
7779
rd := hkdf.Expand(k.New, prk, labeledInfo)
7880
if _, err := io.ReadFull(rd, b); err != nil {
@@ -152,7 +154,7 @@ func (k dhKemBase) authEncap(
152154
if err != nil {
153155
return nil, nil, err
154156
}
155-
kemCtx = append(kemCtx, pkSm...)
157+
kemCtx = concat(kemCtx, pkSm)
156158

157159
ss = k.extractExpand(dh, kemCtx)
158160
return enc, ss, nil
@@ -177,7 +179,7 @@ func (k dhKemBase) coreEncap(
177179
if err != nil {
178180
return nil, nil, err
179181
}
180-
kemCtx = append(append([]byte{}, enc...), pkRm...)
182+
kemCtx = concat(enc, pkRm)
181183

182184
return enc, kemCtx, nil
183185
}
@@ -212,7 +214,7 @@ func (k dhKemBase) AuthDecapsulate(
212214
if err != nil {
213215
return nil, err
214216
}
215-
kemCtx = append(kemCtx, pkSm...)
217+
kemCtx = concat(kemCtx, pkSm)
216218
return k.extractExpand(dh, kemCtx), nil
217219
}
218220

@@ -237,5 +239,5 @@ func (k dhKemBase) coreDecap(
237239
return nil, err
238240
}
239241

240-
return append(append([]byte{}, ct...), pkRm...), nil
242+
return concat(ct, pkRm), nil
241243
}

hpke/marshal.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (c *sealContext) MarshalBinary() ([]byte, error) {
106106
if err != nil {
107107
return nil, err
108108
}
109-
return append([]byte{0}, rawContext...), nil
109+
return concat([]byte{0}, rawContext), nil
110110
}
111111

112112
// UnmarshalSealer parses an HPKE sealer.
@@ -134,7 +134,7 @@ func (c *openContext) MarshalBinary() ([]byte, error) {
134134
if err != nil {
135135
return nil, err
136136
}
137-
return append([]byte{1}, rawContext...), nil
137+
return concat([]byte{1}, rawContext), nil
138138
}
139139

140140
// UnmarshalOpener parses a serialized HPKE opener and returns the corresponding

0 commit comments

Comments
 (0)