You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bring the stateful hash-based signers into line with the JCA contract the newer signers already follow: LMSSignatureSpi.engineVerify() answers false for a signature it cannot decode and raises SignatureException through SecurityExceptions for one naming an OTS type other than the verifying key's rather than letting the lightweight API's unchecked exceptions out, clearing the accumulated message in a finally either way, XMSSSignature.Builder.withSignature() rejects any length other than the RFC 8391 sec. 4.1.8 fixed size so trailing data no longer gives a valid signature a second encoding the way XMSSMTSignature has always refused, and the uninitialised XMSS / XMSS^MT generateKeyPair() branches set their tree digest and default to the constructible XMSSMT-SHA2_20/2_512 parameter set, with the verify contract and the two keygen and parse pitfalls written up in the conventions, architecture and BCPQC skill docs, relates to github #2408.
-`generateKeyPair()` runs the engine, wraps in BC key classes, returns a `KeyPair`.
174
174
-**One public static inner class per parameter set** calling `super(<Alg>Parameters.<paramset>)`.
175
175
176
+
If the SPI has an uninitialised-default branch in `generateKeyPair()` (`if (!initialised) { … }`), treat it as a second, parallel initialisation path: it must set **every** field `initialize(spec, random)` sets, not just `param`, and its parameters must actually be constructible. Both halves went wrong in the older XMSS/XMSS^MT generators (github #2408) — XMSS^MT defaulted to a height/layer pair that is not a legal parameter set, so the call threw outright, and both left `treeDigest` null, so a default-generated key threw `NullPointerException` from `equals()` / `hashCode()` / `getTreeDigest()`. Cover it with a test that calls `KeyPairGenerator.getInstance("<Alg>", "BCPQC").generateKeyPair()` with no `initialize()`, round-trips the key through its `KeyFactory` and compares it to itself.
Extends `java.security.Signature`. Contains a `ByteArrayOutputStream bOut` for message accumulation, the standard `engineInitSign` / `engineInitVerify` / `engineUpdate` / `engineSign` / `engineVerify` overrides, a `Base` inner class with no parameter binding (used when the caller selects via `"Faest"` and the actual parameter set comes from the key), and **one public static inner class per parameter set** that hard-pins the parameter check.
179
181
180
182
In `engineInitVerify` / `engineInitSign`, when the SPI was constructed with a specific parameter set, verify the key's algorithm matches the SPI's parameter set with an exact-message error — `"signature configured for " + canonicalAlg`. Tests in other algorithms assert on that exact string, so the message format is part of the contract; copy it verbatim from SNOVA's `SignatureSpi`.
181
183
184
+
`engineVerify` must never let an unchecked exception out: a signature that will not decode is `false`, one this engine cannot process is a `SignatureException` (built through `SecurityExceptions.signatureException`), and the accumulated message is cleared in a `finally` so the object survives a rejection. The lightweight signer throws unchecked for malformed input by design — translating that is the SPI's job. Full contract, and the malformed-signature battery to check it with, in the `Signature.verify()` section of `docs/claude/conventions.md`.
185
+
182
186
### Step 11 — `prov/.../pqc/jcajce/provider/<Alg>.java` (the Mappings class)
183
187
184
188
The `<Alg>$Mappings` class (extends `AsymmetricAlgorithmProvider`) is what BCPQC loads via reflection. It calls:
Copy file name to clipboardExpand all lines: CONTRIBUTORS.html
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -604,7 +604,7 @@
604
604
<li>jmeeder <https://github.com/jmeeder> - reporting that RFC 4998 evidence-record generation rejected time stamps from an authority naming the digest with NULL parameters where BC names it with them absent, both of which RFC 5754 requires a receiver to accept (issue #2379).</li>
605
605
<li>rimuln <https://github.com/rimuln> - diagnosis and fix for PKCS12 getCertificateAlias returning the alias of an unrelated certificate, tracing it to the alias and certificate enumerations of the keystore's certs table diverging in order once keys() enumerated a copy (issue #2384, PR #2385).</li>
606
606
<li>Yu Bao <yubao@paypal.com> - reporting an API gap, on behalf of the PayPal Cyber Security Team, that the high-level OpenPGP message API (OpenPGPMessageProcessor / OpenPGPMessageInputStream) gave a caller no way to bound how far a compressed data packet expands, where the low-level PGPCompressedData it wraps has carried a bounded getDataStream(long) overload all along, and that OpenPGPPolicy exposed no equivalent property to set. Suggesting a protocol whitelist for CRL Distribution Point fetching, which is now the org.bouncycastle.x509.CRLDP_protocols property, and suggesting that an OCSP response was read up to the length the responder declared for itself, now capped by org.bouncycastle.ocsp.max_response_size, and suggesting bounds on the OpenPGP ASCII armor headers, now capped by org.bouncycastle.openpgp.max_armor_header_length and org.bouncycastle.openpgp.max_armor_headers.</li>
607
-
<li>Arpan Sharma <https://github.com/Arpan0995> - initial audit of BCPQC provider consistency starting with HQC, which led to the exposure of a number of issues in the JCA provider service interfaces for other BCPQC algorithms.</li>
607
+
<li>Arpan Sharma <https://github.com/Arpan0995> - initial audit of BCPQC provider consistency starting with HQC, which led to the exposure of a number of issues in the JCA provider service interfaces for other BCPQC algorithms. In-depth auditing of PQC signature algorithms in the provider leading to the correction of a number of JCA API compliance issues.</li>
608
608
<li>Flowdalic <https://github.com/Flowdalic> - initial implementation of an AnimalSniffer-based Android API-level compatibility check for the Gradle build (PR #336).</li>
609
609
<li>hannesa2 <https://github.com/hannesa2> - initial Dependabot configuration for the Gradle and GitHub Actions ecosystems (PR #883).</li>
610
610
<li>vladhuma <https://github.com/vladhuma> - initial implementation of server-side OCSP stapling for the BCJSSE provider, on behalf of Thales Group (PR #1740).</li>
Copy file name to clipboardExpand all lines: docs/claude/architecture.md
+27Lines changed: 27 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -153,6 +153,33 @@ Practical checklist when porting a new PQC algorithm — easy to leave any of th
153
153
- Tests in `prov/src/test/java/org/bouncycastle/pqc/jcajce/provider/test/<Alg>Test.java` plus an entry in `AllTests.java`, and the signature-encoding assertions above in `core`'s `PqcSignatureEncodingTest` / `PqcMalformedInputTest`. Include a `testBcProviderKeyInfoConverter`-style case that exercises `BouncyCastleProvider.getPublicKey(SubjectPublicKeyInfo)` and `getPrivateKey(PrivateKeyInfo)` against every parameter set, proving the `loadPQCKeys()` registration works.
154
154
-`docs/releasenotes.html` — one `<li>` under the current unreleased version's "Additional Features and Functionality" block.
155
155
156
+
### Two JCA-boundary details the checklist does not spell out
157
+
158
+
Both were found in the stateful hash-based signers (github #2408), which predate the contract the
159
+
newer signers settled on — but neither is specific to them, and neither is caught by a
160
+
sign-then-verify round trip.
161
+
162
+
-**A fixed-size signature encoding has to be length-checked when it is parsed.** If the parse
163
+
reads its fields at fixed offsets and never looks at the total length, appending arbitrary bytes
164
+
to a valid signature yields a second, different encoding that still verifies — encoding
165
+
uniqueness gone, for a scheme whose signature the spec defines as an exact byte count.
166
+
`XMSSSignature.Builder.withSignature()` had this and `XMSSMTSignature` did not, so the same
167
+
library disagreed with itself: RFC 8391 sec. 4.1.8 fixes an XMSS signature at
168
+
`4 + n + (len + h) * n` bytes and the check is a three-line `if`. Assert the appended *and* the
169
+
truncated case; the truncated one often already fails for an unrelated reason, which is what
170
+
makes the appended one easy to miss. See also the `Signature.verify()` contract in
171
+
`conventions.md`.
172
+
-**`generateKeyPair()` with no preceding `initialize()` must return a fully-formed key.** The
173
+
uninitialised branch is a second, parallel initialisation path, and it has to set *everything*
174
+
`initialize(spec, random)` sets — not just `param`. Check its parameters are actually
175
+
constructible (XMSS^MT defaulted to height 10 with 20 layers, which is not a legal parameter set
176
+
at all, so the call simply threw), and check every *other* field the SPI carries: both the XMSS
177
+
and XMSS^MT generators left `treeDigest` null there, so a default-generated key threw
178
+
`NullPointerException` from `equals()`, `hashCode()` and `getTreeDigest()`. Grep the SPI for
179
+
fields assigned in `initialize` and confirm the default branch assigns each one. A one-line
180
+
`KeyPairGenerator.getInstance("<Alg>").generateKeyPair()` test that then round-trips the key
181
+
through its `KeyFactory` and compares it to itself covers both failures.
182
+
156
183
## PQC engines should stay package-private — drive KATs through the public API
157
184
158
185
The lightweight `<Alg>Engine` produced when porting a reference C/Rust implementation tends to expose low-level entry points (`engine.keyGen(seedKey, sk, pk)`, `engine.sign(sk, msg, salt, mseed)`) that KAT vectors need but legitimate callers never touch. Resist the urge to make `<Alg>Engine` public just so `<Alg>KatTest` can poke at internals — that leaks the engine into the published `bcprov` API surface where it becomes load-bearing, and any future internal refactor has to preserve the engine signature too.
Copy file name to clipboardExpand all lines: docs/claude/conventions.md
+50-2Lines changed: 50 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -91,7 +91,7 @@ is usually a sign the mapping is wrong.
91
91
92
92
Many tests assert on exact exception message text (e.g. `isTrue(e.getMessage().equals("..."))` or `getCause().getMessage()` checks). Changing the wording of a thrown exception — even something as small as adding a colon, rewording for clarity, or wrapping with `Exceptions.illegalArgumentException(...)` — will silently break tests in another module. Before modifying any exception message, grep the whole tree for the existing string and update every matching assertion in lockstep.
93
93
94
-
## Cause-chaining via `SecurityExceptions` for cause-less JDK exceptions
94
+
## Cause-chaining via `SecurityExceptions`
95
95
96
96
A handful of `java.security` / `javax.crypto` exceptions ship only a `(String)` constructor — no `(String, Throwable)` form — including `UnrecoverableKeyException`, `IllegalBlockSizeException`, `BadPaddingException`, `NoSuchPaddingException`, `NoSuchProviderException`, `CertificateExpiredException`, `CertificateNotYetValidException`, `InvalidParameterSpecException`, `ShortBufferException`, and `AEADBadTagException`. When wrapping a caught exception with one of these inside a `catch (… e)` block, do not fold the underlying text into the new exception's string and discard the cause:
97
97
@@ -111,10 +111,51 @@ catch (Exception e)
111
111
}
112
112
```
113
113
114
-
Factories exist today for `unrecoverableKeyException`, `illegalBlockSizeException` and `badPaddingException`. Add a new factory there (same one-line shape — `return (X) new X(message).initCause(cause);`) when migrating throws of any other cause-less class above; do **not** roll `new X(msg).initCause(e)` ad-hoc at the throw site. The migration is purely additive: keep the existing message text verbatim (it is almost certainly under test assertions per the previous section) and add `e` as the second argument — callers that do not care still see the same exception type and message, while callers that do can walk `getCause()`.
114
+
**The class covers more than the cause-less exceptions**, and that second group is the one people miss. `SignatureException`, `InvalidKeySpecException`, `InvalidAlgorithmParameterException`, `CertPathValidatorException` and friends *do* have a `(String, Throwable)` constructor — but only from **Java 5**, so using it directly breaks the Java-4 source floor (see `build-jdk14.md`). The factories exist for those too, and several carry a comment saying exactly that ("only exists from Java 5; initCause keeps the legacy (Java 4) builds compiling, so do not 'simplify' this to the two-arg constructor"). So the rule is simply: **in `prov`, never write `new X(msg, cause)` for a `java.security` / `javax.crypto` exception — call the factory.** A file that today sits behind an `ant/jdk14.xml` exclude is not an argument for the two-arg form; the excludes move.
115
+
116
+
Factories exist today for `invalidKeySpecException`, `generalSecurityException`, `invalidKeyException`, `invalidAlgorithmParameterException`, `noSuchAlgorithmException`, `signatureException`, `unrecoverableKeyException`, `illegalBlockSizeException`, `badPaddingException`, `certPathValidatorException`, `certPathBuilderException` and `certificateEncodingException`. Add a new factory there (same one-line shape — `return (X) new X(message).initCause(cause);`) when you need one; do **not** roll `new X(msg).initCause(e)` ad-hoc at the throw site. The migration is purely additive: keep the existing message text verbatim (it is almost certainly under test assertions per the previous section) and add `e` as the second argument — callers that do not care still see the same exception type and message, while callers that do can walk `getCause()`.
117
+
118
+
**There is a `prov/src/main/jdk1.3` overlay of this class**, and it rots like every other overlay: a new factory has to be added there too, or a 1.3-reachable caller fails to compile in that build. It is one factory behind today (`invalidAlgorithmParameterException` is missing) — latent only because nothing calls that one yet.
115
119
116
120
Throw sites *outside* a `catch` block — value-check branches like `if (x.size() == 0) throw new UnrecoverableKeyException("…")` — have nothing to chain and stay as plain `new X(msg)`. The audit grep when adding a factory is `grep -rnE "new $Cls\(" prov/src/main/java` filtered by which lines contain `e.getMessage()` / `e.toString()` (the cause-folding pattern); pure-literal throws are not candidates.
117
121
122
+
## `Signature.verify()` reports; it never throws unchecked
123
+
124
+
A provider `Signature` / `SignatureSpi` has exactly three answers at the JCA boundary, and an
125
+
unchecked exception is none of them:
126
+
127
+
-**Well-formed but does not verify** — return `false`.
128
+
-**Will not decode at all** (empty, truncated, trailing data, garbage) — also return `false`. That
129
+
is an invalid signature, not an error.
130
+
-**Decodes, but this engine cannot process it** — it names a different algorithm, parameter set or
131
+
OTS type than the key — throw `SignatureException` (via `SecurityExceptions.signatureException`,
132
+
per the section above).
133
+
134
+
The lightweight `org.bouncycastle.crypto.*` signers and their `*Signature` / `*Context` parse
135
+
helpers throw unchecked for malformed input *by design*; translating that is the SPI's job. The
136
+
whole provider was measured against one malformed-signature battery (empty / 3 bytes / zeroed /
137
+
signature+1 / signature-1) for github #2408: ML-DSA, SLH-DSA, Falcon, XMSS, XMSS^MT, Ed25519 and
138
+
RSA all answer `false`, and ECDSA throws `SignatureException` on the DER decode failure, which is
139
+
the JCA's own documented behaviour and equally fine. LMS was the sole outlier, letting
140
+
`IllegalStateException("cannot parse signature")` straight out through `Signature.verify()`. Run
141
+
that battery against any signer you touch — it is a dozen lines and it is the only thing that
142
+
surfaces this class of gap.
143
+
144
+
Two details that are easy to get wrong when adding the translation:
145
+
146
+
-**Scope the catch to the decode call, not the whole method.** Past the parse, BC's verifiers are
147
+
written to report an inconsistent signature by returning `false` rather than by throwing — see
148
+
the "these two can get out of sync with an invalid signature, we'll try and fail gracefully"
149
+
branch in `LMSEngine.verifySignature`. A `catch (RuntimeException)` around the entire body
150
+
therefore catches nothing extra today, while silently converting any *future* internal error into
151
+
a quiet `false`. Read the engine before widening.
152
+
-**Reset the accumulated message in a `finally`.** However `engineVerify` leaves, the object must
153
+
go back to the state `engineInitVerify` left it in, or the next `update()` appends to stale data
154
+
and the *following* verify fails for no visible reason. `slhdsa/SignatureSpi` is the model
155
+
(`try { … } finally { bOut.reset(); }`) and `lms/LMSSignatureSpi` now follows it. On the success
156
+
path the digest's `doFinal` has usually reset it already, so the `finally` is a no-op there —
157
+
which is fine, and far safer than one `reset()` per exit that the next branch will forget.
158
+
118
159
## System / security property constants
119
160
120
161
Any system or security property that controls BC behaviour belongs in `core/src/main/java/org/bouncycastle/util/Properties.java` as a `public static final String`, e.g. `Properties.PKCS12_MAX_IT_COUNT`, `Properties.PKCS12_IGNORE_USELESS_PASSWD`, `Properties.EMULATE_ORACLE`. Callers should reference the constant rather than inlining the literal `"org.bouncycastle.…"` name — both in production code and in tests that flip the property via `System.setProperty`. New properties should be added to `Properties` with the same naming pattern (`org.bouncycastle.<area>.<flag>`).
@@ -222,6 +263,13 @@ of older entries do read "Reported ..." for findings that came with substantial
222
263
not an absolute rule — but the default is no entry, and adding one for a bare report is a change
223
264
dgh should make rather than something to assume. Ask if unsure.
224
265
266
+
**Sustained auditing is the standing exception**, and dgh initiates it. Someone who works through a
267
+
subsystem and turns up several confirmed defects gets an entry describing the audit rather than a
268
+
patch — `Arpan Sharma`'s reads "initial audit of BCPQC provider consistency starting with HQC ..."
269
+
and was extended in the same house style when a later sweep produced github #2408. Note the shape:
270
+
it names the area swept and what the sweep led to, not the individual bugs, and it was **appended to
271
+
the existing entry**. Still don't add one unprompted — wait for dgh to ask.
272
+
225
273
When an entry is warranted it goes at the end of the list in the house form
226
274
`<li>name-or-handle <email-or-github-url> - what they contributed (PR #NNNN).</li>`; a bare
227
275
GitHub handle with `https://github.com/<handle>` in place of an email is well established. When a
0 commit comments