Skip to content

Commit 2204472

Browse files
committed
Accept a pure ML-DSA key on the HashML-DSA signature path for its own parameter set, and bind the parameter-set specific SLH-DSA signature services to their parameter sets so the same rule applies there, relates to github #2397.
1 parent 1a45633 commit 2204472

7 files changed

Lines changed: 576 additions & 28 deletions

File tree

‎docs/releasenotes.html‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ <h3>2.1.2 Defects Fixed</h3>
119119
<li>The AIMer, SMAUG-T and NTRU+ parameter set names were not registered as algorithm aliases in the BCPQC provider, so getInstance(spec.getName()) - the natural way to turn an AlgorithmParameterSpec into a service, and what the other sixteen PQC families accept - failed with NoSuchAlgorithmException for those three. The spellings differ from the registered names only in punctuation: AIMerParameterSpec names carry no hyphen ("aimer128f" against the registered "AIMer-128f"), SmaugTParameterSpec uses an underscore ("SMAUGT_MODE1" against "SMAUGT-MODE1") and NTRUPlusParameterSpec no hyphen before the size ("NTRU+KEM768" against "NTRU+KEM-768"), so the mismatch was easy to hit and gave no hint as to the spelling wanted. Each parameter set name is now an alias of its algorithm across every service the family registers - KeyFactory, KeyPairGenerator and Signature for AIMer, KeyFactory, KeyPairGenerator, KeyGenerator and Cipher for SMAUG-T and NTRU+. The registered names are unchanged and remain the canonical ones; JCA algorithm lookup is case insensitive, so both spellings resolve in any case.</li>
120120
<li>Four PQC families disagreed with themselves about how a parameter set is spelled, because their KeyPairGenerator took its JCA algorithm name from the lightweight parameters object rather than from the name it was registered under. Falcon's generator reported getAlgorithm() as "falcon-1024" where it is registered as FALCON-1024 and its keys report FALCON-1024; SMAUG-T's reported "smaugt_mode1" where it is registered as SMAUGT-MODE1; and NTRU+ reported NTRU+KEM768 where it was registered as NTRU+KEM-768. Each family now uses one spelling throughout - generator, parameter spec, generated keys and the "key pair generator locked to ..." message - and each generator's getAlgorithm() returns the name it was obtained under: FALCON-512 / FALCON-1024, SMAUGT-MODE1 / MODE3 / MODE5 / MODET, and NTRU+KEM768 / NTRU+KEM864 / NTRU+KEM1152. The SMAUG-T parameter set names (SmaugTParameterSpec.getName(), and the algorithm the keys report) therefore move from the underscored SMAUGT_MODE1 form to the hyphenated SMAUGT-MODE1 form, and the NTRU+ services are now registered under the un-hyphenated NTRU+KEM768 form that its parameter sets already used. <b>Both previous spellings continue to resolve</b> - SMAUGT_MODE1 and NTRU+KEM-768 are registered as aliases across every service of their families - so existing getInstance calls keep working; only getAlgorithm() and the parameter set name change. SLH-DSA is corrected the same way in the entry above.</li>
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>
122+
<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>
123+
<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>
122124
<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>
123125
</ul>
124126

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,22 @@ public void configure(ConfigurableProvider provider)
120120
addSignatureAlgorithm(provider, "HASH-SLH-DSA", PREFIX + "HashSignatureSpi$Direct", (ASN1ObjectIdentifier)null);
121121
provider.addAlgorithm("Alg.Alias.Signature.HASHWITHSLHDSA", "HASH-SLH-DSA");
122122

123+
// the parameter-set specific SPIs, so that a Signature obtained by parameter set name
124+
// is bound to that parameter set rather than accepting a key of any other
125+
String[] spiNames = new String[]
126+
{
127+
"Sha2_128s", "Sha2_128f", "Sha2_192s", "Sha2_192f", "Sha2_256s", "Sha2_256f",
128+
"Shake_128s", "Shake_128f", "Shake_192s", "Shake_192f", "Shake_256s", "Shake_256f"
129+
};
130+
123131
for (int i = 0; i != algNames.length; i++)
124132
{
125-
provider.addAlgorithm("Alg.Alias.Signature." + algNames[i], "SLH-DSA");
133+
addSignatureAlgorithm(provider, algNames[i], PREFIX + "SignatureSpi$" + spiNames[i], (ASN1ObjectIdentifier)null);
126134
}
127135

128136
for (int i = 0; i != hashAlgNames.length; i++)
129137
{
130-
provider.addAlgorithm("Alg.Alias.Signature." + hashAlgNames[i], "HASH-SLH-DSA");
138+
addSignatureAlgorithm(provider, hashAlgNames[i], PREFIX + "HashSignatureSpi$" + spiNames[i], (ASN1ObjectIdentifier)null);
131139
}
132140

133141
ASN1ObjectIdentifier[] nistOids = new ASN1ObjectIdentifier[]
@@ -148,30 +156,30 @@ public void configure(ConfigurableProvider provider)
148156

149157
for (int i = 0; i != nistOids.length; i++)
150158
{
151-
provider.addAlgorithm("Alg.Alias.Signature." + nistOids[i], "SLH-DSA");
152-
provider.addAlgorithm("Alg.Alias.Signature.OID." + nistOids[i], "SLH-DSA");
159+
provider.addAlgorithm("Alg.Alias.Signature." + nistOids[i], algNames[i]);
160+
provider.addAlgorithm("Alg.Alias.Signature.OID." + nistOids[i], algNames[i]);
153161
}
154162

155163
nistOids = new ASN1ObjectIdentifier[]
156164
{
157165
NISTObjectIdentifiers.id_hash_slh_dsa_sha2_128s_with_sha256,
158166
NISTObjectIdentifiers.id_hash_slh_dsa_sha2_128f_with_sha256,
159-
NISTObjectIdentifiers.id_hash_slh_dsa_shake_128s_with_shake128,
160-
NISTObjectIdentifiers.id_hash_slh_dsa_shake_128f_with_shake128,
161167
NISTObjectIdentifiers.id_hash_slh_dsa_sha2_192s_with_sha512,
162168
NISTObjectIdentifiers.id_hash_slh_dsa_sha2_192f_with_sha512,
163-
NISTObjectIdentifiers.id_hash_slh_dsa_shake_192s_with_shake256,
164-
NISTObjectIdentifiers.id_hash_slh_dsa_shake_192f_with_shake256,
165169
NISTObjectIdentifiers.id_hash_slh_dsa_sha2_256s_with_sha512,
166170
NISTObjectIdentifiers.id_hash_slh_dsa_sha2_256f_with_sha512,
171+
NISTObjectIdentifiers.id_hash_slh_dsa_shake_128s_with_shake128,
172+
NISTObjectIdentifiers.id_hash_slh_dsa_shake_128f_with_shake128,
173+
NISTObjectIdentifiers.id_hash_slh_dsa_shake_192s_with_shake256,
174+
NISTObjectIdentifiers.id_hash_slh_dsa_shake_192f_with_shake256,
167175
NISTObjectIdentifiers.id_hash_slh_dsa_shake_256s_with_shake256,
168176
NISTObjectIdentifiers.id_hash_slh_dsa_shake_256f_with_shake256
169177
};
170178

171179
for (int i = 0; i != nistOids.length; i++)
172180
{
173-
provider.addAlgorithm("Alg.Alias.Signature." + nistOids[i], "HASH-SLH-DSA");
174-
provider.addAlgorithm("Alg.Alias.Signature.OID." + nistOids[i], "HASH-SLH-DSA");
181+
provider.addAlgorithm("Alg.Alias.Signature." + nistOids[i], hashAlgNames[i]);
182+
provider.addAlgorithm("Alg.Alias.Signature.OID." + nistOids[i], hashAlgNames[i]);
175183
}
176184
}
177185
}

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/mldsa/HashSignatureSpi.java‎

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,44 @@ protected void verifyInit(PublicKey publicKey)
5656

5757
this.keyParams = key.getKeyParams();
5858

59-
if (parameters != null)
60-
{
61-
String canonicalAlg = MLDSAParameterSpec.fromName(parameters.getName()).getName();
62-
if (!canonicalAlg.equals(key.getAlgorithm()))
63-
{
64-
throw new InvalidKeyException("signature configured for " + canonicalAlg);
65-
}
66-
}
59+
checkKeyParameters(parameters, key.getKeyParams().getParameters());
6760
}
6861
else
6962
{
7063
throw new InvalidKeyException("unknown public key passed to ML-DSA");
7164
}
7265
}
7366

67+
/**
68+
* FIPS 204 sec. 5 defines one key generation algorithm per parameter set, and a key it produces
69+
* carries no commitment to pure ML-DSA over HashML-DSA, so a <b>pure</b> key of the matching
70+
* parameter set is admissible on this pre-hash path. That matters in practice because a key
71+
* recovered from an X.509 certificate carries the pure OID (RFC 9881), and was previously
72+
* refused by these SPIs (see <a href="https://github.com/bcgit/bc-java/issues/2397">github
73+
* #2397</a>).
74+
* <p>
75+
* The tolerance is deliberately one way only. A key that already names a HashML-DSA parameter
76+
* set has been narrowed to the pre-hash mode, so it stays refused by the pure
77+
* {@link SignatureSpi} and is accepted here only when it names this SPI's own pre-hash variant.
78+
* A key of a different parameter set is refused either way, which is the check worth having.
79+
* </p>
80+
*/
81+
static void checkKeyParameters(MLDSAParameters parameters, MLDSAParameters keyParameters)
82+
throws InvalidKeyException
83+
{
84+
if (parameters == null)
85+
{
86+
return;
87+
}
88+
89+
if (parameters.getK() != keyParameters.getK()
90+
|| (keyParameters.isPreHash() && keyParameters.getType() != parameters.getType()))
91+
{
92+
throw new InvalidKeyException("signature configured for "
93+
+ MLDSAParameterSpec.fromName(parameters.getName()).getName());
94+
}
95+
}
96+
7497
protected void signInit(PrivateKey privateKey, SecureRandom random)
7598
throws InvalidKeyException
7699
{
@@ -81,14 +104,7 @@ protected void signInit(PrivateKey privateKey, SecureRandom random)
81104

82105
this.keyParams = key.getKeyParams();
83106

84-
if (parameters != null)
85-
{
86-
String canonicalAlg = MLDSAParameterSpec.fromName(parameters.getName()).getName();
87-
if (!canonicalAlg.equals(key.getAlgorithm()))
88-
{
89-
throw new InvalidKeyException("signature configured for " + canonicalAlg);
90-
}
91-
}
107+
checkKeyParameters(parameters, key.getKeyParams().getParameters());
92108
}
93109
else
94110
{

0 commit comments

Comments
 (0)