Skip to content

Commit 05958c2

Browse files
committed
The X25519 and X448 KeyAgreements now take the unified agreement and curve binding from their own algorithm name rather than the XDH name reported under org.bouncycastle.emulate.oracle, which only changes exception messages.
1 parent 451ca8b commit 05958c2

3 files changed

Lines changed: 101 additions & 6 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Date: 2026, TBD
7070

7171
- The BCPQC MAYO, SDitH, SNOVA and NTRU+ KeyPairGenerators refused every java.security.spec.NamedParameterSpec with InvalidAlgorithmParameterException, whatever parameter set it named and in whatever case: they lower-cased the name and then looked it up in a table of upper-case names. The name is now upper-cased, so NamedParameterSpec("MAYO-1") and NamedParameterSpec("mayo-1") both select MAYO-1, as the other BCPQC families already allowed. The generators' own ParameterSpec classes were never affected.
7272

73+
- With org.bouncycastle.emulate.oracle set, the X25519 and X448 KeyAgreements took the name they report under it, XDH, for the algorithm itself: the unified agreements (X25519UwithSHA256KDF, X448UwithSHA512KDF and their CKDF forms) refused a DHUParameterSpec and ran as plain static agreements without one, and the curve specific agreements accepted a key on the other curve. The property now affects only the name used in exception messages.
74+
7375
### 2.1.3 Additional Features and Functionality
7476

7577
- The CRMF certificate request message controls now include the RFC 4211 sec. 6.6 protocolEncrKey control, which names the key a CA is to encrypt its response with: org.bouncycastle.cert.crmf.ProtocolEncrKeyControl carries the SubjectPublicKeyInfo the control is defined to take, and CertificateRequestMessage.getControl() recognises id-regCtrl-protocolEncrKey alongside the regToken, authenticator and pkiArchiveOptions controls it already returned (github PR #2443).

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/edec/KeyAgreementSpi.java‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,25 @@
3030
public class KeyAgreementSpi
3131
extends BaseAgreementSpi
3232
{
33+
// taken from the real algorithm name: under EMULATE_ORACLE kaAlgorithm is "XDH" and is only for messages.
34+
private final boolean isUnified;
35+
private final String curve;
36+
3337
private RawAgreement agreement;
3438
private DHUParameterSpec dhuSpec;
3539
private byte[] result;
3640

3741
KeyAgreementSpi(String algorithm)
3842
{
39-
super(Properties.isOverrideSet(Properties.EMULATE_ORACLE) ? "XDH" : algorithm, null);
43+
this(algorithm, null);
4044
}
4145

4246
KeyAgreementSpi(String algorithm, DerivationFunction kdf)
4347
{
4448
super(Properties.isOverrideSet(Properties.EMULATE_ORACLE) ? "XDH" : algorithm, kdf);
49+
50+
this.isUnified = algorithm.indexOf('U') >= 0;
51+
this.curve = algorithm.startsWith("XDH") ? null : (algorithm.startsWith("X448") ? "X448" : "X25519");
4552
}
4653

4754
protected byte[] doCalcSecret()
@@ -77,7 +84,7 @@ else if (priv instanceof X448PrivateKeyParameters)
7784
ukmParametersSalt = null;
7885
if (params instanceof DHUParameterSpec)
7986
{
80-
if (kaAlgorithm.indexOf('U') < 0)
87+
if (!isUnified)
8188
{
8289
throw new InvalidAlgorithmParameterException("agreement algorithm not DHU based");
8390
}
@@ -92,7 +99,7 @@ else if (priv instanceof X448PrivateKeyParameters)
9299
}
93100
else if (params != null)
94101
{
95-
if (kaAlgorithm.indexOf('U') >= 0)
102+
if (isUnified)
96103
{
97104
throw new InvalidAlgorithmParameterException(kaAlgorithm + " key agreement requires DHUParameterSpec for initialisation");
98105
}
@@ -116,7 +123,7 @@ else if (params != null)
116123
}
117124
else
118125
{
119-
if (kaAlgorithm.indexOf('U') >= 0)
126+
if (isUnified)
120127
{
121128
throw new InvalidAlgorithmParameterException(kaAlgorithm + " key agreement requires DHUParameterSpec for initialisation");
122129
}
@@ -165,12 +172,12 @@ protected Key engineDoPhase(Key key, boolean lastPhase)
165172
private RawAgreement getAgreement(String alg)
166173
throws InvalidKeyException
167174
{
168-
if (!(kaAlgorithm.equals("XDH") || kaAlgorithm.startsWith(alg)))
175+
if (!(curve == null || curve.equals(alg)))
169176
{
170177
throw new InvalidKeyException("inappropriate key for " + kaAlgorithm);
171178
}
172179

173-
if (kaAlgorithm.indexOf('U') > 0)
180+
if (isUnified)
174181
{
175182
if (alg.startsWith("X448"))
176183
{

‎prov/src/test/java/org/bouncycastle/jce/provider/test/KeyAgreementInitParametersTest.java‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.bouncycastle.jce.provider.BouncyCastleProvider;
1919
import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec;
2020
import org.bouncycastle.util.Arrays;
21+
import org.bouncycastle.util.Properties;
2122
import org.bouncycastle.util.test.SimpleTest;
2223

2324
/**
@@ -84,6 +85,91 @@ public void performTest()
8485
checkAcceptsSalt("ECDHwithSHA256HKDF", ecKey);
8586

8687
checkNoUkm(ecKey, xKey);
88+
89+
checkEmulateOracle(xKey, generate("X448", null));
90+
}
91+
92+
/**
93+
* Properties.EMULATE_ORACLE makes the XDH agreements report themselves as "XDH", and the SPI
94+
* used to take that name for the algorithm too: the unified agreements lost the DHU requirement
95+
* (refusing a DHUParameterSpec, running a plain agreement without one) and the curve specific
96+
* ones accepted a key on the other curve. The name is only for messages.
97+
*/
98+
private void checkEmulateOracle(KeyPair xKey, KeyPair x448Key)
99+
throws Exception
100+
{
101+
Properties.setThreadOverride(Properties.EMULATE_ORACLE, true);
102+
try
103+
{
104+
checkRejectsPlainUKM("X25519UwithSHA256KDF", xKey);
105+
checkRejectsPlainUKM("X448UwithSHA512KDF", x448Key);
106+
checkRejectsNoSpec("X25519UwithSHA256KDF", xKey);
107+
checkRejectsNoSpec("X448UwithSHA512KDF", x448Key);
108+
109+
checkRejectsOtherCurve("X25519", x448Key);
110+
checkRejectsOtherCurve("X25519withSHA256KDF", x448Key);
111+
checkRejectsOtherCurve("X448", xKey);
112+
checkRejectsOtherCurve("X448withSHA512KDF", xKey);
113+
114+
checkUnifiedAgreement("X25519UwithSHA256KDF", xKey, generate("X25519", null));
115+
checkUnifiedAgreement("X448UwithSHA512KDF", x448Key, generate("X448", null));
116+
117+
// the XDH names are not tied to a curve
118+
KeyAgreement agreement = agreement("XDH");
119+
if (agreement != null)
120+
{
121+
agreement.init(xKey.getPrivate());
122+
agreement.init(x448Key.getPrivate());
123+
}
124+
}
125+
finally
126+
{
127+
Properties.removeThreadOverride(Properties.EMULATE_ORACLE);
128+
}
129+
}
130+
131+
private void checkRejectsOtherCurve(String algorithm, KeyPair kp)
132+
throws Exception
133+
{
134+
KeyAgreement agreement = agreement(algorithm);
135+
if (agreement == null)
136+
{
137+
return;
138+
}
139+
140+
try
141+
{
142+
agreement.init(kp.getPrivate());
143+
144+
fail(algorithm + " accepted a key on the other curve");
145+
}
146+
catch (InvalidKeyException e)
147+
{
148+
// expected
149+
}
150+
}
151+
152+
private void checkUnifiedAgreement(String algorithm, KeyPair kp, KeyPair other)
153+
throws Exception
154+
{
155+
String keyAlg = algorithm.startsWith("X448") ? "X448" : "X25519";
156+
KeyPair ephemeral = generate(keyAlg, null);
157+
KeyPair otherEphemeral = generate(keyAlg, null);
158+
159+
KeyAgreement a = agreement(algorithm);
160+
KeyAgreement b = agreement(algorithm);
161+
if (a == null)
162+
{
163+
return;
164+
}
165+
166+
a.init(kp.getPrivate(), new DHUParameterSpec(ephemeral, otherEphemeral.getPublic(), UKM));
167+
b.init(other.getPrivate(), new DHUParameterSpec(otherEphemeral, ephemeral.getPublic(), UKM));
168+
a.doPhase(other.getPublic(), true);
169+
b.doPhase(kp.getPublic(), true);
170+
171+
isTrue(algorithm + " unified agreement did not agree",
172+
Arrays.areEqual(a.generateSecret("AES[256]").getEncoded(), b.generateSecret("AES[256]").getEncoded()));
87173
}
88174

89175
/**

0 commit comments

Comments
 (0)