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
+1Lines changed: 1 addition & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -57,6 +57,7 @@ <h3>2.1.2 Defects Fixed</h3>
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
58
<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
59
<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
+
<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>
60
61
<li>The bcrypt round count in an encrypted OpenSSH v1 private key was used unbounded. It is read from the key's own kdfoptions and drives the KDF before anything about the key has been verified, and a round costs several milliseconds, so the 2^31-1 the wire format allows is worth CPU-months from a key file of a few hundred bytes - the OpenSSH analogue of the caps already applied to the PKCS#12, BCFKS, BKS and PBES2 iteration counts, and the only member of that family with no bound at all. It is now capped by the new org.bouncycastle.openssh.max_rounds property (Properties.OPENSSH_MAX_ROUNDS), default 1048576 against ssh-keygen's default of 16, throwing IllegalArgumentException when exceeded. Reached only when a passphrase is supplied, so this is the key-import path rather than passive parsing.</li>
61
62
<li>The OER decoder skips an extension it has no definition for by reading its declared length one byte at a time, and the loop ignored the -1 that InputStream.read returns at end of stream. The length is taken from the encoding and bounded only by 2^31-1 - it is the one length consumer in OERInputStream that does not go through the decoder's allocation ceiling - so a ten-byte payload declaring a large unknown extension burned around 40 seconds of CPU per extension slot, and, since each known extension is parsed from its own bounded stream, the slots are independently exhaustible. Worst of all the parse then returned normally, so nothing recorded that it had happened. The extension body is now consumed in bounded chunks and a truncated one is rejected with an EOFException at the genuine end of input. Reachable from the public ITS/ETSI entry points that decode OER, such as new ETSISignedData(byte[]).</li>
62
63
<li>The SNOVA signer's linear solve was not constant-time. SnovaSigner.performGaussianElimination searched downward for the first non-zero pivot, swapped rows when it found one lower down, and skipped the row-add when the elimination factor was zero — three branches on the Gauss matrix, which is derived from the private key and the per-signature vinegar, so the branch pattern leaked information about both. The elimination is now branchless: every lower row is added into the pivot row under a mask that is all-ones only while the pivot is still zero, the singular case is accumulated and tested once after the loop instead of returning early, and the zero-factor guard is dropped (a zero factor already makes the multiply a no-op). Signature and key output are byte-for-byte identical — verified against the known-answer vectors for all 44 SNOVA parameter sets. This completes SNOVA's secret-path constant-time work: the secret-indexed GF(16) multiplication and inverse tables were already removed when the GF(16) arithmetic was consolidated onto the constant-time shared implementation. The only residual variable-time behaviour is the rejection-resample when a random linear system is singular, a rare and key-independent event, as in the SNOVA reference.</li>
0 commit comments