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
Copy file name to clipboardExpand all lines: docs/releasenotes.html
+2Lines changed: 2 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -55,6 +55,7 @@ <h3>2.1.2 Defects Fixed</h3>
55
55
<li>NTRU reduced secret values with the % operator in three helpers whose reference implementations are deliberately division-free, so the reduction was carried out by an integer division whose latency depends on the secret operand. Polynomial.modQ(x, q) was x % q with a variable divisor, which no compiler can strength-reduce to a multiply the way it can a constant one, so it emitted a real division on every call - including on the decapsulation path, through rqToS3 and trinaryZqToZ3, where the dividend derives from the private key. It is now x & (q - 1), matching the reference MODQ macro; that is exact because q is always a power of two (NTRUParameterSet.q() returns 1 << logQ) and every call site passes a dividend already masked to 16 bits. Polynomial.mod3 (both the short and byte overloads) and NTRUSampling.mod3 used % 3 on the secret key polynomials f and g during key generation, on the message polynomials r and m during encapsulation, and on coefficients recovered during decapsulation; they now use the reference implementation's division-free fold-and-select, folding by 255, then 15, then 3 before a single masked conditional subtraction. All four sites produce byte-identical results to the previous code across their entire input domain, so keys, ciphertexts, shared secrets and the known-answer vectors are unchanged. Reported by the Robusta team.</li>
56
56
<li>JcePBMac1CalculatorBuilder.build() took the PBKDF2 iterationCount out of an RFC 9579 PBMAC1Params directly, unlike every sibling PBE/PBMAC1 path in the tree (the legacy-PBE branch of JcePKCS12MacCalculatorBuilderProvider, the BC-lightweight BcPKCS12PBMAC1CalculatorBuilder/PKCS12PBEUtils path, PKCS12PBMAC1KeyStoreSpi, and PKCS12Util.calculatePBMAC1), all of which already bound the count before deriving a key. Since PKCS12PfxPdu.isMacValid dispatches an id-PBMAC1 MacData to this builder, and the MAC key must be derived before the MAC can be checked, a PKCS#12 file with an attacker-chosen iteration count (up to 2^31-1) pinned a CPU core running PBKDF2-HMAC for as long as the count demanded before the (unauthenticated) file was ever rejected - a pre-authentication CPU denial of service from a file a few hundred bytes long. The same gap was reachable via JcePBMac1CalculatorProviderBuilder for a PBMAC1-protected CMP PBMProtectedPKIMessage. The iteration count is now bounded by the existing org.bouncycastle.pbe.max_iteration_count property (Properties.PBE_MAX_ITERATION_COUNT, default 10,000,000), throwing OperatorCreationException when exceeded, matching the cap already applied on every sibling path.</li>
57
57
<li>Locating the JRE's default trust store in BCJSSE was not done in a privileged block, so under a security manager the java.io.FilePermission for it was required of every protection domain on the call stack rather than of the provider alone. The read of the file was already privileged, but the java.io.File.exists() probes that find it - javax.net.ssl.trustStore, then ${java.home}/lib/security/jssecacerts, then cacerts - were not. In a container that grants the BC jars their own permissions while sandboxing application code, SSLContext.getDefault() (or SSLContext.getInstance("Default", "BCJSSE")) therefore failed with "Default SSL algorithm not found in JRE", caused by a KeyManagementException reporting access denied ("java.io.FilePermission" "...jssecacerts" "read"); wrapping the call in the application's own doPrivileged did not help, since the denied domain is on the stack either way, and because DefaultSSLContextSpi caches the outcome in a static holder the failure was permanent for the life of the JVM. The default key store path (javax.net.ssl.keyStore) had the same gap in ProvKeyManagerFactorySpi and additionally opened and closed its file unprivileged. Locating, opening and closing the default key and trust stores now all run inside AccessController.doPrivileged, so only the provider's own protection domain is consulted; behaviour without a security manager, and where the whole stack is trusted, is unchanged.</li>
58
+
<li>The byte[] and InputStream constructors of org.bouncycastle.tsp.ers.ERSEvidenceRecord, and the byte[] constructor of org.bouncycastle.tsp.ers.ERSArchiveTimeStamp, could let an unchecked exception from ASN.1 decoding escape on malformed input, past their declared TSPException / ERSException contract - an IllegalArgumentException from the getInstance type checks, an IllegalStateException where an optional tagged field (cryptoInfos or encryptionInfo in an EvidenceRecord, or the digestAlgorithm, attributes or reducedHashtree of an ArchiveTimeStamp) carries a primitive encoding in place of the implicit constructed form, or an ArrayIndexOutOfBoundsException where an EvidenceRecord SEQUENCE carried fewer than three elements. Evidence records are third-party artefacts parsed before any signature over them has been checked, so a caller that had correctly handled the documented exceptions still saw an unchecked one propagate. All three constructors now report malformed input as an ERSException with the original exception preserved as its cause; well-formed RFC 4998 evidence records and archive time-stamps are unaffected.</li>
58
59
<li>EC scalar multiplication of a variable point by a secret scalar previously ran on the curve's default windowed-NAF multiplier, whose branch pattern, table indexing and doubling-run lengths all depend on the scalar, exposing private-key-derived material through timing and memory-access side channels. Every secret-scalar point multiplication now runs on the new constant-time multiplier (see Additional Features below) via ECAlgorithms.multiplySecret. This change is a hardening: no practical key recovery has been demonstrated against these code paths, but also note that on curves without custom fixed-limb field implementations (the brainpool and GOST curves, or a caller-defined curve) the field arithmetic beneath the point operations remains BigInteger-based, so its timing can still vary with operand values there. Reported by the Robusta team.</li>
59
60
<li>Sorting the elements of a SET was an insertion sort that re-derived an element's DER encoding every time it was shifted, so ordering N elements cost O(N^2) encodings rather than O(N) encodings compared O(N log N) times. Input already in descending order is that sort's worst case, every insertion shifting the whole placed prefix, and the sort is reached from toDERObject() / getEncoded(ASN1Encoding.DER) and equals() - which for CMS covers the DER re-encode of the signed attributes performed as an input to signature verification, i.e. before the signature has been checked. Each element is now encoded once and the encodings ordered with a stable O(N log N) sort, via the new org.bouncycastle.util.Arrays.sort(Object[], Comparator) wrapper. Measured on 20,000 elements, the DER re-encode drops from 1.9s to 7ms for descending input and from 1.5s to 20ms for random input, while input already in ascending order - which is what conformant DER looks like - stays as fast as it was. Elements whose encodings compare equal keep their relative order, as they did before, and the ordering produced is unchanged in every case.</li>
60
61
<li>The ZUC EIA3 message authentication codes (org.bouncycastle.crypto.macs.Zuc128Mac and Zuc256Mac) accumulated the keystream contribution of each message bit inside a branch on that bit, so the work done - and the time taken - was proportional to the Hamming weight of the message and to how predictable its bit pattern was. Measured over a 2KB message, throughput varied by a factor of 3.7 (Zuc128Mac) and 5.2 (Zuc256Mac) between an all-zero and a random message. The contribution is now accumulated branchlessly by masking with the bit, so the cost no longer depends on the message content; measured throughput is flat across all-zero, random and all-ones inputs. MAC values are unchanged, and the 3GPP test vectors are unaffected. The cost is now uniformly that of the previous worst case: a message whose bits are mostly clear takes longer to authenticate than it did before, while one with a high or unpredictable proportion of set bits takes the same or less.</li>
@@ -65,6 +66,7 @@ <h3>2.1.2 Defects Fixed</h3>
65
66
<li>The PBKDF2 keyLength carried in a BCFKS keystore and in an RFC 9579 PBMAC1-protected PKCS#12 file was used unbounded, even though the iteration count beside it is capped: it sizes the key derivation output, so an attacker-supplied value drove an arbitrarily long derivation from a file of a couple of hundred bytes, before the MAC over that file had been checked. The value was additionally multiplied by 8 to convert to bits, which overflows for a large enough keyLength and turned KeyStore.load into an unchecked NegativeArraySizeException escaping its declared IOException. The keyLength is now bounded at all six sites - BcFKSKeyStoreSpi's scrypt and PBKDF2 MAC key derivations, both PKCS#12 keystore SPIs, the pkcs PKCS12Util PBMAC1 verification and JcePBMac1CalculatorBuilder - with the bound applied before deriving, matching the existing iteration-count caps.</li>
66
67
<li>ASN1BMPString was the last ASN.1 primitive to size a buffer from the declared length before reading any content: its parse path allocated a char[] for the whole declared length up front, so an eight-byte header could drive an allocation as large as the parser's length bound (the max heap, for a stream whose size cannot be discovered) and raise an OutOfMemoryError - an Error, so it escaped the IOException the parse API declares, and it worked nested inside any structure arriving off the wire. The content is now read through DefiniteLengthInputStream.toByteArray, which grows its buffer as bytes actually arrive, so a truncated BMPString fails promptly with the usual truncation error. Every other primitive already routed through that path (github #2338).</li>
67
68
<li>KeyBoxByteBuffer.rangeOf checked its range with "end - start < 0 || start < 0", which a sufficiently negative end slips past: the subtraction overflows to a positive value (start = 1 with end = Integer.MIN_VALUE wraps to Integer.MAX_VALUE), clearing that guard and the buffer-limit check below it, so a 38-byte keybox file reached new byte[end - start] and allocated 2GB. end is now checked on its own. The keybox test's existing "End is negative" case passed on the subtraction alone and did not cover this, so a wrapping case was added alongside.</li>
69
+
<li>The element-count check in org.bouncycastle.asn1.tsp.EvidenceRecord read "sequence.size() < 3 && sequence.size() > 5", a condition no value can satisfy, so the check never fired. RFC 4998 sec. 4 gives EvidenceRecord three mandatory fields (version, digestAlgorithms, archiveTimeStampSequence) and two optional ones, and the constructor reads getObjectAt(0), getObjectAt(1) and getObjectAt(size - 1) unconditionally, so a SEQUENCE carrying fewer than three elements raised ArrayIndexOutOfBoundsException instead of the IllegalArgumentException the getInstance contract documents. The check now reads "< 3 || > 5"; sequences of three to five elements are unaffected.</li>
68
70
<li>PGPSecretKeyParser could not terminate on a truncated GnuPG extended key expression. The header loop exits only on reading a "Key" header, and consumeUntil returned void, so it could not distinguish the ':' delimiter from end-of-input and spun forever accumulating nothing. A zero-byte stream was the cheapest trigger of all, because isExtendedSExpression read -1 and reported "extended". End-of-input is no longer treated as an extended expression, and a header list that ends before the Key header is now rejected with an IOException.</li>
69
71
<li>An MLS leaf_index is a uint32 on the wire held in a signed int, and NodeIndex(LeafIndex) doubled it with an int multiply, which wraps for any index at or above 2^30. The resulting negative node index then compared as less than every signed bound it was checked against, so LeafIndex.directPath - which has no bound check of its own, unlike its NodeIndex counterpart - never reached the root and accumulated nodes until the heap was exhausted, and LeafIndex.commonAncestor spun forever on the mixed-sign values. An index of exactly 2^31 was worse than a hang: it wrapped to node 0, so it aliased a real leaf rather than being refused. The doubling is now done as unsigned long arithmetic and directPath bounds its argument, so an out-of-tree index - whether it wrapped or is simply larger than the tree - is refused rather than looped on. The decode constructor deliberately still accepts the full uint32 range, since the RFC 9420 "messages" test vector requires every value to round-trip; the range is enforced where the index is used. This continues the unsigned-comparison hardening of the previous release.</li>
70
72
<li>MLS proposal validation for an external commit (a NEW_MEMBER_COMMIT sender) only counted the Remove proposals in the list - unlike the normal path, it never validated one - so a removed leaf index reached applyRemove straight from the wire, and one naming a leaf outside the tree escaped as an unchecked exception out of Group.handle instead of being rejected as an invalid proposal list. Both paths now share a bounds check on the removed index. The external path still cannot use the normal validateRemove, which additionally rejects self-removes: an external resync Commit legitimately removes the sender's own former leaf.</li>
0 commit comments