Skip to content

Commit 69fd0df

Browse files
committed
Allow the ML-DSA and SLH-DSA signature context to be set before initSign/initVerify as well as after, rather than dereferencing the absent key, relates to github #2396.
1 parent 2204472 commit 69fd0df

4 files changed

Lines changed: 268 additions & 7 deletions

File tree

‎docs/releasenotes.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ <h3>2.1.2 Defects Fixed</h3>
121121
<li>The JCA/JCE provider classes for the pre-standardisation SPHINCS+ and Kyber have been removed from the BCPQC provider hierarchy: org.bouncycastle.pqc.jcajce.provider.sphincsplus and .kyber, their SPHINCSPlus and Kyber Mappings classes, and the unused org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus mappings alongside them. Neither was listed in BouncyCastlePQCProvider's algorithm set or in BouncyCastleProvider's, so none of the services they described - "SPHINCS+-SHA2-128S", the BCPQC-side "ML-KEM-512", and the rest - had been obtainable through either provider; they were superseded by SLH-DSA and ML-KEM in the BC provider, which are registered and are what callers should use. The corresponding exports have been dropped from both module-info descriptors. The lightweight implementations are untouched, as is the unrelated org.bouncycastle.pqc.legacy.sphincsplus package.</li>
122122
<li>The parameter-set specific HashML-DSA Signature services in the BC provider refused a pure ML-DSA key of their own parameter set, so a caller who obtained a key as "ML-DSA-65" and a signature as "ML-DSA-65-WITH-SHA512" was met with InvalidKeyException("signature configured for ML-DSA-65-WITH-SHA512"). FIPS 204 sec. 5 defines one key generation algorithm per parameter set and the keys it produces carry no commitment to the pure mode over the pre-hash one, so the pure key was a perfectly good HashML-DSA key; the SPIs were comparing the key's algorithm name against their own, which differ by the "-WITH-SHA512" suffix. It particularly bit certificate-sourced keys, which carry the pure OID (RFC 9881), and the provider was already inconsistent about it - the unparameterised "HASH-ML-DSA" and "HASH-ML-DSA-EXTERNAL-HASH" services accepted pure keys all along. The comparison is now against the parameter set rather than the name, so a pure key is accepted wherever its parameter set matches. Signatures are unchanged: HashMLDSASigner derives the same SHA-512 pre-hash for a pure and a pre-hash key of the same parameter set, so the bytes produced are identical either way. <b>The tolerance is one way only</b> - a key that names a HashML-DSA parameter set has been narrowed to the pre-hash mode and is still refused by the pure ML-DSA services - and a key of any other parameter set is still refused in both directions (github #2397).</li>
123123
<li>The parameter-set specific SLH-DSA Signature services were not bound to their parameter set at all: every one of the twenty four algorithm names, and their OIDs, was registered as an alias of the unparameterised "SLH-DSA" or "HASH-SLH-DSA" service, so the name a caller asked for had no effect on which keys were accepted. Signature.getInstance("SLH-DSA-SHAKE-256S") would sign quite happily with an SLH-DSA-SHA2-128F key, producing a SHA2-128F signature, and likewise across every other pair. This matters to a caller using the algorithm name as a policy gate - a service that means to sign or verify only at a chosen parameter set - which had no way to tell that the name was being ignored. Each name and OID is now registered against an SPI bound to its own parameter set, applying the same rule described for ML-DSA above: a pure key is accepted by the pre-hash service of its own parameter set, a pre-hash key is refused by the pure service, and a key of any other parameter set is refused either way. <b>This is a behavioural change for a caller that relied on a parameter-set named SLH-DSA Signature accepting a key of a different parameter set</b>, which now raises InvalidKeyException; the unparameterised "SLH-DSA" and "HASH-SLH-DSA" services name no parameter set and are unchanged, so they remain the way to work with a key whose parameter set is not known in advance. Note also that the pre-hash OID table was in a different order from the algorithm names it is now index-matched against, which is corrected here - while everything aliased to a single service the ordering could not be observed.</li>
124+
<li>Signature.setParameter(...) on any of the ML-DSA or SLH-DSA services threw NullPointerException when called before initSign / initVerify, out of a method declared to throw InvalidAlgorithmParameterException. The context carried by a ContextParameterSpec is applied by re-initialising the underlying signer with the key the Signature holds, and BaseDeterministicOrRandomSignature.engineSetParameter went straight to that re-initialisation without checking there was a key to re-initialise with. The context may now be set either side of initialisation, as the RSASSA-PSS parameters may be: setting it on a signature that already has a key re-initialises the signer at once as before, and setting it beforehand records it to be applied when the key arrives. Initialising no longer discards a context set beforehand, which it previously did by resetting the context to empty - so a caller who set one first and would have had it silently dropped now gets the signature they asked for. Setting a context after initialisation, which is what the existing tests and examples do, produces exactly the signatures it did before, and setting one in the middle of an update is still refused with a ProviderException. Note the context now also survives a subsequent re-initialisation of the same Signature object, again as the RSASSA-PSS parameters do (github #2396).</li>
124125
<li>An OCSP response carrying no nextUpdate could be cached and reused as though it stated a validity interval, so a response could go on answering for a certificate after the responder had newer information about it - after a revocation, in particular. Any such reuse was bounded only by garbage collection rather than by an interval: OcspCache holds each responder's response map through a WeakReference, itself in a WeakHashMap, and nothing else refers to that map once the call returns, so entries last until the next collection. That is unpredictable rather than long - short on a busy JVM, potentially much longer on a large heap that collects rarely - and it is not a window the responder or the caller had any say in. RFC 6960 sec. 4.2.2.1 says the opposite of what that assumes: "if nextUpdate is not set, the responder is indicating that newer revocation information is available all the time", which is a statement that there is no interval to reuse the response over, not that it never expires. OcspCache now separates the two questions it had been asking with one method: a response arriving from the responder is accepted as before, whether or not it states a nextUpdate, while only a response that states one may be served from the cache afterwards. Nothing is rejected that was previously accepted - a responder that omits nextUpdate simply costs another request per validation, which is what "available all the time" asks for. Additionally, both the cached and the caller-supplied (stapled) paths now apply RFC 6960 sec. 4.2.2.1's other freshness rule, "responses whose thisUpdate time is later than the local system time SHOULD be considered unreliable", which neither had checked: a response dated ahead of the time being validated for by more than a 15 minute clock-skew allowance is treated as unreliable, raising "OCSP response not yet valid" on the stapled path.</li>
125126
</ul>
126127

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/util/BaseDeterministicOrRandomSignature.java‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ final protected void engineInitVerify(PublicKey publicKey)
4242
throws InvalidKeyException
4343
{
4444
verifyInit(publicKey);
45-
paramSpec = ContextParameterSpec.EMPTY_CONTEXT_SPEC;
4645
isInitState = true;
4746
reInit();
4847
}
@@ -54,7 +53,6 @@ final protected void engineInitSign(
5453
throws InvalidKeyException
5554
{
5655
signInit(privateKey, null);
57-
paramSpec = ContextParameterSpec.EMPTY_CONTEXT_SPEC;
5856
isInitState = true;
5957
reInit();
6058
}
@@ -65,7 +63,6 @@ final protected void engineInitSign(
6563
throws InvalidKeyException
6664
{
6765
signInit(privateKey, random);
68-
paramSpec = ContextParameterSpec.EMPTY_CONTEXT_SPEC;
6966
isInitState = true;
7067
reInit();
7168
}
@@ -95,6 +92,17 @@ final protected void engineUpdate(
9592

9693
protected abstract void updateEngine(byte[] buf, int off, int len) throws SignatureException;
9794

95+
/**
96+
* Set the signature context, either before or after engineInitSign / engineInitVerify.
97+
* <p>
98+
* Where the signature has a key already the underlying signer is re-initialised with the new
99+
* context at once; where it does not the context is recorded and applied when the key arrives,
100+
* as the RSASSA-PSS implementation does with its own parameters. Calling this first used to let
101+
* a NullPointerException out of a method declared to throw InvalidAlgorithmParameterException,
102+
* from dereferencing the key that was not there yet (see
103+
* <a href="https://github.com/bcgit/bc-java/issues/2396">github #2396</a>).
104+
* </p>
105+
*/
98106
protected void engineSetParameter(
99107
AlgorithmParameterSpec params)
100108
throws InvalidAlgorithmParameterException
@@ -118,16 +126,14 @@ protected void engineSetParameter(
118126

119127
if (params instanceof ContextParameterSpec)
120128
{
121-
this.paramSpec = (ContextParameterSpec)params;
122-
reInit();
129+
setContext((ContextParameterSpec)params);
123130
}
124131
else
125132
{
126133
byte[] context = SpecUtil.getContextFrom(params);
127134
if (context != null)
128135
{
129-
this.paramSpec = new ContextParameterSpec(context);
130-
reInit();
136+
setContext(new ContextParameterSpec(context));
131137
}
132138
else
133139
{
@@ -136,6 +142,17 @@ protected void engineSetParameter(
136142
}
137143
}
138144

145+
private void setContext(ContextParameterSpec context)
146+
{
147+
this.paramSpec = context;
148+
this.engineParams = null;
149+
150+
if (keyParams != null)
151+
{
152+
reInit();
153+
}
154+
}
155+
139156
private void reInit()
140157
{
141158
CipherParameters param = keyParams;

‎prov/src/test/java/org/bouncycastle/pqc/jcajce/provider/test/AllTests.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static Test suite()
6969
suite.addTestSuite(NamedKeyPairGeneratorTest.class);
7070
suite.addTestSuite(NamedKeyFactoryTest.class);
7171
suite.addTestSuite(PreHashKeyInteropTest.class);
72+
suite.addTestSuite(SignatureSetParameterTest.class);
7273
suite.addTestSuite(MayoKeyPairGeneratorTest.class);
7374
suite.addTestSuite(MayoTest.class);
7475
suite.addTestSuite(SnovaTest.class);
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
package org.bouncycastle.pqc.jcajce.provider.test;
2+
3+
import java.security.AlgorithmParameters;
4+
import java.security.KeyPair;
5+
import java.security.KeyPairGenerator;
6+
import java.security.ProviderException;
7+
import java.security.Security;
8+
import java.security.Signature;
9+
10+
import junit.framework.TestCase;
11+
import org.bouncycastle.jcajce.spec.ContextParameterSpec;
12+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
13+
import org.bouncycastle.util.Arrays;
14+
import org.bouncycastle.util.Strings;
15+
16+
/**
17+
* Signature.setParameter(...) on the ML-DSA and SLH-DSA services, which take a signature context
18+
* through a {@link ContextParameterSpec}.
19+
* <p>
20+
* The context may be set either side of initSign / initVerify, as it may be for the RSASSA-PSS
21+
* parameters: setting it while the signature already has a key re-initialises the underlying signer
22+
* at once, and setting it beforehand records it for the key to arrive. Setting it first used to let
23+
* a NullPointerException out of a method declared to throw InvalidAlgorithmParameterException,
24+
* from dereferencing the key that was not there yet (github #2396).
25+
*/
26+
public class SignatureSetParameterTest
27+
extends TestCase
28+
{
29+
private static final byte[] MSG = Strings.toByteArray("the quick brown fox");
30+
private static final byte[] CONTEXT = Strings.toByteArray("Hello, world!");
31+
32+
private static final String[] SIGNATURE_ALGORITHMS =
33+
{
34+
"ML-DSA",
35+
"ML-DSA-65",
36+
"ML-DSA-65-WITH-SHA512",
37+
"HASH-ML-DSA",
38+
"SLH-DSA",
39+
"SLH-DSA-SHA2-128F",
40+
"SLH-DSA-SHA2-128F-WITH-SHA256",
41+
"HASH-SLH-DSA"
42+
};
43+
44+
private static final String[] KEY_ALGORITHMS =
45+
{
46+
"ML-DSA-65",
47+
"ML-DSA-65",
48+
"ML-DSA-65",
49+
"ML-DSA-65",
50+
"SLH-DSA-SHA2-128F",
51+
"SLH-DSA-SHA2-128F",
52+
"SLH-DSA-SHA2-128F",
53+
"SLH-DSA-SHA2-128F"
54+
};
55+
56+
public void setUp()
57+
{
58+
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null)
59+
{
60+
Security.addProvider(new BouncyCastleProvider());
61+
}
62+
}
63+
64+
/**
65+
* Setting the context before initialising has to reach the signer, not be dropped on the way -
66+
* so the signature it produces must be the one the established after-init order produces, and
67+
* must not verify against a signature service given no context at all.
68+
*/
69+
public void testSetParameterBeforeInit()
70+
throws Exception
71+
{
72+
for (int i = 0; i != SIGNATURE_ALGORITHMS.length; i++)
73+
{
74+
String algorithm = SIGNATURE_ALGORITHMS[i];
75+
76+
KeyPair kp = KeyPairGenerator.getInstance(KEY_ALGORITHMS[i], "BC").generateKeyPair();
77+
78+
Signature before = Signature.getInstance(algorithm, "BC");
79+
80+
before.setParameter(new ContextParameterSpec(CONTEXT));
81+
before.initSign(kp.getPrivate());
82+
before.update(MSG);
83+
84+
byte[] sigBefore = before.sign();
85+
86+
Signature after = Signature.getInstance(algorithm, "BC");
87+
88+
after.initSign(kp.getPrivate());
89+
after.setParameter(new ContextParameterSpec(CONTEXT));
90+
after.update(MSG);
91+
92+
assertTrue(algorithm + ": context set before init did not reach the signer",
93+
Arrays.areEqual(sigBefore, after.sign()));
94+
95+
Signature verifier = Signature.getInstance(algorithm, "BC");
96+
97+
verifier.setParameter(new ContextParameterSpec(CONTEXT));
98+
verifier.initVerify(kp.getPublic());
99+
verifier.update(MSG);
100+
101+
assertTrue(algorithm, verifier.verify(sigBefore));
102+
103+
Signature noContext = Signature.getInstance(algorithm, "BC");
104+
105+
noContext.initVerify(kp.getPublic());
106+
noContext.update(MSG);
107+
108+
assertFalse(algorithm + ": verified without the context", noContext.verify(sigBefore));
109+
}
110+
}
111+
112+
public void testSetParameterAfterInit()
113+
throws Exception
114+
{
115+
for (int i = 0; i != SIGNATURE_ALGORITHMS.length; i++)
116+
{
117+
String algorithm = SIGNATURE_ALGORITHMS[i];
118+
119+
KeyPair kp = KeyPairGenerator.getInstance(KEY_ALGORITHMS[i], "BC").generateKeyPair();
120+
121+
Signature signer = Signature.getInstance(algorithm, "BC");
122+
123+
signer.initSign(kp.getPrivate());
124+
signer.setParameter(new ContextParameterSpec(CONTEXT));
125+
signer.update(MSG);
126+
127+
byte[] sig = signer.sign();
128+
129+
Signature verifier = Signature.getInstance(algorithm, "BC");
130+
131+
verifier.initVerify(kp.getPublic());
132+
verifier.setParameter(new ContextParameterSpec(CONTEXT));
133+
verifier.update(MSG);
134+
135+
assertTrue(algorithm, verifier.verify(sig));
136+
137+
Signature wrongContext = Signature.getInstance(algorithm, "BC");
138+
139+
wrongContext.initVerify(kp.getPublic());
140+
wrongContext.setParameter(new ContextParameterSpec(Strings.toByteArray("other context")));
141+
wrongContext.update(MSG);
142+
143+
assertFalse(algorithm, wrongContext.verify(sig));
144+
}
145+
}
146+
147+
/**
148+
* The null spec resolves to the empty context, which is what a signature with no context set
149+
* uses anyway, so it is accepted before initialisation as well.
150+
*/
151+
public void testSetNullParameterBeforeInit()
152+
throws Exception
153+
{
154+
KeyPair kp = KeyPairGenerator.getInstance("ML-DSA-65", "BC").generateKeyPair();
155+
156+
Signature sig = Signature.getInstance("ML-DSA", "BC");
157+
158+
sig.setParameter(null);
159+
sig.initSign(kp.getPrivate());
160+
sig.update(MSG);
161+
162+
byte[] s = sig.sign();
163+
164+
Signature verifier = Signature.getInstance("ML-DSA", "BC");
165+
166+
verifier.initVerify(kp.getPublic());
167+
verifier.update(MSG);
168+
169+
assertTrue(verifier.verify(s));
170+
}
171+
172+
public void testGetParametersReflectsContextSetBeforeInit()
173+
throws Exception
174+
{
175+
KeyPair kp = KeyPairGenerator.getInstance("ML-DSA-65", "BC").generateKeyPair();
176+
177+
Signature sig = Signature.getInstance("ML-DSA", "BC");
178+
179+
sig.setParameter(new ContextParameterSpec(CONTEXT));
180+
sig.initSign(kp.getPrivate());
181+
182+
AlgorithmParameters params = sig.getParameters();
183+
184+
assertNotNull(params);
185+
assertTrue(Arrays.areEqual(CONTEXT,
186+
params.getParameterSpec(ContextParameterSpec.class).getContext()));
187+
}
188+
189+
/**
190+
* Re-setting the context replaces the previous one rather than being reported through a stale
191+
* cached AlgorithmParameters.
192+
*/
193+
public void testResetContext()
194+
throws Exception
195+
{
196+
byte[] second = Strings.toByteArray("second context");
197+
198+
KeyPair kp = KeyPairGenerator.getInstance("ML-DSA-65", "BC").generateKeyPair();
199+
200+
Signature sig = Signature.getInstance("ML-DSA", "BC");
201+
202+
sig.setParameter(new ContextParameterSpec(CONTEXT));
203+
sig.initSign(kp.getPrivate());
204+
sig.setParameter(new ContextParameterSpec(second));
205+
206+
assertTrue(Arrays.areEqual(second,
207+
sig.getParameters().getParameterSpec(ContextParameterSpec.class).getContext()));
208+
209+
sig.update(MSG);
210+
211+
byte[] s = sig.sign();
212+
213+
Signature verifier = Signature.getInstance("ML-DSA", "BC");
214+
215+
verifier.setParameter(new ContextParameterSpec(second));
216+
verifier.initVerify(kp.getPublic());
217+
verifier.update(MSG);
218+
219+
assertTrue(verifier.verify(s));
220+
}
221+
222+
public void testSetParameterMidUpdateStillRejected()
223+
throws Exception
224+
{
225+
KeyPair kp = KeyPairGenerator.getInstance("ML-DSA-65", "BC").generateKeyPair();
226+
227+
Signature sig = Signature.getInstance("ML-DSA", "BC");
228+
229+
sig.initSign(kp.getPrivate());
230+
sig.update(MSG);
231+
232+
try
233+
{
234+
sig.setParameter(new ContextParameterSpec(CONTEXT));
235+
fail("no exception");
236+
}
237+
catch (ProviderException e)
238+
{
239+
assertEquals("cannot call setParameter in the middle of update", e.getMessage());
240+
}
241+
}
242+
}

0 commit comments

Comments
 (0)