Skip to content

MINOR: Bump org.bouncycastle:bcpkix-jdk18on from 1.85 to 1.86 - #1325

Merged
jbonofre merged 1 commit into
mainfrom
dependabot/maven/org.bouncycastle-bcpkix-jdk18on-1.86
Oct 5, 2026
Merged

jbonofre merged 1 commit into
mainfrom
dependabot/maven/org.bouncycastle-bcpkix-jdk18on-1.86

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Oct 5, 2026

Copy link
Copy Markdown
Contributor

Bumps org.bouncycastle:bcpkix-jdk18on from 1.85 to 1.86.

Changelog

Sourced from org.bouncycastle:bcpkix-jdk18on's changelog.

Bouncy Castle Crypto Package - Release Notes

1.0 Introduction

The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. The package is organised so that it contains a light-weight API suitable for use in any environment (including the J2ME) with the additional infrastructure to conform the algorithms to the JCE framework.

2.0 Release History

2.1.1 Version

Release: 1.87
Date: 2026, TBD

2.1.2 Defects Fixed

  • A PBEKey stored in a BCFKS KeyStore was not reported as a key entry: isKeyEntry() returned false for it, so KeyStore.getEntry() returned null rather than a SecretKeyEntry, although getKey() recovered it. The PBE key entry type added for github #2164 was missing from engineIsKeyEntry, and is now included.

  • The CMS content-type decoders SignedData, EnvelopedData, AuthenticatedData, AuthEnvelopedData and EncryptedData read their mandatory fields by index or enumeration with no lower-bound size check, so a ContentInfo whose inner content was an empty or too-short SEQUENCE - malformed but DER-parseable - left the decode as an unchecked NoSuchElementException or ArrayIndexOutOfBoundsException rather than the CMSException the CMSSignedData, CMSEnvelopedData, CMSAuthenticatedData and CMSAuthEnvelopedData constructors declare (the IllegalArgumentException CMSEncryptedData documents). Each now rejects a short sequence with IllegalArgumentException before reading, as the sibling content types CompressedData and DigestedData already did, so the wrappers surface it as their declared exception; the case that claims an OPTIONAL field and then truncates the mandatory ones after it is covered too. The streaming parsers CMSSignedDataParser, CMSEnvelopedDataParser, CMSAuthenticatedDataParser, CMSAuthEnvelopedDataParser and CMSCompressedDataParser had the same gap and more: an absent or truncated inner SEQUENCE, a mandatory field of the wrong type, or absent encapsulated or encrypted content escaped as NullPointerException, ClassCastException, IllegalArgumentException or IllegalStateException. The asn1.cms stream parsers now report a missing mandatory field as an IOException, and the CMS parsers report malformed content as CMSException ("Malformed content." / "Missing content.") or the IOException they declare.

  • A KeyAgreement asked for its shared secret before doPhase returned data rather than refusing. javax.crypto.KeyAgreement specifies IllegalStateException for that state, but nothing in the provider tracked it, so each SPI handed back whatever its result field held: for Diffie-Hellman that was the private value itself - engineInit seeded result with x, so generateSecret() returned the private exponent padded to the prime's length and generateSecret("AES") an all-zero key taken from that padding - while ECDH returned null and its named-algorithm overload raised NullPointerException. BaseAgreementSpi now records whether a doPhase has completed the agreement since the last init and refuses the request with an IllegalStateException naming the algorithm, so every family in the provider - DH, ECDH and ECMQV, the SM2 exchange, both ECGOST families, XDH, SM9 and NewHope - answers the same way, and the DH SPI no longer holds the private value in that field at all.

  • Mac.getInstance and KeyGenerator.getInstance by the HMAC SHA-512/224 and SHA-512/256 object identifiers (1.2.840.113549.2.12 and .13) failed, although the same algorithms resolved by name and the matching SecretKeyFactory aliases were registered: the SHA512 mappings called addHMACAlgorithm for the two truncated variants without the addHMACAlias that registers their OIDs against Mac and KeyGenerator. Both are now aliased, as every other HMAC in that class already was.

  • A KTSParameterSpec naming an HKDF key-derivation function with a parameters field - a form the provider does not service - was accepted at Cipher init and then failed out of wrap or unwrap with an unchecked IllegalStateException neither method declares. The KTS key-wrapping Ciphers (ML-KEM, Classic McEliece, FrodoKEM, the composite KEM and RSA-KEM) now validate the spec's KDF when they take it, reporting an unserviceable one as the InvalidAlgorithmParameterException engineInit declares, which is what the javax.crypto.KEM services already did through KdfUtil.resolveKemSpec.

  • A DTLS handshake deadlocked when a handshake message ahead of the peer's ChangeCipherSpec (a client's CertificateVerify, say) was lost while the ChangeCipherSpec and Finished behind it arrived: the record layer moved its read epoch on at the ChangeCipherSpec and then discarded every retransmission of the lost message as belonging to the old epoch, whose records are only accepted once the handshake has completed. Each side then waited on the other until a handshake timeout, if one was configured, ended it. Every client-authenticated handshake, and every handshake in which the server issues a NewSessionTicket, was exposed. Until the handshake completes, handshake records from the current epoch are now still accepted after the read epoch has moved on, and each message is checked against the epoch of the record that carried it. The DTLS loopback tests now run their handshakes at 10% datagram loss in each direction, with a client-authenticated handshake at 25%.

  • The lightweight SubjectPublicKeyInfoFactory and PrivateKeyInfoFactory encoded a GOST R 34.10-2012 key on one of the legacy CryptoPro curves under id-GostR3410-2001, although RFC 9215 sec. 4.2 permits those curves for 2012 keys. The digestParamSet now decides: a GOST R 34.11-94 parameter set means 2001 (RFC 4491 sec. 2.3.2), a GOST R 34.11-2012 digest or none means 2012 with 256/512 taken from the curve field size, and any other value is rejected. GOST3410PublicKeyAlgParameters treats digestParamSet as OPTIONAL on both read and write per RFC 9215, and PrivateKeyInfoFactory now passes attributes through for ECGOST3410 keys (bc-csharp github #707).

  • The name-constraint host canonicalisation removed a single RFC 1034 root-label dot, the only empty label a name may legally carry, but nothing refused the ones that are not legal: a dNSName, rfc822Name host or uniformResourceIdentifier host such as "example.com.." kept a phantom empty label after the strip and so matched no constraint at all, escaping an excluded subtree naming the host it appears to carry. A tested name whose host carries an empty label - a second trailing dot, a doubled dot or a leading dot - is now refused outright wherever a constraint of that type is in force, rather than canonicalised into a name it is not: removing the extra dots would decide on the caller's behalf that "example.com.." names example.com, which is not how a consumer resolving or comparing the name reads it, and refusing fails closed in both directions where canonicalising would newly admit such a name under a permitted subtree. The single trailing dot is canonicalised as before, a bare "." remains the root label rather than an empty one, and the guard is scoped to the host, so the doubled dot a quoted local part may legally carry is unaffected. Constraints are untouched - one may still begin with a dot, which is how this implementation spells "subdomains only" (github PR #2436).

  • SSLContext.createSSLEngine() from the BCJSSE provider in the 1.86 bctls jar failed with NoSuchMethodError on every JDK from 9 up, leaving engine-based users of the provider (Netty, Vert.x and the like) unable to open a connection. The jdk1.5 and jdk1.9 copies of the package-private SSLEngineUtil had declared create(ContextData) with different return types since 2019 - SSLEngine and ProvSSLEngine - and the root ProvSSLContextSpi, compiled against the first, is paired at runtime with the versions/9 copy on any modern JDK. Until 1.86 the java9 compile had hidden this by implicitly recompiling the whole base tree into META-INF/versions/9 (447 classes, ProvSSLContextSpi among them); the -implicit:none added in 1.86 to stop that duplication exposed the mismatch. The jdk1.9 copy now declares the same return type as the root one, a JDK 25 test creates engines against the built jar, and a new multiReleaseCheck Gradle task on every distributed jar reads the constant pool of each class in the jar and in the sibling BC jars it depends on and fails the build when a member reference does not resolve against the copy of its target that a JDK would pair it with, so the class of defect cannot ship again; the same check runs on arbitrary jars, a published release included, as multiReleaseCheckJar (github #2448).

  • The BCFKS key store derived its scrypt keys with the block size r in place of the parallelization parameter p, while writing the p the caller configured out to the store: BcFKSKeyStoreSpi passed getBlockSize() to SCrypt.generate for both arguments and never read the encoded parallelization parameter at all, so every store whose ScryptConfig gave a p other than its r encoded parameters that do not derive its own keys. The store was self-consistent - BC read back what BC wrote - but a conformant RFC 7914 reader computed a different key and so failed the integrity check and the store decryption, and BC could not open such a store written by anyone else. Derivation now follows RFC 7914. A store written by 1.86 or earlier is still read: the integrity check is retried under the old convention, and where a signature check leaves no MAC to settle it the store decryption is retried instead, in both cases reporting the failure of the encoded parameters rather than of the fallback. The write side is governed by org.bouncycastle.bcfks.scrypt_p_eq_r, default true, which writes p equal to r whatever the ScryptConfig asked for: the two conventions then agree, so a store written here is both RFC 7914 correct and readable by 1.86 and earlier. Clearing the property honours the configured p, which those releases cannot read unless p already equals r; the default is intended to become false in a later release, once enough of the installed base is writing parameters that describe themselves. Loading with a BCFKSLoadStoreParameter carrying a ScryptConfig accepts an encoded p equal to either the configured p or the block size, so a store round trips under the configuration that wrote it whichever way the property was set; every other parameter is compared as before. The parallelization parameter is now bounded alongside the cost parameter before the derivation, as the PKCS#8 and PKCS#12 scrypt paths already bound it.

  • Building an evidence record was cubic in the number of data objects: SortedHashList and SortedIndexedHashList held their hashes in a LinkedList and found each insertion point by walking it with get(index), so a single add() was quadratic in the position it inserted at and building a list of n hashes cubic, and both lists sit on the generation path - the reduced hash tree of ERSArchiveTimeStampGenerator, the Merkle tree of BinaryTreeRootCalculator.computeRootHash(), and the hash list of every ERSDataGroup. Each now collects its hashes and sorts them once, in toList(); the sort is stable and the old insertion placed a hash after the last one comparing equal to it, which is where a stable sort puts it, so the order of the leaves and every root hash are unchanged. getFirst() answers with a scan rather than a sort and toList() sorts a copy, so neither accessor disturbs what has been added. Generating a time-stamp request over 8,000 data objects goes from about 210 seconds to under a tenth of a second, and 100,000 objects, which the old code could not reach in any practical time, takes about 0.2 seconds (github #2456).

  • An ERSDataGroup recomputed its hash on every request rather than taking it from the cache ERSCachingData exists to provide: the group overrode getHash(), which left the calculateHash() the cache calls unreachable - and wrong, as it copied the member hashes with a loop bounded by the size of the empty list it was copying into, so it would have digested nothing had anything reached it. The computation is back in calculateHash() and the override is gone, so a group's hash is computed once per digest algorithm and previous-chain hash, as every other ERSData's is. The value itself is unchanged.

  • ERSArchiveTimeStampGenerator rebuilt its reduced hash tree from the data objects on every call, so the usual generateTimeStampRequest() followed by generateArchiveTimeStamp() or generateArchiveTimeStamps() built it twice. The leaves are now built once and dropped when data or a previous chain is added. A Set of the data groups it had been given, which nothing ever read, has gone with it.

  • Grain-128AEAD returned corrupted plaintext from a decryption driven in chunks. The stream cipher data operator splits a processBytes() call that spans the buffered authentication tag into two output segments, the bytes released from the tag buffer and then the bytes taken straight from the caller's input, and wrote the second segment at the caller's output offset instead of after the first, so the second segment overwrote the head of the first and the tail of the reported output was never written at all. The call still returned the full byte count, and because the engine's state update depends on the input and the keystream rather than on where the output lands, the tag still verified: the wrong plaintext came back with no error raised. Any chunk after the first that carried more than the 8 byte tag length was affected. Grain-128AEAD is the only engine that uses this operator, and one shot decryption, the encryption path and every other AEAD engine were unaffected. The second segment is now written at the advanced offset, matching the equivalent step of the general decryption path (github PR #2447).

  • The certificate path validators looked a certificate up in an indirect CRL by serial number alone and then compared the issuer of whichever entry came first, so where two issuers had each revoked the same serial number - an indirect CRL (RFC 5280 sec. 5.2.5) lists certificates from more than one issuer, and a serial number is only unique within its issuer - a certificate whose own entry followed the other issuer's was reported as not revoked. The BC provider's PKIX validator, the pkix X509RevocationChecker and the legacy org.bouncycastle.x509 validator now check every entry carrying the serial number against the issuer it applies to, the one named by its certificateIssuer extension or inherited from the entries before it (sec. 5.3.3), and the provider's X509CRL implements getRevokedCertificate(X509Certificate), the JDK's lookup for indirect CRLs, the same way rather than inheriting the default that assumes a single issuer. CRL.isRevoked() on the provider's CRLs had the same fault, stopping at the first entry carrying the serial number, and now does the same. The validators also missed, whatever the serial numbers, any revocation listed for an issuer other than the CRL issuer when the CRL had been parsed by the JDK's own provider, whose getRevokedCertificate(BigInteger) looks only at the CRL issuer's entries; they now look further whenever that lookup cannot be conclusive. In the jdk1.4 distribution the two provider validators read an entry's certificate issuer through a cast to a class the provider's own CRLs do not use, so an indirect CRL carrying an entry with the certificate's serial number failed validation with no reason given, whether or not that entry was the certificate's; they now share the lookup above.

  • The AEAD stream cipher data operator, which Grain-128AEAD alone uses, wrote its output without first checking that the caller's buffer was long enough, so a short output buffer surfaced as an ArrayIndexOutOfBoundsException from inside the engine rather than as the OutputLengthException the general path reports for every other AEAD engine. Both directions of processBytes(), and processByte(), now check before anything is written or buffered, and only when the call releases output, as the general path does.

  • The RFC 9709 content-encryption AlgorithmIdentifier, which carries the real algorithm inside the parameters of an outer id-alg-cek-hkdf-sha256, was unwrapped at only one of the points where a CMS recipient makes a decision about it. Key-size validation was corrected for plain key transport in 1.86, but the same call in the KEK, RSA-KTS and KEM recipients, and in the key-transport recipient's own ORI-KEM branch, still compared the recovered key against the outer identifier, which registers no key size, so setKeySizeValidation(true) silently checked nothing there; the setAllowedContentAlgorithms allow-list and the setMinimumTagSize floor were applied to the outer identifier on every recipient family, including the one already corrected, so neither constrained an RFC 9709 message. The unwrap now happens once for the key-size check and once for the two policy checks, and every recipient polices and validates the content-encryption algorithm the message actually carries. A recipient with no allow-list, no tag floor and no key-size validation configured behaves exactly as before; a caller who listed id-alg-cek-hkdf-sha256 in an allow-list in order to admit RFC 9709 messages must now list the content-encryption algorithms themselves (github PR #2446).

  • A CMS message whose EncryptedContentInfo named the RFC 9709 key derivation but carried no readable content-encryption AlgorithmIdentifier in its parameters was reported as a NullPointerException, or as an IllegalArgumentException from the ASN.1 decoder, out of methods declared to throw CMSException, RecipientInformation.getContent() among them. The four places that resolve the wrapper - the CEK derivation, the content cipher selection, the key-size check, and the recipient's allowed-algorithm and tag-size checks - now share one resolver, which reports an absent or unreadable inner algorithm as a CMSException.

  • The two YubiKey OpenPGP smart-card decryptor factories zeroized the user PIN array the KeyPassphraseProvider handed them rather than a copy of it, in a finally block after each private-key operation. Both providers BC ships return the application's own array by reference - DefaultKeyPassphraseProvider hands back the char[] it has cached for the key, and the provider inside OpenPGPApi.editKey returns its argument - so the first card operation destroyed the caller's PIN and the next private-key operation presented an all-zero PIN to the card, which the card refuses at the cost of a PIN retry. The PIN is now fetched as a clone the card operation owns - in OpenPGPSmartCard.requireUserPin, where the smart-card restructuring of this release put the fetch the two factories used to make - and KeyPassphraseProvider.getKeyPassword records that the array it returns stays owned by the provider (github PR #2444).

  • The CRMF PKIPublicationInfo structure accepted, and could be built with, publication information RFC 4211 sec. 6.3 forbids: pubInfos MUST NOT be present if the action is dontPublish, and the field is SEQUENCE SIZE (1..MAX), so a present one is never empty. Both contradictions are now rejected with an IllegalArgumentException, on parsing and on construction from an array of SinglePubInfo. An absent pubInfos with the pleasePublish action, which is how the RFC spells "don't care", is unaffected, and no constructor in the library could produce either rejected form.

  • The CMS RFC 8418 key agreement schemes (dhSinglePass-stdDH-hkdf-sha256/384/512, used with X25519 and X448) derived the key-encryption key with the user keying material in the entityUInfo of the ECC-CMS-SharedInfo but never as the HKDF salt, where RFC 8418 sec. 2.2 requires both - its recipe is salt = ukm, PRK = HKDF-Extract(salt, K), KEK = HKDF-Expand(PRK, DER(ECC-CMS-SharedInfo), SizeInOctets(KEK)). A message carrying a ukm therefore did not interoperate with a conforming implementation in either direction. The ukm is now passed as the salt as well, on both the generating and the receiving side, for those three schemes. A message with a ukm written by 1.86, the only release with RFC 8418 support, is not readable by this release and vice versa; messages without a ukm, and the X9.63-KDF key agreement schemes, are unaffected. The round-trip test now covers both the ukm and no-ukm cases for all six curve and scheme combinations, and checks the key-encryption key against the RFC's own recipe rather than only against BC itself (github #2454).

  • A JKS store shorter than the SHA-1 checksum it ends with threw an unchecked ArrayIndexOutOfBoundsException out of KeyStore.load, which declares IOException for a store it cannot read: JKSKeyStoreSpi.validateStream subtracted the digest size from the raw store length without checking it, so the digest update clamped its negative length to zero and the System.arraycopy that lifted the stored checksum out failed on a negative source index. The length is now checked against the checksum plus the 12-byte header before the checksum position is used, and a store too short to carry either is reported as an EOFException. The JKS store is reached through the compatibility probe in AdaptingKeyStoreSpi, so any key store type that probes for it was exposed, and the legacy jdk1.1 and jdk1.4 provider copies carry the same fix (github #2451).

  • A custom Argon2BytesGenerator.BlockPool was left to zeroise the blocks it recycled itself, and had no way to know how many blocks to hold: the generator returned each block to the pool with the password-derived data still in it, so only the FixedBlockPool BC ships cleared them, and sizing any other pool meant replicating the internal memory alignment and the block count of the fill step. The generator now clears every block before it goes back, so a pool neither has to clear nor can observe that data, and Argon2BytesGenerator.getBlockCount(memory, lanes) gives the number of blocks a run takes - which the default pool now uses, so it no longer discards and reallocates the four blocks of the fill step on every call. FixedBlockPool drops the two clears it no longer needs, leaving one zeroisation per block per use rather than two, and a generateBytes() that fails part way through now returns and clears the blocks it took, along with its own working buffer, rather than leaving both to the garbage collector (github #2452).

  • The PKCS#12 key stores wrote the MAC key-derivation parameters of a file they had loaded into every file they wrote afterwards, under whatever password the caller stored with. For PKCS12-PBMAC1 that carried the loaded file's PBKDF2 salt, iteration count, key length and PRF, because the parameters were minted only when the store held none and were then assigned back, so the branch ran once per store object rather than once per write - which also meant one store reused a single PBKDF2 salt across every write, including writes under different passwords, with no file loaded at all. The classic store inherited the MAC salt length and digest algorithm the same way, so a file declaring a zero-length MAC salt was re-stored with one, and a file it had loaded under RFC 9579 handed on that file's PBKDF2 salt too, both stores reading PBMAC1. The values were also latched before the MAC was verified and were not cleared by the load(null, null) a caller must issue to recover, so a file that failed the check left them behind for the caller's own file. The PBKDF2 salt and the MAC salt are now generated for every write, the MAC salt at no fewer than 8 octets, nothing is latched until the file has verified, and an AlgorithmIdentifier supplied through a PKCS12StoreParameter is still written as it was given. A loaded file's PRF, key length, digest algorithm and MacData iteration count are still kept, the last as before; the PBKDF2 count is kept where it is at least the count being written with and raised to it otherwise, since the file being re-stored is not the one that count was chosen for - RFC 9579's own test vectors ask for 2048. That count is now org.bouncycastle.pkcs12.pbkdf2_it_count, default 65,536, the write-side counterpart for a PBMAC1 MAC of what org.bouncycastle.pkcs12.store_it_count is for the PBE. Reading is unaffected: a file's MAC is verified with the parameters it carries, whatever they are (github #2450).

  • Cipher.SM9 took its data-encapsulation mode for decryption from the ciphertext rather than from the mode the Cipher was configured with. The GM/T 0080-2020 SM9Cipher structure names the mode in an enType field, but GM/T 0044.4 defines the authenticator as C3 = MAC(K2, C2), over the encapsulated message alone, so enType is not covered by it: re-encoding a ciphertext with the other enType leaves C1, C3 and C2 untouched and steers the recipient into the other mode. GM/T 0044.4 takes K1 and K2 from a single KDF output of klen = mlen + K2_len bits in stream mode and K1_len + K2_len bits in SM4 mode, where K1_len = 128, so when C2 is 16 bytes long the two modes make the identical KDF call and derive the same K1 and K2: a one-block SM4 ciphertext relabelled as stream mode passes the MAC check and the recipient returns K1 xor C2 - from which both the SM4 key K1 and the padded plaintext block follow, wherever that output is observable. CipherSpi now decrypts in the configured mode and rejects a ciphertext whose enType disagrees with it, so the mode is symmetric between encryption and decryption. That check compares two values a relabelling attacker can make agree, and so does not by itself protect a recipient whose Cipher is configured for stream mode - the relabelled one-block ciphertext then matches the configuration - so SM9Engine additionally refuses a 16-byte C2, the one C2 length at which the two modes collide, in both modes and both directions: on decryption, and on encryption a 16-byte message in stream mode and a message of fewer than 16 bytes, which pads to one block, in SM4 mode. Refusing the length on decryption protects the recipient that does so, but the message a relabelled ciphertext gives away is the SM4-mode sender's, who cannot tell whether the recipient's implementation refuses it, which is why the SM4 mode no longer produces one; messages of every other length are unchanged in both modes. A stream-mode ciphertext must accordingly be decrypted through a stream-mode Cipher ("SM9/XOR/NoPadding") rather than the SM4-mode default that Cipher.getInstance("SM9") gives; a message of fewer than 16 bytes has to be sent in stream mode, and one of exactly 16 bytes - a 128-bit key, say - in SM4 mode; and a ciphertext made by an earlier version whose C2 is 16 bytes long is no longer decrypted, whichever mode wrote it - a one-block SM4-mode ciphertext, or a stream-mode one carrying a 16-byte message. The SM9 KEM is unaffected.

  • SM9 public-key encryption: Cipher.SM9 decrypted more than one encoding of a ciphertext and now takes only the DER encoding encryption produces, with a 65-byte uncompressed C1 and a 32-byte C3; the SM9Cipher ASN.1 type holds C1 and C3 to those sizes too, and represents all five GM/T 0080-2020 enType values where it had refused SM4-CBC, -OFB and -CFB, which Cipher.SM9 does not implement. SM9Engine reports a ciphertext it cannot decrypt with the InvalidCipherTextException processBlock declares, where an out-of-range C1 coordinate or an SM4-mode C2 of the wrong length had escaped as unchecked exceptions, and likewise a message too long for its SM4-mode ciphertext to fit an array, refuses one whose K1 is zero as it refuses one failing the MAC check, and after a refused init is left uninitialised rather than keyed as before. Cipher.SM9 counts buffered input in getOutputSize, keeps the input update() had buffered when doFinal throws ShortBufferException, so that the call can be repeated with a larger output buffer, throws that exception rather than an ArrayIndexOutOfBoundsException for an output offset near Integer.MAX_VALUE, reports its key size to a restricted crypto policy, refuses an AlgorithmParameterSpec on decryption as on encryption, refuses WRAP_MODE and UNWRAP_MODE with the UnsupportedOperationException javax.crypto.Cipher documents for them rather than an InvalidParameterException, and refuses a key-exchange or destroyed key at init with InvalidKeyException. A key destroyed after init is reported by the exception the operation declares for a state it cannot run in - sign() of Signature.SM9 with SignatureException, doFinal() of Cipher.SM9 and the phases of KeyAgreement.SM9 with IllegalStateException, decapsulate() of KEM.SM9-KEM with DecapsulateException - where Cipher.SM9 had reported a malformed ciphertext, sign() and decapsulate() an IllegalStateException neither declares, and the first phase of KeyAgreement.SM9 had sent an ephemeral first. The encryption keys, which the KEM and the key exchange share, are decoded only from the uncompressed form getEncoded() writes, with coordinates below q and G2 membership checked, which a new SM9G2Point.isInSubgroup() also checks for a point built by arithmetic; a master public key at infinity is refused, and so, wherever it is formed, is a recipient whose derived point is at infinity; nothing encrypts or encapsulates to a recipient formed under HID_EXCHANGE (0x02), and no KEM or decryption key is derived or imported under it, though the hid may otherwise be any value the KGC publishes rather than only 0x02 or 0x03; a null master public key or an empty identity is refused; key equality takes in the hid, usage, master public key and identity; a user private key imported through SM9EncPrivateKeyParameters.fromEncoded, fromEncodedExchangeKey or KeyFactory.SM9 is refused unless it is the key the KGC derives for its identity and hid under the master public key it is imported with, which costs a pairing, where the four had been taken as given; and a new fromEncoded overload checks a master private key against the master public key its KGC published. Decryption's pairing starts its Miller loop from a random representative of the private key and blinds its inversions, the exponentiation by the sender's ephemeral no longer runs faster for a shorter exponent and works on its intermediate values multiplied by a random factor drawn for each call, and the derived keys, plaintext copies and KDF input are erased once used. The SM4 mode is documented as the SM4-ECB, no-IV form of the GM/T 0044.5-2016 Annex D.1 example, which does not interoperate with peers following the official English edition (CBC with an untransmitted zero IV) or GB/T 38635.2-2020 (a 16-byte IV at the front of C2). For all four SM9 functions: BouncyCastleProvider.getPublicKey and getPrivateKey now resolve the SM9 master keys; KeyFactory.SM9 answers to the SM9-ENC and SM9-SIGN names the keys report, reads a key only from the encoding getEncoded() writes, and gives a PKCS#8 spec from getKeySpec only for a master private key, where it had given a user private key one it could not read back; KeyPairGenerator.SM9-ENC and SM9-SIGN refuse a strength other than 256; the exported arithmetic, hash and KDF methods refuse arguments they cannot honour rather than truncating them; SM9P256V1Point refuses, with an UnsupportedOperationException, to write the compressed encoding SM9's G1 curve cannot read back; SM9Engine.getOutputSize refuses before init, where it had answered as for decryption; a master private key is decoded only from its 32 bytes, where an encoding of any length had been read as the scalar; on the signature side SM9SigMasterPrivateKeyParameters.generateUserKey refuses a null or empty identity, and SM9SigPrivateKeyParameters.fromEncoded a null or empty identity and a null master public key, as the encryption side does; SM9Sm3.h1 and h2 take hlen from log2 n itself, as GM/T 0044.2 5.4.2 defines it, rather than from n's bit length, which agree for SM9's N but not for every n the methods accept; the KGC's multiplications in G2 - [ks]P2 for a signature master key and [t2]P2 for each encryption user key - run a fixed-point comb over a table of multiples of P2, reading each entry in full and carrying the entries by a random factor drawn for each call, over a scalar blinded with a random multiple of N, and the subgroup check on a decoded G2 point runs a chain of doublings and additions over the non-adjacent form of the BN parameter t from a random representative of the point, both in Jacobian coordinates, where they had worked in affine coordinates on the point and the scalar themselves; the multiplications of a point of G1 by a secret scalar - [ke]P1 for an encryption master key and [t2]P1 for each signing key, the signer's [l]ds and the ephemeral's multiple of the recipient's or the peer's point - blind the scalar in the same way and carry the entries they read by a random factor drawn for each call, reading each in full from an entry drawn at random, where they had read the scalar's own digits from entries that were the same for every call; the arithmetic in F_q and its extensions, which the pairing, G_T and G2 compute in, runs on fixed-width limbs in constant time, where it had run on BigInteger, G1's arithmetic in F_q reduces its sums, differences and products below q without branching on a comparison with q, and neither the exponentiation by a secret exponent in G_T, which reads its multipliers from a table in full, nor the multiplications by a secret scalar in G1 and G2 branch on the secret's bits; and a random source that yields nothing usable is reported rather than hanging a draw or settling it on 1.

  • The SM9 KEM: KEM.SM9-KEM, KeyGenerator.SM9-KEM and the lightweight SM9KEMGenerator and SM9KEMExtractor now refuse, when the key or spec is handed over - the provider services with the checked exception their API declares - what they had accepted and then failed on or silently ignored: a KDF the provider does not service; a KDF other than the specs' default, or any otherInfo, in a KEMGenerateSpec or KEMExtractSpec, which the KeyGenerator had ignored, giving the same key whatever was asked for - KEM.SM9-KEM layers a KDF through a KTSParameterSpec; a key size that is not a positive whole number of bytes, which had given fewer bits than asked for or an empty key; and a key-exchange key, and, at the provider services, a destroyed key. After a refused init the KeyGenerator is left uninitialised, where generateKey() had gone on to throw a ClassCastException over the spec refused or to run the operation configured before it. A malformed encapsulation is reported as one rather than in an internal class's words, and the encryption keys are checked as for encryption, the rules on recipients included. Decapsulation's pairing and encapsulation's exponentiation are hardened as decryption's and encryption's are, and the serialised pairing value, the KDF input and the KeyGenerator's copy of each secret are erased once used. SM9KEMGenerator refuses a recipient key of the wrong kind with IllegalArgumentException, as SM9Engine does, rather than with a ClassCastException.

  • The SM9 key exchange: KeyAgreement.SM9 and the lightweight SM9KeyExchange each kept the ephemeral r after an exchange completed, so that it could be combined with a second peer value, where GM/T 0044.3 draws a fresh r for each exchange; both now discard it once the key is derived. KeyAgreement.SM9 also drops the previous session at the start of init and of a first phase, before examining the new arguments, so that a rejected call no longer leaves an earlier shared secret, key or ephemeral in place, and it refuses a destroyed key at init rather than after sending an ephemeral. SM9KeyExchange keeps its own copy of the peer identity and refuses a null or empty one, drops the confirmation tags of a completed exchange when another begins, and accepts only points of SM9's own G1, where a point of another curve had produced a "shared key"; and the key length must be a positive whole number of bytes, which KeyAgreement.SM9 and KEM.SM9-KEM had rounded in opposite directions. The pairing on the private key and the exponentiations by r are hardened as for encryption, and the KDF input, the serialised pairing values and the inner hash of the confirmation tags are erased once used.

  • SM9 signatures: Signature.SM9 accepted more than one encoding of a signature - a non-minimal DER length, h and S trading a byte across their boundary, S in the hybrid point form - and now verifies only the encoding sign() produces, the lightweight SM9Signer only h followed by S in uncompressed form, while the SM9Signature ASN.1 type holds h and S to their 32 and 65 bytes. The exponentiation by the signing nonce no longer runs faster for a shorter nonce and works on its intermediate values multiplied by a random factor drawn for each call, and the signing scalar l = (r - h) mod N is formed with the constant-time helper. Signature.SM9 draws the nonce from the SecureRandom handed to initSign, is left as initVerify left it however verify() returns, is left uninitialised by a refused init, where it had gone on signing or verifying under the key of the init before it, answers getParameters() with null and refuses a destroyed key at init; SM9Signer copies the identity it verifies under, refuses an empty identity, refuses parameters of the wrong kind and use before init with IllegalArgumentException and IllegalStateException, is left uninitialised by a refused init, catches an exception in verification only from decoding S, where it had reported any exception at all as a failed verification, and hashes the message as update() is given it, where it held all of it until it signed or verified and left copies of it behind. SM9Sm3.h2, which SM9Signer no longer calls, is deprecated. A user private key at infinity or in the hybrid form, and a master public key outside G2 or with a coordinate at or above q, are refused on decoding; key equality takes in the master public key and identity; a master private key can be checked on decoding against the master public key its KGC published, as for the encryption keys; and a user private key is checked on import against the master public key and identity it is filed under, as an encryption user key is, where one filed under another master public key or identity imported and made signatures that did not verify. SM9Signer also reports a key destroyed since init through the CryptoException generateSignature declares rather than as an IllegalStateException.

  • Decrypting an OpenPGP message in two steps - recovering the session key from a SKESK packet and then decrypting the SEIPD v1 body through PGPEncryptedDataList.extractSessionKeyEncryptedData() - stopped detecting a wrong passphrase. 1.86 suppressed the legacy CFB "quick check" on the two repeated prefix bytes for every session-key decryption, to close the Mister-Zuccherato oracle on the path a PKESK session key reaches, but the same class also carries password-derived session keys, where reporting the check is what identifies a wrong passphrase and lets the next passphrase or SKESK packet be tried. A SKESK v4 packet deriving the session key from the S2K output directly (no encrypted session key) yields a well formed session key for any passphrase, so a wrong one no longer failed at all: it surfaced as a parse or integrity failure further down the stream. BouncyCastle's own high-level API decrypts this way, so OpenPGPMessageProcessor took the first wrong passphrase offered for a success and never tried the remaining ones. A new PGPEncryptedDataList.extractSessionKeyEncryptedData(boolean) states whether the session key came from a password: true restores the check and with it the PGPDataValidationException on a wrong passphrase, the existing no-argument method goes on suppressing it, and the high-level API passes true on its passphrase paths alone, so a session key recovered from a public key operation is still never quick checked (github #2459).

  • Both copies of PKIXCertPathReviewer (org.bouncycastle.pkix.jcajce and the legacy org.bouncycastle.x509) took the first date-valid CRL issued by the certificate's issuer as an answer about that certificate, applying neither of the RFC 5280 sec. 6.3.3 rules that decide whether a CRL covers it: the (b)(2)(i) match between a name in the CRL's issuing distribution point and a name in the certificate's distribution point, and the (d) intersection of the revocation reasons the two assert. Only the (b)(2)(ii) to (iv) onlyContains booleans were applied. A CA-signed, in-date CRL with no entries, scoped to another distribution point or to a partition of the revocation reasons, was therefore reported as proof of non-revocation - isValidCertPath() true with an empty error list for a certificate its own CA had revoked for key compromise, where CertPathValidator("PKIX") rejects the same chain against the same trust anchor - and it suppressed the distribution point fetch that would otherwise have retrieved the authoritative CRL. Where the reviewer makes the trust decision rather than serving as diagnostics beside a real validation this is a revocation bypass, and SignedMailValidator (bcmail) reaches it with the CRLs carried inside the signed message. Both copies now apply the (b)(2)(i) name match and require a CRL to cover every revocation reason before it can settle the certificate's status, through the public PKIXCRLValidator helpers the validation engine already uses, and keep looking when a candidate does not qualify - falling back, as before, to the distribution point fetch and then to the existing "no valid CRL found" error. A CRL carrying no issuing distribution point, one naming the certificate's own distribution point, and one naming the certificate issuer (the distribution point the engine falls back to) are all still accepted.

  • RFC3280CertPathUtilities.checkCRL threw java.lang.NullPointerException rather than a CertPathValidatorException when every candidate CRL for a distribution point was skipped instead of rejected, which is what happens when the reasons a CRL covers add nothing to those already checked - the RFC 5280 sec. 6.3.3 (d) case - since the exception it rethrows is only ever recorded in a catch block. Validation failed closed either way, but outside the declared contract of CertPathValidator.validate(); a run with nothing recorded now reports "No valid CRL found.". All four copies are corrected: pkix, prov, and the prov jdk1.3 and jdk1.4 overlays.

... (truncated)

Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [org.bouncycastle:bcpkix-jdk18on](https://github.com/bcgit/bc-java) from 1.85 to 1.86.
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.md)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcpkix-jdk18on
  dependency-version: '1.86'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file java Pull requests that update Java code labels Oct 5, 2026
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file java Pull requests that update Java code labels Oct 5, 2026
@github-actions github-actions Bot added this to the 20.0.0 milestone Oct 5, 2026
@jbonofre
jbonofre merged commit e479da4 into main Oct 5, 2026
22 of 25 checks passed
@jbonofre
jbonofre deleted the dependabot/maven/org.bouncycastle-bcpkix-jdk18on-1.86 branch October 5, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file java Pull requests that update Java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant