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
TimeStampToken now reports a malformed TSTInfo, and a token signed by other than one signer, as the TSPException its constructor declares rather than letting an unchecked exception out through the TimeStampResponse byte[] and InputStream paths, and asn1.tsp.TSTInfo requires the five to ten elements RFC 3161 sec. 2.4.2 gives the type so a short sequence is refused rather than running its enumeration out, relates to github #2415.
Copy file name to clipboardExpand all lines: CONTRIBUTORS.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -532,7 +532,7 @@ We also wish to acknowledge financial and collaborative support from [CISCO](htt
532
532
- subbudvk \<https://github.com/subbudvk\> - initial author on S2K parser hardening work for OpenPGP API.
533
533
- mkarasik \<https://github.com/mkarasik\> - initial work on EST server-side key generation (RFC 7030 4.4).
534
534
- Bernd Prünster (A-SIT Plus) \<bernd.pruenster@a-sit.at\> - reported lenient ASN.1 UTCTime/GeneralizedTime parsing accepting structurally malformed content, with fuzzing-derived test cases.
535
-
- Naveed Khan \<https://github.com/rootvector2\> - constant time comparison of membership and confirmation tags in the MLS API. Reported unbounded array allocation in the BKS/UBER keystore load path (OutOfMemoryError DoS from a crafted keystore) and introduction of a capped decompression limit in PGPCompressedData.getDataStream(), both with POC. Original submission creating S/MIME backing temp files with owner-only permissions (PR #2326). Rejecting empty sequences in the X.509 extension parsers whose RFC 5280 syntax is SEQUENCE SIZE (1..MAX) - CRLDistPoint, CertificatePolicies, ExtendedKeyUsage, PolicyMappings, and SubjectDirectoryAttributes (PR #2331). Hardening asn1.cms.SignerInfo decode to reject a non-INTEGER version or non-tagged unsignedAttrs via getInstance rather than leaking a ClassCastException (PR #2342). Fixing an off-by-4 header-length guard in the OpenPGP NotationData signature subpacket parser (PR #2346). Rejecting CR/LF in the S/MIME streaming writer header names and values (SMIMEEnvelopedWriter/SMIMESignedWriter withHeader) to prevent MIME header injection (PR #2348). Adding minimum-length guards to the SM2 decrypt and GOST28147/DSTU7624/DESede/RC2 key-wrap unwrap paths (PR #2359). Guarding the ECDH session-key length in the JCE OpenPGP decryptor (PR #2383). Fixing the identity fast-path type guards in the OER getInstance factories and the transposed isInstance arguments in OEROptional.getObject (PR #2373). Requiring the signature handed to AIMerSigner.verifySignature to be exactly the parameter set's signature size (PR #2401). Constant time comparison of the SRP-6a evidence messages (PR #2406).
535
+
- Naveed Khan \<https://github.com/rootvector2\> - constant time comparison of membership and confirmation tags in the MLS API. Reported unbounded array allocation in the BKS/UBER keystore load path (OutOfMemoryError DoS from a crafted keystore) and introduction of a capped decompression limit in PGPCompressedData.getDataStream(), both with POC. Original submission creating S/MIME backing temp files with owner-only permissions (PR #2326). Rejecting empty sequences in the X.509 extension parsers whose RFC 5280 syntax is SEQUENCE SIZE (1..MAX) - CRLDistPoint, CertificatePolicies, ExtendedKeyUsage, PolicyMappings, and SubjectDirectoryAttributes (PR #2331). Hardening asn1.cms.SignerInfo decode to reject a non-INTEGER version or non-tagged unsignedAttrs via getInstance rather than leaking a ClassCastException (PR #2342). Fixing an off-by-4 header-length guard in the OpenPGP NotationData signature subpacket parser (PR #2346). Rejecting CR/LF in the S/MIME streaming writer header names and values (SMIMEEnvelopedWriter/SMIMESignedWriter withHeader) to prevent MIME header injection (PR #2348). Adding minimum-length guards to the SM2 decrypt and GOST28147/DSTU7624/DESede/RC2 key-wrap unwrap paths (PR #2359). Guarding the ECDH session-key length in the JCE OpenPGP decryptor (PR #2383). Fixing the identity fast-path type guards in the OER getInstance factories and the transposed isInstance arguments in OEROptional.getObject (PR #2373). Requiring the signature handed to AIMerSigner.verifySignature to be exactly the parameter set's signature size (PR #2401). Constant time comparison of the SRP-6a evidence messages (PR #2406). Surfacing a malformed TSTInfo in a time-stamp token as TSPException rather than an unchecked exception out of the TimeStampResponse constructors (PR #2415).
536
536
- suraj0208 \<https://github.com/suraj0208\> - initial work on auto-detecting private key reader (JcaPrivateKeyReader).
537
537
- liamgilligan \<https://github.com/liamgilligan\> - noticing the BIP-340 step numbering in the BIP340Signer signing comments was incorrect (PR #2340).
538
538
- digi-scrypt \<https://github.com/digi-scrypt\> - disabling DTD and external-entity resolution in KMIPInputStream to close an XXE (local file disclosure / SSRF) exposure in KMIP XML parsing (PR #2315).
Copy file name to clipboardExpand all lines: docs/releasenotes.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,6 +22,7 @@ Date: 2026, TBD
22
22
- The LMS and HSS key parameter classes now apply at construction the checks their decoders apply, so a key built directly cannot be one the decoder would refuse. LMSPrivateKeyParameters accepted an identifier of any length although the decoder reads exactly 16 bytes - such a key encoded but could not be read back - and left q, maxQ and the seed length unchecked; the seed is now required to be at least m bytes at decode as well, where a one-byte seed had been decoding silently and then deriving every one-time key from it. HSSPrivateKeyParameters checked neither its level count nor that it had been given a component key per level and a chaining signature per level below the root, and then indexed both lists, so a mismatch surfaced as IndexOutOfBoundsException - or, where a level happened to match, as a null chaining signature that only failed at signing time; the level is now checked after the reset that fills it in, since a null is legitimate on the way in. LMSPrivateKeyParameters.getInstance(byte[], byte[]) adopted the public key supplied beside the private one without comparing them, so a mismatched public key was simply reported by getPublicKey(); it now cross-checks the identifier, both parameter sets and, where the tree cache already holds it, the root, as the HSS entry point does. The decoders also now report a bad version or seed length as IOException rather than IllegalStateException, so a caller can catch one type for a malformed key, and the package-private LM-OTS public key decoder no longer declares throws Exception or dereferences an unrecognised typecode. The deprecated org.bouncycastle.pqc.crypto.lms copies carry the decoder corrections.
23
23
- In the LMS JCE layer, LMSKeyGenParameterSpec.fromNames knew all twenty LMS parameter-set names but only four of the sixteen LM-OTS ones, so none of the SP 800-208 n24 or SHAKE sets could be named; all sixteen are now present. KeyPairGenerator.initialize(int, SecureRandom) reports InvalidParameterException, which is what the JCA specifies and which extends the IllegalArgumentException it raised before, so existing catches still match. BCLMSPrivateKey.getIndex now takes the exhaustion check and the index read under the key's own monitor rather than as two separate calls, and two unused fields have gone from LMSSignatureSpi. Note that the LMS Signature claims its one-time key at the first update() rather than at sign(), so a Signature that is initialised and updated and then abandoned spends an index without producing a signature - the safe direction for a one-time scheme, and now documented on the SPI.
24
24
- An HSS private key claimed the two records of its position under two different monitors. The top-level index and the bottom component key's one-time index q are independent records of the same position - the decoder requires them to agree, see the entry below - but generateLMSContext incremented the index under the HSS key's own monitor, released it, and only then claimed q under the component key's. A getEncoded() issued in between saw the index advanced and q not, and produced an encoding this implementation's own decoder rejects; and two threads meeting at a bottom-tree boundary could both pass the exhaustion test, take consecutive top-level indices and claim the same q, after which one of them was refused with "ots private key exhausted" by a key still reporting usages remaining, a top-level index had been spent with no signature made, and the two records stayed one apart for the rest of the key's life in that process - so it could no longer be encoded, cloned or sharded, and getIndex() and getUsagesRemaining() misreported by one. No one-time key was reused: the component key's claim is itself atomic, and the divergence runs index ahead of leaves, so the effect was on the key's usability rather than on the signatures it had made. Both records are now claimed under the one monitor, and the component key is claimed before the index is incremented so that an exhausted one leaves both untouched. The deprecated org.bouncycastle.pqc.crypto.lms copy carries the same correction.
25
+
- TimeStampToken parsed the attacker-controlled TSTInfo content of a time-stamp token (org.bouncycastle.tsp.TimeStampToken, reached from TimeStampResponse(byte[]) and (InputStream)) inside a try that caught only CMSException, so a well-formed RFC 3161 TimeStampResp whose embedded token carried a malformed TSTInfo - a SEQUENCE with fewer elements than the five mandatory fields, a non-SEQUENCE, truncated DER, or an unknown context tag - let an IllegalArgumentException or a NoSuchElementException escape the constructor's declared throws TSPException, IOException contract. A token signed by no signers or by more than one raised a bare IllegalArgumentException from the same constructor, before that try, for the same reason. Both are now reported as TSPException, matching the package-private TimeStampResponse(DLSequence) constructor and the existing getSignedAttributes guard in the same method; the signer-count refusal is a TSPValidationException, as the neighbouring check on the content type already was. Related, and the cause of the NoSuchElementException: asn1.tsp.TSTInfo read its five mandatory fields off the sequence with no bound on how many elements were actually there, so a short SEQUENCE left the enumeration to run out rather than being refused, and an oversize one was accepted with its extra elements absorbed into the optional slots. Decode now requires the five to ten elements RFC 3161 sec. 2.4.2 gives the type, which is what the sibling Accuracy and ArchiveTimeStamp decoders already did, so the failure is the IllegalArgumentException that getInstance is expected to raise (github #2415).
25
26
- Neither the HSS nor the XMSS^MT private key decoder checked its declared index against the traversal state stored beside it, although the two are independent records of the same position in the key and so can be compared. For HSS the records are the top-level index and the component keys' one-time indices q; for XMSS^MT they are the global index and the per-layer BDS states. A stored key whose index had been rolled back while its state stayed advanced - a partial write, a restore from backup, a buggy storage layer - was therefore accepted, and it then signed a second message under a one-time key the key had already used, producing a signature that verified, so nothing anywhere surfaced the reuse. RFC 8554 sec. 1 and RFC 8391 sec. 1.1 both require each one-time key to be used exactly once, and this is the failure those requirements exist to prevent; the single-tree XMSS decoder has tied its BDS state to its index since that state was first validated, and this brings the two multi-tree schemes into line. HSS decode now requires the declared index to equal the position the component q values imply - a level above the last contributes (q - 1) leaves of the levels beneath it, since its q has already advanced past the subtree it signed - and XMSS^MT decode now requires each present layer's BDS index to equal the leaf index that layer derives from the global index, allowing the one position where a layer has moved into a new subtree and its state legitimately still carries the previous subtree's final index. A layer with no state yet is unaffected, since those are built lazily at signing time. Related, and the same shape of omission: an XMSS or XMSS^MT private key encoding carries the tree root twice - the key's own root field and the root node of the BDS state stored beside it, which for XMSS^MT is the top layer's - and the two were never compared either. A corrupted root was accepted and then poisoned every signature the key made, because the root is hashed into the message digest: the signature did not verify and nothing indicated why. Decode now requires the two copies to agree. The BDS node values themselves are not checkable the way the LMS tree cache above is - a BDS authentication path, stack, retain or keep node does not have its children stored alongside it, so recomputing one means building a subtree, which is the work the state exists to avoid. Both checks are integer comparisons over the levels of the key, too small to measure against the surrounding decode, and both were verified not to reject any legitimate key by walking every index a key can reach: the full key space of the two-level HSS and the h=4/d=2, h=6/d=2, h=6/d=3, h=9/d=3 and h=8/d=4 XMSS^MT parameter sets, plus a three-level HSS key across a subtree boundary and an HSS shard. Since those node values cannot be recomputed, the encoded state now carries a checksum over itself instead, with the owning key's public seed hashed in front of it. Any corruption of the stored state is refused at decode rather than being loaded and then producing signatures that silently do not verify, and because the public seed is bound in, a state transplanted between two keys of the same parameter set is refused too, even though it is internally consistent and arrives with its own matching root. The public seed is bound rather than the secret seed or the PRF key deliberately: the state's own root and index are inside the encoding and so are already covered, hashing secret material would make the stored checksum a commitment to it for no gain in detection, and the PRF key does not influence the state at all. **This is an error-detecting code and not integrity protection** - anyone able to rewrite the stored key recomputes it, so it establishes that the state is unchanged since it was written, never that it was correct when written, and the allocation bounds on the encoding remain the guard against a crafted one. It costs one SHA-256 over the state, measured at 5 to 8 microseconds each way for the h=10 and h=16 parameter sets, and 32 bytes of encoding. The state encoding was added earlier in this same cycle and has not been released, so the checksum is simply part of it rather than a new version: a state written by a 1.86 beta is rejected, which is recovered from by re-exporting the key. The deprecated org.bouncycastle.pqc.crypto.lms copy carries the HSS check as well (github #2414).
26
27
- The S/MIME example smoke test in the misc module (org.bouncycastle.mail.smime.examples.test.AllTests) drove SendSignedAndEncryptedMail against smtp.gmail.com, and that example finishes with Transport.send() under JavaMail's default settings, which have no connect timeout. Where outbound port 25 is refused the failure was swallowed and the test passed; where it is silently dropped, as on many home networks, the connect blocked and ./gradlew build hung in :misc:test indefinitely with "0 tests completed". The test now delivers to an SMTP stub on a loopback port, with connect / read / write timeouts as a backstop, and asserts the message arrived (github #2407).
27
28
- Composite ML-KEM encapsulation took the traditional component public key bytes it feeds the KEM combiner from the recipient key's own encoding, while decapsulation recomputes the point from the private key and so always produced an uncompressed one. Section 4 of draft-ietf-lamps-pq-composite-kem requires an EC component to be carried as an uncompressed point, but a component key that encodes itself compressed - a BC EC key whose point format has been set through org.bouncycastle.jce.interfaces.ECPointEncoder, or a key from a provider that preserves a compressed encoding - was passed through as it came. Both sides then combined a different tradPK and derived different shared secrets, with no error reported on either: encapsulation and decapsulation both succeeded and the recipient simply could not decrypt. The EC component is now normalised to an uncompressed point wherever the engine serialises one, which covers the ephemeral key that forms the ciphertext as well. X25519 and X448 components have a single encoding and were unaffected, as were EC keys left in their default (uncompressed) format, whose shared secrets are unchanged. CompositePublicKey.getEncoded() took its component bytes the same way, so such a key also encoded to a composite key other implementations reject and whose bytes changed across an encode / decode / encode round trip - 1238 bytes rather than 1270 for MLKEM768-ECDH-P256, and for the composite ML-DSA keys sharing that method, 2006 rather than 2038 for MLDSA65-ECDSA-P256. It now normalises the component the same way. This is a write-side change only: a composite key carrying a compressed EC component is still decoded, since the component key factories accept either form, and continues to verify signatures as before - it simply re-encodes in the normalised form. The shared normalisation is org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil.getUncompressedSubjectPublicKeyBytes.
0 commit comments