Skip to content

Commit 6fd0964

Browse files
committed
More work in crypto.hpke
- Validate public key lengths and uncompressed format - Clamp XDH private key output - Switch to RawAgreement (add BasicRawAgreement adapter) - Avoid String.getBytes for label conversion
1 parent ee16879 commit 6fd0964

File tree

6 files changed

+211
-41
lines changed

6 files changed

+211
-41
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.bouncycastle.crypto.agreement;
2+
3+
import java.math.BigInteger;
4+
5+
import org.bouncycastle.crypto.BasicAgreement;
6+
import org.bouncycastle.crypto.CipherParameters;
7+
import org.bouncycastle.crypto.RawAgreement;
8+
import org.bouncycastle.util.BigIntegers;
9+
10+
public final class BasicRawAgreement
11+
implements RawAgreement
12+
{
13+
public final BasicAgreement basicAgreement;
14+
15+
public BasicRawAgreement(BasicAgreement basicAgreement)
16+
{
17+
if (basicAgreement == null)
18+
{
19+
throw new NullPointerException("'basicAgreement' cannot be null");
20+
}
21+
22+
this.basicAgreement = basicAgreement;
23+
}
24+
25+
public void init(CipherParameters parameters)
26+
{
27+
basicAgreement.init(parameters);
28+
}
29+
30+
public int getAgreementSize()
31+
{
32+
return basicAgreement.getFieldSize();
33+
}
34+
35+
public void calculateAgreement(CipherParameters publicKey, byte[] buf, int off)
36+
{
37+
BigInteger z = basicAgreement.calculateAgreement(publicKey);
38+
BigIntegers.asUnsignedByteArray(z, buf, off, getAgreementSize());
39+
}
40+
}

core/src/main/java/org/bouncycastle/crypto/hpke/DHKEM.java

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
77
import org.bouncycastle.crypto.AsymmetricCipherKeyPairGenerator;
8-
import org.bouncycastle.crypto.BasicAgreement;
98
import org.bouncycastle.crypto.CryptoServicesRegistrar;
9+
import org.bouncycastle.crypto.RawAgreement;
10+
import org.bouncycastle.crypto.agreement.BasicRawAgreement;
1011
import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement;
11-
import org.bouncycastle.crypto.agreement.XDHBasicAgreement;
12+
import org.bouncycastle.crypto.agreement.X25519Agreement;
13+
import org.bouncycastle.crypto.agreement.X448Agreement;
1214
import org.bouncycastle.crypto.ec.CustomNamedCurves;
1315
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
1416
import org.bouncycastle.crypto.generators.X25519KeyPairGenerator;
@@ -27,6 +29,8 @@
2729
import org.bouncycastle.math.ec.ECPoint;
2830
import org.bouncycastle.math.ec.FixedPointCombMultiplier;
2931
import org.bouncycastle.math.ec.WNafUtil;
32+
import org.bouncycastle.math.ec.rfc7748.X25519;
33+
import org.bouncycastle.math.ec.rfc7748.X448;
3034
import org.bouncycastle.util.Arrays;
3135
import org.bouncycastle.util.BigIntegers;
3236
import org.bouncycastle.util.Pack;
@@ -37,7 +41,7 @@ class DHKEM
3741
{
3842
private AsymmetricCipherKeyPairGenerator kpGen;
3943

40-
private BasicAgreement agreement;
44+
private RawAgreement rawAgreement;
4145

4246
// kem ids
4347
private final short kemId;
@@ -59,7 +63,7 @@ protected DHKEM(short kemid)
5963
case HPKE.kem_P256_SHA256:
6064
this.hkdf = new HKDF(HPKE.kdf_HKDF_SHA256);
6165
domainParams = getDomainParameters("P-256");
62-
this.agreement = new ECDHCBasicAgreement();
66+
rawAgreement = new BasicRawAgreement(new ECDHCBasicAgreement());
6367
bitmask = (byte)0xff;
6468
Nsk = 32;
6569
Nsecret = 32;
@@ -72,7 +76,7 @@ protected DHKEM(short kemid)
7276
case HPKE.kem_P384_SHA348:
7377
this.hkdf = new HKDF(HPKE.kdf_HKDF_SHA384);
7478
domainParams = getDomainParameters("P-384");
75-
this.agreement = new ECDHCBasicAgreement();
79+
rawAgreement = new BasicRawAgreement(new ECDHCBasicAgreement());
7680
bitmask = (byte)0xff;
7781
Nsk = 48;
7882
Nsecret = 48;
@@ -85,7 +89,7 @@ protected DHKEM(short kemid)
8589
case HPKE.kem_P521_SHA512:
8690
this.hkdf = new HKDF(HPKE.kdf_HKDF_SHA512);
8791
domainParams = getDomainParameters("P-521");
88-
this.agreement = new ECDHCBasicAgreement();
92+
rawAgreement = new BasicRawAgreement(new ECDHCBasicAgreement());
8993
bitmask = 0x01;
9094
Nsk = 66;
9195
Nsecret = 64;
@@ -97,7 +101,7 @@ protected DHKEM(short kemid)
97101
break;
98102
case HPKE.kem_X25519_SHA256:
99103
this.hkdf = new HKDF(HPKE.kdf_HKDF_SHA256);
100-
this.agreement = new XDHBasicAgreement();
104+
rawAgreement = new X25519Agreement();
101105
Nsecret = 32;
102106
Nsk = 32;
103107
Nenc = 32;
@@ -108,7 +112,7 @@ protected DHKEM(short kemid)
108112
break;
109113
case HPKE.kem_X448_SHA512:
110114
this.hkdf = new HKDF(HPKE.kdf_HKDF_SHA512);
111-
this.agreement = new XDHBasicAgreement();
115+
rawAgreement = new X448Agreement();
112116
Nsecret = 64;
113117
Nsk = 56;
114118
Nenc = 56;
@@ -129,6 +133,10 @@ public byte[] SerializePublicKey(AsymmetricKeyParameter key)
129133
case HPKE.kem_P256_SHA256:
130134
case HPKE.kem_P384_SHA348:
131135
case HPKE.kem_P521_SHA512:
136+
/*
137+
* RFC 9180 7.1.1. For P-256, P-384, and P-521, the SerializePublicKey() function of the KEM performs
138+
* the uncompressed Elliptic-Curve-Point-to-Octet-String conversion according to [SECG].
139+
*/
132140
return ((ECPublicKeyParameters)key).getQ().getEncoded(false);
133141
case HPKE.kem_X448_SHA512:
134142
return ((X448PublicKeyParameters)key).getEncoded();
@@ -146,30 +154,74 @@ public byte[] SerializePrivateKey(AsymmetricKeyParameter key)
146154
case HPKE.kem_P256_SHA256:
147155
case HPKE.kem_P384_SHA348:
148156
case HPKE.kem_P521_SHA512:
157+
{
158+
/*
159+
* RFC 9180 7.1.2. For P-256, P-384, and P-521, the SerializePrivateKey() function of the KEM
160+
* performs the Field-Element-to-Octet-String conversion according to [SECG].
161+
*/
149162
return BigIntegers.asUnsignedByteArray(Nsk, ((ECPrivateKeyParameters)key).getD());
163+
}
150164
case HPKE.kem_X448_SHA512:
151-
return ((X448PrivateKeyParameters)key).getEncoded();
165+
{
166+
/*
167+
* RFC 9180 7.1.2. For [..] X448 [..]. The SerializePrivateKey() function MUST clamp its output
168+
* [..].
169+
*
170+
* NOTE: Our X448 implementation clamps generated keys, but de-serialized keys are preserved as is
171+
* (clamping applied only during usage).
172+
*/
173+
byte[] encoded = ((X448PrivateKeyParameters)key).getEncoded();
174+
X448.clampPrivateKey(encoded);
175+
return encoded;
176+
}
152177
case HPKE.kem_X25519_SHA256:
153-
return ((X25519PrivateKeyParameters)key).getEncoded();
178+
{
179+
/*
180+
* RFC 9180 7.1.2. For X25519 [..]. The SerializePrivateKey() function MUST clamp its output [..].
181+
*
182+
* NOTE: Our X25519 implementation clamps generated keys, but de-serialized keys are preserved as
183+
* is (clamping applied only during usage).
184+
*/
185+
byte[] encoded = ((X25519PrivateKeyParameters)key).getEncoded();
186+
X25519.clampPrivateKey(encoded);
187+
return encoded;
188+
}
154189
default:
155190
throw new IllegalStateException("invalid kem id");
156191
}
157192
}
158193

159-
public AsymmetricKeyParameter DeserializePublicKey(byte[] encoded)
194+
public AsymmetricKeyParameter DeserializePublicKey(byte[] pkEncoded)
160195
{
196+
if (pkEncoded == null)
197+
{
198+
throw new NullPointerException("'pkEncoded' cannot be null");
199+
}
200+
if (pkEncoded.length != Nenc)
201+
{
202+
throw new IllegalArgumentException("'pkEncoded' has invalid length");
203+
}
204+
161205
switch (kemId)
162206
{
163207
case HPKE.kem_P256_SHA256:
164208
case HPKE.kem_P384_SHA348:
165209
case HPKE.kem_P521_SHA512:
166-
// TODO Does the encoding have to be uncompressed? (i.e. encoded.length MUST be Nenc?)
167-
ECPoint G = domainParams.getCurve().decodePoint(encoded);
210+
/*
211+
* RFC 9180 7.1.1. For P-256, P-384, and P-521 [..]. DeserializePublicKey() performs the
212+
* uncompressed Octet-String-to-Elliptic-Curve-Point conversion.
213+
*/
214+
if (pkEncoded[0] != 0x04) // "0x04" is the marker for an uncompressed encoding
215+
{
216+
throw new IllegalArgumentException("'pkEncoded' has invalid format");
217+
}
218+
219+
ECPoint G = domainParams.getCurve().decodePoint(pkEncoded);
168220
return new ECPublicKeyParameters(G, domainParams);
169221
case HPKE.kem_X448_SHA512:
170-
return new X448PublicKeyParameters(encoded);
222+
return new X448PublicKeyParameters(pkEncoded);
171223
case HPKE.kem_X25519_SHA256:
172-
return new X25519PublicKeyParameters(encoded);
224+
return new X25519PublicKeyParameters(pkEncoded);
173225
default:
174226
throw new IllegalStateException("invalid kem id");
175227
}
@@ -198,6 +250,10 @@ public AsymmetricCipherKeyPair DeserializePrivateKey(byte[] skEncoded, byte[] pk
198250
case HPKE.kem_P256_SHA256:
199251
case HPKE.kem_P384_SHA348:
200252
case HPKE.kem_P521_SHA512:
253+
/*
254+
* RFC 9180 7.1.2. For P-256, P-384, and P-521 [..]. DeserializePrivateKey() performs the Octet-
255+
* String-to-Field-Element conversion according to [SECG].
256+
*/
201257
BigInteger d = new BigInteger(1, skEncoded);
202258
ECPrivateKeyParameters ec = new ECPrivateKeyParameters(d, domainParams);
203259

@@ -317,7 +373,7 @@ protected byte[][] Encap(AsymmetricKeyParameter pkR, AsymmetricCipherKeyPair kpE
317373
byte[][] output = new byte[2][];
318374

319375
// DH
320-
byte[] secret = calculateAgreement(agreement, kpE.getPrivate(), pkR);
376+
byte[] secret = calculateRawAgreement(rawAgreement, kpE.getPrivate(), pkR);
321377

322378
byte[] enc = SerializePublicKey(kpE.getPublic());
323379
byte[] pkRm = SerializePublicKey(pkR);
@@ -335,7 +391,7 @@ protected byte[] Decap(byte[] enc, AsymmetricCipherKeyPair kpR)
335391
AsymmetricKeyParameter pkE = DeserializePublicKey(enc);
336392

337393
// DH
338-
byte[] secret = calculateAgreement(agreement, kpR.getPrivate(), pkE);
394+
byte[] secret = calculateRawAgreement(rawAgreement, kpR.getPrivate(), pkE);
339395

340396
byte[] pkRm = SerializePublicKey(kpR.getPublic());
341397
byte[] KEMContext = Arrays.concatenate(enc, pkRm);
@@ -350,12 +406,22 @@ protected byte[][] AuthEncap(AsymmetricKeyParameter pkR, AsymmetricCipherKeyPair
350406
AsymmetricCipherKeyPair kpE = kpGen.generateKeyPair(); // todo: can be replaced with deriveKeyPair(random)
351407

352408
// DH(skE, pkR)
353-
byte[] secret1 = calculateAgreement(agreement, kpE.getPrivate(), pkR);
409+
rawAgreement.init(kpE.getPrivate());
410+
int agreementSize = rawAgreement.getAgreementSize();
411+
412+
byte[] secret = new byte[agreementSize * 2];
413+
414+
rawAgreement.calculateAgreement(pkR, secret, 0);
354415

355416
// DH(skS, pkR)
356-
byte[] secret2 = calculateAgreement(agreement, kpS.getPrivate(), pkR);
417+
rawAgreement.init(kpS.getPrivate());
418+
if (agreementSize != rawAgreement.getAgreementSize())
419+
{
420+
throw new IllegalStateException();
421+
}
422+
423+
rawAgreement.calculateAgreement(pkR, secret, agreementSize);
357424

358-
byte[] secret = Arrays.concatenate(secret1, secret2);
359425
byte[] enc = SerializePublicKey(kpE.getPublic());
360426

361427
byte[] pkRm = SerializePublicKey(pkR);
@@ -373,13 +439,16 @@ protected byte[] AuthDecap(byte[] enc, AsymmetricCipherKeyPair kpR, AsymmetricKe
373439
{
374440
AsymmetricKeyParameter pkE = DeserializePublicKey(enc);
375441

442+
rawAgreement.init(kpR.getPrivate());
443+
444+
int agreementSize = rawAgreement.getAgreementSize();
445+
byte[] secret = new byte[agreementSize * 2];
446+
376447
// DH(skR, pkE)
377-
byte[] secret1 = calculateAgreement(agreement, kpR.getPrivate(), pkE);
448+
rawAgreement.calculateAgreement(pkE, secret, 0);
378449

379450
// DH(skR, pkS)
380-
byte[] secret2 = calculateAgreement(agreement, kpR.getPrivate(), pkS);
381-
382-
byte[] secret = Arrays.concatenate(secret1, secret2);
451+
rawAgreement.calculateAgreement(pkS, secret, agreementSize);
383452

384453
byte[] pkRm = SerializePublicKey(kpR.getPublic());
385454
byte[] pkSm = SerializePublicKey(pkS);
@@ -397,12 +466,13 @@ private byte[] ExtractAndExpand(byte[] dh, byte[] kemContext)
397466
return hkdf.LabeledExpand(eae_prk, suiteID, "shared_secret", kemContext, Nsecret);
398467
}
399468

400-
private static byte[] calculateAgreement(BasicAgreement agreement, AsymmetricKeyParameter privateKey,
469+
private static byte[] calculateRawAgreement(RawAgreement rawAgreement, AsymmetricKeyParameter privateKey,
401470
AsymmetricKeyParameter publicKey)
402471
{
403-
agreement.init(privateKey);
404-
BigInteger z = agreement.calculateAgreement(publicKey);
405-
return BigIntegers.asUnsignedByteArray(agreement.getFieldSize(), z);
472+
rawAgreement.init(privateKey);
473+
byte[] z = new byte[rawAgreement.getAgreementSize()];
474+
rawAgreement.calculateAgreement(publicKey, z, 0);
475+
return z;
406476
}
407477

408478
private static ECDomainParameters getDomainParameters(String curveName)

core/src/main/java/org/bouncycastle/crypto/hpke/HKDF.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
import org.bouncycastle.crypto.params.HKDFParameters;
99
import org.bouncycastle.util.Arrays;
1010
import org.bouncycastle.util.Pack;
11+
import org.bouncycastle.util.Strings;
1112

1213
class HKDF
1314
{
14-
private final static String versionLabel = "HPKE-v1";
15+
private final static byte[] VERSION_LABEL = getBytes("HPKE-v1");
1516
private final HKDFBytesGenerator kdf;
1617
private final int hashLength;
1718

@@ -50,7 +51,7 @@ protected byte[] LabeledExtract(byte[] salt, byte[] suiteID, String label, byte[
5051
salt = new byte[hashLength];
5152
}
5253

53-
byte[] labeledIKM = Arrays.concatenate(versionLabel.getBytes(), suiteID, label.getBytes(), ikm);
54+
byte[] labeledIKM = Arrays.concatenate(VERSION_LABEL, suiteID, getBytes(label), ikm);
5455

5556
return kdf.extractPRK(salt, labeledIKM);
5657
}
@@ -61,7 +62,9 @@ protected byte[] LabeledExpand(byte[] prk, byte[] suiteID, String label, byte[]
6162
{
6263
throw new IllegalArgumentException("Expand length cannot be larger than 2^16");
6364
}
64-
byte[] labeledInfo = Arrays.concatenate(Pack.shortToBigEndian((short)L), versionLabel.getBytes(), suiteID, label.getBytes());
65+
66+
byte[] labeledInfo = Arrays.concatenate(Pack.shortToBigEndian((short)L), VERSION_LABEL, suiteID,
67+
getBytes(label));
6568

6669
kdf.init(HKDFParameters.skipExtractParameters(prk, Arrays.concatenate(labeledInfo, info)));
6770

@@ -97,4 +100,14 @@ protected byte[] Expand(byte[] prk, byte[] info, int L)
97100

98101
return rv;
99102
}
103+
104+
private static byte[] getBytes(String label)
105+
{
106+
/*
107+
* RFC 9180 seems silent about this conversion, but all given labels are ASCII anyway.
108+
*
109+
* NOTE: String#getBytes not reliable because it depends on the platform's default charset.
110+
*/
111+
return Strings.toByteArray(label);
112+
}
100113
}

core/src/main/java/org/bouncycastle/math/ec/rfc7748/X25519.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ public static boolean calculateAgreement(byte[] k, int kOff, byte[] u, int uOff,
3030
return !Arrays.areAllZeroes(r, rOff, POINT_SIZE);
3131
}
3232

33+
public static void clampPrivateKey(byte[] k)
34+
{
35+
if (k.length != SCALAR_SIZE)
36+
{
37+
throw new IllegalArgumentException("k");
38+
}
39+
40+
k[0] &= 0xF8;
41+
k[SCALAR_SIZE - 1] &= 0x7F;
42+
k[SCALAR_SIZE - 1] |= 0x40;
43+
}
44+
3345
private static int decode32(byte[] bs, int off)
3446
{
3547
int n = bs[off] & 0xFF;
@@ -60,9 +72,7 @@ public static void generatePrivateKey(SecureRandom random, byte[] k)
6072

6173
random.nextBytes(k);
6274

63-
k[0] &= 0xF8;
64-
k[SCALAR_SIZE - 1] &= 0x7F;
65-
k[SCALAR_SIZE - 1] |= 0x40;
75+
clampPrivateKey(k);
6676
}
6777

6878
public static void generatePublicKey(byte[] k, int kOff, byte[] r, int rOff)

0 commit comments

Comments
 (0)