This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
The following commit(s) were added to refs/heads/master by this push:
new 32887ed02 Follow-on from last DER patch (#726)
32887ed02 is described below
commit 32887ed02b276f797ad2e5bac8c1bc287d4eec13
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Tue Sep 22 09:28:40 2026 +0100
Follow-on from last DER patch (#726)
---
src/site/asciidoc/migration.adoc | 1 +
src/site/asciidoc/wss4j40.adoc | 67 +++++++++
.../org/apache/wss4j/common/crypto/CryptoBase.java | 34 +++--
.../org/apache/wss4j/common/crypto/Merlin.java | 29 +++-
.../org/apache/wss4j/common/crypto/MerlinAKI.java | 39 ++++-
...CastleUtils.java => X509KeyIdentifierUtil.java} | 60 ++++++--
.../org/apache/wss4j/common/token/DOMX509SKI.java | 17 ++-
.../common/crypto/AuthorityKeyIdentifierTest.java | 20 +--
.../apache/wss4j/common/crypto/DERDecoderTest.java | 166 +++++++++++++++++++++
.../common/crypto/MalformedKeyIdentifierTest.java | 154 +++++++++++++++++++
.../wss4j/common/crypto/NameConstraintsTest.java | 46 ++++++
.../org/apache/wss4j/common/crypto/SKITest.java | 48 ++++++
12 files changed, 642 insertions(+), 39 deletions(-)
diff --git a/src/site/asciidoc/migration.adoc b/src/site/asciidoc/migration.adoc
index 2b1245b3a..bc474ff56 100644
--- a/src/site/asciidoc/migration.adoc
+++ b/src/site/asciidoc/migration.adoc
@@ -21,6 +21,7 @@
Information about migrating to various new versions of WSS4J is provided in
this section.
+include::wss4j40.adoc[]
include::wss4j22.adoc[]
include::wss4j21.adoc[]
include::wss4j20.adoc[]
diff --git a/src/site/asciidoc/wss4j40.adoc b/src/site/asciidoc/wss4j40.adoc
new file mode 100644
index 000000000..9d25ca610
--- /dev/null
+++ b/src/site/asciidoc/wss4j40.adoc
@@ -0,0 +1,67 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+=== Apache WSS4J 4.0.0 Migration Guide
+
+This section is a migration guide for helping Apache WSS4J 3.0.x users to
migrate
+to the 4.0.x releases.
+
+==== Stricter DER decoding of X.509 key identifiers
+
+The SubjectKeyIdentifier (2.5.29.14) and AuthorityKeyIdentifier (2.5.29.35)
+extensions are no longer decoded with Bouncy Castle. WSS4J now decodes them
+itself, which removes the last compile-time dependency on the optional Bouncy
+Castle artifacts from these code paths.
+
+RFC 5280 requires certificate extensions to be DER encoded, and the WSS4J
decoder
+enforces that. Bouncy Castle accepted the broader BER encoding, so a
certificate
+that is not strictly DER encoded may now be rejected where it previously
worked.
+The encodings that change behaviour are:
+
+* a length in long form where the short form would do (for example `81 03`
+ instead of `03`), or a length carrying a leading zero byte;
+* a SubjectKeyIdentifier encoded as a constructed OCTET STRING (tag `24`) whose
+ segments Bouncy Castle would have concatenated;
+* an AuthorityKeyIdentifier `keyIdentifier` encoded as a constructed
+ `[0]` (tag `A0`) rather than a primitive one (tag `80`);
+* an AuthorityKeyIdentifier SEQUENCE using indefinite length;
+* an AuthorityKeyIdentifier whose fields appear out of order, or which repeats
+ the `[0]` field.
+
+Such a certificate now causes an `IllegalArgumentException` from
+`BouncyCastleUtils.getSubjectKeyIdentifierBytes` and
+`BouncyCastleUtils.getAuthorityKeyIdentifierBytes`, and a `WSSecurityException`
+from `Crypto.getSKIBytesFromCert`. This surfaces when signing with an SKI key
+identifier (`DOMX509SKI`) and when establishing trust with `MerlinAKI`.
+
+If you hit this, the certificate is malformed and should be reissued. There is
no
+configuration option to restore the previous tolerance.
+
+==== Crypto.getSKIBytesFromCert shares the key identifier decoder
+
+`CryptoBase.getSKIBytesFromCert` previously had its own SubjectKeyIdentifier
+decoder, which was more tolerant than the one used to emit an SKI key
identifier.
+Both now use the same decoder, so the identifier WSS4J writes and the one it
+accepts agree. A certificate whose SubjectKeyIdentifier has trailing bytes
after
+the extension value, or whose inner OCTET STRING is not exactly the length the
+wrapper declares, is now rejected with a `WSSecurityException` instead of being
+decoded on a best-effort basis.
+
+The behaviour for well formed certificates is unchanged, as is the SHA-1
fallback
+for certificates that carry no SubjectKeyIdentifier extension.
diff --git
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/CryptoBase.java
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/CryptoBase.java
index 7279e75f1..3a00688b9 100644
---
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/CryptoBase.java
+++
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/CryptoBase.java
@@ -208,6 +208,8 @@ public abstract class CryptoBase implements Crypto {
*
* @param cert The certificate to read SKI
* @return The byte array containing the binary SKI data
+ * @throws WSSecurityException if the SubjectKeyIdentifier extension is
present but is not
+ * valid DER, or if no SKI extension is present and SHA-1 is
unavailable
*/
public byte[] getSKIBytesFromCert(X509Certificate cert) throws
WSSecurityException {
//
@@ -232,15 +234,18 @@ public abstract class CryptoBase implements Crypto {
}
//
- // Strip away first (four) bytes from the DerValue (tag and length of
- // ExtensionValue OCTET STRING and KeyIdentifier OCTET STRING)
+ // Strip away the tag and length of the ExtensionValue OCTET STRING
and of the
+ // KeyIdentifier OCTET STRING it wraps. X509KeyIdentifierUtil is the
single decoder for
+ // this extension, so that the identifier we emit and the one we
accept agree.
//
- DERDecoder extVal = new DERDecoder(derEncodedValue);
- extVal.expect(DERDecoder.TYPE_OCTET_STRING); // ExtensionValue OCTET
STRING
- extVal.getLength();
- extVal.expect(DERDecoder.TYPE_OCTET_STRING); // KeyIdentifier OCTET
STRING
- int keyIDLen = extVal.getLength();
- return extVal.getBytes(keyIDLen);
+ try {
+ return
X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(derEncodedValue);
+ } catch (IllegalArgumentException ex) {
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.UNSUPPORTED_SECURITY_TOKEN, ex,
"noSKIHandling",
+ new Object[] {"Invalid SubjectKeyIdentifier certificate
extension"}
+ );
+ }
}
/**
@@ -387,8 +392,10 @@ public abstract class CryptoBase implements Crypto {
* that it is a single byte comparison, the performance hit is negligible.
*
* @param cert the certificate to extract NameConstraints from
- * @return the NameConstraints, or null if not present
- * @throws WSSecurityException if a processing error occurs decoding the
Octet String
+ * @return the DER-encoded NameConstraints sequence, or an empty array if
the certificate
+ * carries no NameConstraints extension
+ * @throws WSSecurityException if a processing error occurs decoding the
Octet String, or if
+ * the extension is tagged as something other than a SEQUENCE or
an OCTET STRING
*/
protected byte[] getNameConstraints(final X509Certificate cert) throws
WSSecurityException {
byte[] bytes = cert.getExtensionValue(NAME_CONSTRAINTS_OID);
@@ -405,8 +412,11 @@ public abstract class CryptoBase implements Crypto {
case DERDecoder.TYPE_SEQUENCE:
return bytes;
default:
- throw new IllegalArgumentException(
- "Invalid type for NameConstraints; must be Sequence or
OctetString-encoded Sequence");
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.FAILURE, "certpath",
+ new Object[] {"Invalid type for NameConstraints; must be a
SEQUENCE or an "
+ + "OCTET STRING-encoded SEQUENCE"}
+ );
}
}
}
diff --git
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/Merlin.java
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/Merlin.java
index 2fa2f204b..e75628382 100644
---
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/Merlin.java
+++
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/Merlin.java
@@ -1429,8 +1429,7 @@ public class Merlin extends CryptoBase {
(X509Certificate) keyStore.getCertificate(alias);
if (cert != null) {
if (certProviderHandlesNameConstraints) {
- TrustAnchor anchor = new TrustAnchor(cert,
getNameConstraints(cert));
- set.add(anchor);
+ set.add(createTrustAnchor(cert, alias));
} else {
TrustAnchor anchor = new TrustAnchor(cert, null);
set.add(anchor);
@@ -1439,6 +1438,32 @@ public class Merlin extends CryptoBase {
}
}
+ /**
+ * Build a {@code TrustAnchor} carrying the certificate's NameConstraints.
+ * <p>
+ * {@code getNameConstraints} reports "no constraints" as an empty array,
which
+ * {@code TrustAnchor} rejects; it wants null. It also signals constraints
it cannot decode
+ * with an IllegalArgumentException, which would otherwise escape a method
declared to throw
+ * WSSecurityException.
+ *
+ * @param cert the certificate to anchor trust in
+ * @param alias the keystore alias it was read from, used to identify it
in any error
+ */
+ private TrustAnchor createTrustAnchor(X509Certificate cert, String alias)
throws WSSecurityException {
+ byte[] nameConstraints = getNameConstraints(cert);
+ if (nameConstraints != null && nameConstraints.length == 0) {
+ nameConstraints = null; //NOPMD - TrustAnchor reads null, not
empty, as "unconstrained"
+ }
+ try {
+ return new TrustAnchor(cert, nameConstraints);
+ } catch (IllegalArgumentException ex) {
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.FAILURE, ex, "certpath",
+ new Object[] {"Invalid NameConstraints extension on keystore
entry " + alias}
+ );
+ }
+ }
+
/**
* Get an implementation-specific identifier that corresponds to the
X509Certificate. In
* this case, the identifier is the KeyStore alias.
diff --git
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/MerlinAKI.java
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/MerlinAKI.java
index 5e77f8abc..27bf7d7bf 100644
---
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/MerlinAKI.java
+++
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/MerlinAKI.java
@@ -124,8 +124,7 @@ public class MerlinAKI extends Merlin {
String issuerString = certs[0].getIssuerX500Principal().getName();
try {
if (certs.length == 1) {
- byte[] keyIdentifierBytes =
- BouncyCastleUtils.getAuthorityKeyIdentifierBytes(certs[0]);
+ byte[] keyIdentifierBytes =
getAuthorityKeyIdentifier(certs[0]);
X509Certificate[] foundCerts =
getX509CertificatesFromKeyIdentifier(keyIdentifierBytes);
// If the certs have not been found, the issuer is not in the
keystore/truststore
@@ -248,7 +247,7 @@ public class MerlinAKI extends Merlin {
if (certs != null && certs.length > 0 && certs[0] instanceof
X509Certificate) {
byte[] subjectKeyIdentifier =
-
BouncyCastleUtils.getSubjectKeyIdentifierBytes((X509Certificate)certs[0]);
+ getSubjectKeyIdentifier((X509Certificate)certs[0],
alias);
if (subjectKeyIdentifier != null
&& Arrays.equals(subjectKeyIdentifier, keyIdentifier))
{
return certs;
@@ -263,4 +262,38 @@ public class MerlinAKI extends Merlin {
return new Certificate[]{};
}
+ /**
+ * Read the AuthorityKeyIdentifier of a received certificate.
+ * <p>
+ * The certificate is attacker supplied, so a malformed extension is
reported as a
+ * WSSecurityException rather than the IllegalArgumentException the
decoder raises. Otherwise
+ * an unchecked exception escapes a method declared to throw
WSSecurityException.
+ */
+ private static byte[] getAuthorityKeyIdentifier(X509Certificate cert)
throws WSSecurityException {
+ try {
+ return X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(cert);
+ } catch (IllegalArgumentException ex) {
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.FAILURE, ex, "certpath",
+ new Object[] {"Invalid AuthorityKeyIdentifier certificate
extension"}
+ );
+ }
+ }
+
+ /**
+ * Read the SubjectKeyIdentifier of a certificate held in the keystore or
truststore.
+ * <p>
+ * A certificate we cannot decode cannot match the identifier we are
looking for, so the entry
+ * is skipped rather than failing the whole lookup. Trust still fails if
no other entry
+ * matches.
+ */
+ private static byte[] getSubjectKeyIdentifier(X509Certificate cert, String
alias) {
+ try {
+ return X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(cert);
+ } catch (IllegalArgumentException ex) {
+ LOG.debug("Skipping alias {} with an invalid SubjectKeyIdentifier
extension", alias, ex);
+ return null; //NOPMD - no usable identifier is distinct from an
empty one
+ }
+ }
+
}
diff --git
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/BouncyCastleUtils.java
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/X509KeyIdentifierUtil.java
similarity index 59%
rename from
ws-security-common/src/main/java/org/apache/wss4j/common/crypto/BouncyCastleUtils.java
rename to
ws-security-common/src/main/java/org/apache/wss4j/common/crypto/X509KeyIdentifierUtil.java
index 521a47702..68aa4a4e5 100644
---
a/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/BouncyCastleUtils.java
+++
b/ws-security-common/src/main/java/org/apache/wss4j/common/crypto/X509KeyIdentifierUtil.java
@@ -23,15 +23,33 @@ import java.security.cert.X509Certificate;
import org.apache.wss4j.common.ext.WSSecurityException;
-public final class BouncyCastleUtils {
- private static final byte TYPE_CONTEXT_SPECIFIC_0 = (byte)0x80;
- private static final byte TYPE_CONTEXT_SPECIFIC_1 = (byte)0xA1;
- private static final byte TYPE_CONTEXT_SPECIFIC_2 = (byte)0x82;
+/**
+ * Decodes the X.509 key identifier extensions.
+ */
+public final class X509KeyIdentifierUtil {
+ /** AuthorityKeyIdentifier keyIdentifier [0] IMPLICIT KeyIdentifier -
primitive, context-specific 0. */
+ private static final byte TAG_KEY_IDENTIFIER = (byte)0x80;
+ /** AuthorityKeyIdentifier authorityCertIssuer [1] GeneralNames -
constructed, context-specific 1. */
+ private static final byte TAG_AUTHORITY_CERT_ISSUER = (byte)0xA1;
+ /** AuthorityKeyIdentifier authorityCertSerialNumber [2] IMPLICIT
CertificateSerialNumber -
+ * primitive, context-specific 2. */
+ private static final byte TAG_AUTHORITY_CERT_SERIAL_NUMBER = (byte)0x82;
- private BouncyCastleUtils() {
+ private X509KeyIdentifierUtil() {
// complete
}
+ /**
+ * Read the keyIdentifier of the AuthorityKeyIdentifier extension
(2.5.29.35) of the
+ * given certificate.
+ * <p>
+ * X.509 extensions are required to use DER; BER encodings are rejected.
+ *
+ * @param cert the certificate to read the AuthorityKeyIdentifier from.
+ * @return an empty array if the certificate has no AuthorityKeyIdentifier
extension, null if
+ * the extension is present but carries no keyIdentifier,
otherwise the keyIdentifier.
+ * @throws IllegalArgumentException if the extension is present but is not
valid DER.
+ */
public static byte[] getAuthorityKeyIdentifierBytes(X509Certificate cert) {
byte[] extensionValue = cert.getExtensionValue("2.5.29.35"); //NOPMD
if (extensionValue == null) {
@@ -40,6 +58,16 @@ public final class BouncyCastleUtils {
return getAuthorityKeyIdentifierBytes(extensionValue);
}
+ /**
+ * Read the SubjectKeyIdentifier extension (2.5.29.14) of the given
certificate.
+ * <p>
+ * X.509 extensions are required to use DER; BER encodings are rejected.
+ *
+ * @param cert the certificate to read the SubjectKeyIdentifier from.
+ * @return an empty array if the certificate has no SubjectKeyIdentifier
extension, otherwise
+ * the key identifier.
+ * @throws IllegalArgumentException if the extension is present but is not
valid DER.
+ */
public static byte[] getSubjectKeyIdentifierBytes(X509Certificate cert) {
byte[] extensionValue = cert.getExtensionValue("2.5.29.14"); //NOPMD
if (extensionValue == null) {
@@ -55,9 +83,10 @@ public final class BouncyCastleUtils {
return null; //NOPMD - AuthorityKeyIdentifier#getKeyIdentifier
returns null when absent
}
DERDecoder authorityKeyIdentifier = new DERDecoder(extensionBytes);
- byte[] keyIdentifier = readOptionalValue(authorityKeyIdentifier,
TYPE_CONTEXT_SPECIFIC_0);
- readOptionalValue(authorityKeyIdentifier, TYPE_CONTEXT_SPECIFIC_1);
- readOptionalValue(authorityKeyIdentifier, TYPE_CONTEXT_SPECIFIC_2);
+ byte[] keyIdentifier = readOptionalValue(authorityKeyIdentifier,
TAG_KEY_IDENTIFIER);
+ // The remaining fields are not used by WSS4J, but must still be
well formed.
+ skipOptionalValue(authorityKeyIdentifier,
TAG_AUTHORITY_CERT_ISSUER);
+ skipOptionalValue(authorityKeyIdentifier,
TAG_AUTHORITY_CERT_SERIAL_NUMBER);
authorityKeyIdentifier.expectEnd();
return keyIdentifier;
} catch (WSSecurityException ex) {
@@ -90,7 +119,7 @@ public final class BouncyCastleUtils {
}
private static byte[] readOptionalValue(DERDecoder decoder, byte type)
throws WSSecurityException {
- if (!decoder.hasRemaining() || !decoder.test(type)) {
+ if (!startsWith(decoder, type)) {
return null; //NOPMD - an absent optional value is distinct from
an empty value
}
decoder.expect(type);
@@ -98,4 +127,17 @@ public final class BouncyCastleUtils {
return decoder.getBytes(length);
}
+ private static void skipOptionalValue(DERDecoder decoder, byte type)
throws WSSecurityException {
+ if (!startsWith(decoder, type)) {
+ return;
+ }
+ decoder.expect(type);
+ int length = decoder.getLength();
+ decoder.skip(length);
+ }
+
+ private static boolean startsWith(DERDecoder decoder, byte type) throws
WSSecurityException {
+ return decoder.hasRemaining() && decoder.test(type);
+ }
+
}
diff --git
a/ws-security-common/src/main/java/org/apache/wss4j/common/token/DOMX509SKI.java
b/ws-security-common/src/main/java/org/apache/wss4j/common/token/DOMX509SKI.java
index b9bfc2476..0de945499 100644
---
a/ws-security-common/src/main/java/org/apache/wss4j/common/token/DOMX509SKI.java
+++
b/ws-security-common/src/main/java/org/apache/wss4j/common/token/DOMX509SKI.java
@@ -20,7 +20,8 @@
package org.apache.wss4j.common.token;
import org.apache.wss4j.common.WSS4JConstants;
-import org.apache.wss4j.common.crypto.BouncyCastleUtils;
+import org.apache.wss4j.common.crypto.X509KeyIdentifierUtil;
+import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.util.DOM2Writer;
import org.w3c.dom.Document;
import org.apache.wss4j.common.util.XMLUtils;
@@ -38,9 +39,19 @@ public final class DOMX509SKI {
/**
* Constructor.
+ *
+ * @throws WSSecurityException if the certificate's SubjectKeyIdentifier
extension is present
+ * but is not valid DER.
*/
- public DOMX509SKI(Document doc, X509Certificate remoteCertificate) {
- skiBytes =
BouncyCastleUtils.getSubjectKeyIdentifierBytes(remoteCertificate);
+ public DOMX509SKI(Document doc, X509Certificate remoteCertificate) throws
WSSecurityException {
+ try {
+ skiBytes =
X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(remoteCertificate);
+ } catch (IllegalArgumentException ex) {
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.UNSUPPORTED_SECURITY_TOKEN, ex,
"noSKIHandling",
+ new Object[] {"Invalid SubjectKeyIdentifier certificate
extension"}
+ );
+ }
element = doc.createElementNS(WSS4JConstants.SIG_NS, "ds:X509SKI");
element.setTextContent(
diff --git
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/AuthorityKeyIdentifierTest.java
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/AuthorityKeyIdentifierTest.java
index ff0f0d340..95c6e1a37 100644
---
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/AuthorityKeyIdentifierTest.java
+++
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/AuthorityKeyIdentifierTest.java
@@ -55,7 +55,7 @@ public class AuthorityKeyIdentifierTest {
assertNotNull(cert);
// Get AuthorityKeyIdentifier from the cert
- byte[] keyIdentifierBytes =
BouncyCastleUtils.getAuthorityKeyIdentifierBytes(cert);
+ byte[] keyIdentifierBytes =
X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(cert);
assertNotNull(keyIdentifierBytes);
// Now load the CA cert
@@ -67,7 +67,7 @@ public class AuthorityKeyIdentifierTest {
// Get SubjectKeyIdentifier from the CA cert
byte[] subjectKeyIdentifierBytes =
- BouncyCastleUtils.getSubjectKeyIdentifierBytes(caCert);
+ X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(caCert);
assertNotNull(subjectKeyIdentifierBytes);
assertTrue(Arrays.equals(keyIdentifierBytes,
subjectKeyIdentifierBytes));
@@ -81,11 +81,11 @@ public class AuthorityKeyIdentifierTest {
assertArrayEquals(
expectedKeyIdentifier,
-
BouncyCastleUtils.getAuthorityKeyIdentifierBytes(authorityKeyIdentifier)
+
X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(authorityKeyIdentifier)
);
assertArrayEquals(
expectedKeyIdentifier,
-
BouncyCastleUtils.getSubjectKeyIdentifierBytes(subjectKeyIdentifier)
+
X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(subjectKeyIdentifier)
);
}
@@ -95,9 +95,9 @@ public class AuthorityKeyIdentifierTest {
byte[] authorityIssuerAndSerial = {4, 10, 48, 8, (byte)0xA1, 3, 48, 1,
0, (byte)0x82, 1, 1};
byte[] emptyAuthorityKeyIdentifier = {4, 2, 48, 0};
-
assertNull(BouncyCastleUtils.getAuthorityKeyIdentifierBytes(authorityKeyIdentifier));
-
assertNull(BouncyCastleUtils.getAuthorityKeyIdentifierBytes(authorityIssuerAndSerial));
-
assertNull(BouncyCastleUtils.getAuthorityKeyIdentifierBytes(emptyAuthorityKeyIdentifier));
+
assertNull(X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(authorityKeyIdentifier));
+
assertNull(X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(authorityIssuerAndSerial));
+
assertNull(X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(emptyAuthorityKeyIdentifier));
}
@Test
@@ -108,15 +108,15 @@ public class AuthorityKeyIdentifierTest {
assertThrows(
IllegalArgumentException.class,
- () ->
BouncyCastleUtils.getSubjectKeyIdentifierBytes(truncatedSubjectKeyIdentifier)
+ () ->
X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(truncatedSubjectKeyIdentifier)
);
assertThrows(
IllegalArgumentException.class,
- () ->
BouncyCastleUtils.getAuthorityKeyIdentifierBytes(trailingAuthorityKeyIdentifier)
+ () ->
X509KeyIdentifierUtil.getAuthorityKeyIdentifierBytes(trailingAuthorityKeyIdentifier)
);
assertThrows(
IllegalArgumentException.class,
- () ->
BouncyCastleUtils.getSubjectKeyIdentifierBytes(indefiniteLengthSubjectKeyIdentifier)
+ () ->
X509KeyIdentifierUtil.getSubjectKeyIdentifierBytes(indefiniteLengthSubjectKeyIdentifier)
);
}
diff --git
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/DERDecoderTest.java
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/DERDecoderTest.java
new file mode 100644
index 000000000..2462b3cb5
--- /dev/null
+++
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/DERDecoderTest.java
@@ -0,0 +1,166 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.wss4j.common.crypto;
+
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for the bounds handling of DERDecoder. A DER length is
attacker-controlled wherever the
+ * decoder is pointed at a certificate extension, so an over-long or
non-minimal length must be
+ * rejected rather than acted upon.
+ */
+public class DERDecoderTest {
+
+ /**
+ * A declared length of Integer.MAX_VALUE from an eleven byte input. The
bounds check must not
+ * be computed as "pos + length", which overflows to a negative number and
lets the decoder
+ * through to a two gigabyte allocation.
+ */
+ @Test
+ public void testGetBytesRejectsLengthThatWouldOverflowThePosition() throws
Exception {
+ // 04 09 | 04 84 7FFFFFFF | 01 02 03 -- the shape
CryptoBase.getSKIBytesFromCert decodes
+ byte[] extension = {4, 9, 4, (byte)0x84, 0x7F, (byte)0xFF, (byte)0xFF,
(byte)0xFF, 1, 2, 3};
+
+ DERDecoder decoder = new DERDecoder(extension);
+ decoder.expect(DERDecoder.TYPE_OCTET_STRING);
+ decoder.getLength();
+ decoder.expect(DERDecoder.TYPE_OCTET_STRING);
+ int keyIdentifierLength = decoder.getLength();
+ assertEquals(Integer.MAX_VALUE, keyIdentifierLength);
+
+ assertThrows(WSSecurityException.class, () ->
decoder.getBytes(keyIdentifierLength));
+ }
+
+ @Test
+ public void testGetBytesRejectsLengthBeyondTheRemainingInput() throws
Exception {
+ DERDecoder decoder = new DERDecoder(new byte[] {1, 2, 3, 4});
+ decoder.skip(2);
+
+ assertArrayEquals(new byte[] {3, 4}, new DERDecoder(new byte[] {3,
4}).getBytes(2));
+ assertThrows(WSSecurityException.class, () -> decoder.getBytes(3));
+ }
+
+ @Test
+ public void testGetBytesRejectsNegativeLength() throws Exception {
+ DERDecoder decoder = new DERDecoder(new byte[] {1, 2, 3, 4});
+
+ assertThrows(WSSecurityException.class, () -> decoder.getBytes(-1));
+ }
+
+ /**
+ * Skipping past the end must fail rather than leave the position out of
bounds, where a large
+ * enough length would wrap it negative and turn a later read into an
ArrayIndexOutOfBounds.
+ */
+ @Test
+ public void testSkipRejectsLengthBeyondTheRemainingInput() throws
Exception {
+ DERDecoder decoder = new DERDecoder(new byte[] {1, 2, 3, 4});
+ decoder.skip(4);
+ assertThrows(WSSecurityException.class, () -> decoder.skip(1));
+
+ DERDecoder overflowing = new DERDecoder(new byte[] {1, 2, 3, 4});
+ overflowing.skip(2);
+ assertThrows(WSSecurityException.class, () ->
overflowing.skip(Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void testGetLengthReadsShortAndLongForm() throws Exception {
+ assertEquals(0, new DERDecoder(new byte[] {0}).getLength());
+ assertEquals(127, new DERDecoder(new byte[] {0x7F}).getLength());
+ assertEquals(128, new DERDecoder(new byte[] {(byte)0x81,
(byte)0x80}).getLength());
+ assertEquals(256, new DERDecoder(new byte[] {(byte)0x82, 1,
0}).getLength());
+ assertEquals(Integer.MAX_VALUE,
+ new DERDecoder(new byte[] {(byte)0x84, 0x7F, (byte)0xFF,
(byte)0xFF, (byte)0xFF}).getLength());
+ }
+
+ /**
+ * Indefinite length is reported as -1, as the method contract has always
stated. Every caller
+ * feeds the result to getBytes or skip, both of which reject a negative
length.
+ */
+ @Test
+ public void testGetLengthReportsIndefiniteLengthAsMinusOne() throws
Exception {
+ assertEquals(-1, new DERDecoder(new byte[] {(byte)0x80, 0,
0}).getLength());
+
+ DERDecoder decoder = new DERDecoder(new byte[] {(byte)0x80, 0, 0});
+ int length = decoder.getLength();
+ assertThrows(WSSecurityException.class, () ->
decoder.getBytes(length));
+ }
+
+ /**
+ * DER requires the shortest possible length encoding, so a long form that
could have been
+ * short, or one carrying a leading zero, is invalid.
+ */
+ @Test
+ public void testGetLengthRejectsNonMinimalEncodings() {
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x81, 0x7F}).getLength());
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x82, 0,
(byte)0x80}).getLength());
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x83, 0, 1,
0}).getLength());
+ }
+
+ @Test
+ public void testGetLengthRejectsLengthsThatDoNotFitInAnInt() {
+ // 0x80000000 is one past Integer.MAX_VALUE
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x84, (byte)0x80, 0, 0,
0}).getLength());
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x84, (byte)0xFF,
(byte)0xFF, (byte)0xFF, (byte)0xFF}).getLength());
+ // more length bytes than an int can hold
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x85, 1, 0, 0, 0,
0}).getLength());
+ }
+
+ @Test
+ public void testGetLengthRejectsTruncatedLengthSpecification() {
+ assertThrows(WSSecurityException.class, () -> new DERDecoder(new
byte[0]).getLength());
+ assertThrows(WSSecurityException.class,
+ () -> new DERDecoder(new byte[] {(byte)0x82, 1}).getLength());
+ }
+
+ @Test
+ public void testExpectEnd() throws Exception {
+ DERDecoder decoder = new DERDecoder(new byte[] {4, 1, 9});
+ decoder.expect(DERDecoder.TYPE_OCTET_STRING);
+ decoder.getBytes(decoder.getLength());
+ decoder.expectEnd();
+
+ DERDecoder trailing = new DERDecoder(new byte[] {4, 1, 9, 0});
+ trailing.expect(DERDecoder.TYPE_OCTET_STRING);
+ trailing.getBytes(trailing.getLength());
+ assertThrows(WSSecurityException.class, trailing::expectEnd);
+ }
+
+ @Test
+ public void testHasRemaining() throws Exception {
+ DERDecoder decoder = new DERDecoder(new byte[] {1, 2});
+ decoder.skip(1);
+ assertEquals(true, decoder.hasRemaining());
+ decoder.skip(1);
+ assertEquals(false, decoder.hasRemaining());
+
+ assertEquals(false, new DERDecoder(new byte[0]).hasRemaining());
+ }
+}
diff --git
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/MalformedKeyIdentifierTest.java
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/MalformedKeyIdentifierTest.java
new file mode 100644
index 000000000..b3616f37f
--- /dev/null
+++
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/MalformedKeyIdentifierTest.java
@@ -0,0 +1,154 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.wss4j.common.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.token.DOMX509SKI;
+import org.apache.wss4j.common.util.Loader;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * A certificate carrying a malformed key identifier extension reaches these
paths from the wire.
+ * The decoder signals a malformed extension with an IllegalArgumentException,
which must not
+ * escape a trust path declared to throw WSSecurityException.
+ */
+public class MalformedKeyIdentifierTest {
+
+ /**
+ * Self-signed, CN=malformed-aki. Its AuthorityKeyIdentifier extnValue is
+ * 04 08 30 05 80 03 01 02 03 00 -- a well formed SEQUENCE followed by a
trailing byte.
+ */
+ private static final String MALFORMED_AKI_CERT =
+
"MIIBuTCCASKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1tYWxmb3JtZWQtYWtp"
+ +
"MB4XDTI1MDEwMTAwMDAwMFoXDTM1MDEwMTAwMDAwMFowGDEWMBQGA1UEAwwNbWFsZm9ybWVkLWFr"
+ +
"aTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAq6urq6urq6urq6urq6urq6urq6urq6urq6ur"
+ +
"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur"
+ +
"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6sCAwEAAaMTMBEwDwYD"
+ +
"VR0jBAgwBYADAQIDADANBgkqhkiG9w0BAQsFAAOBgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+
+ /**
+ * Self-signed, CN=malformed-ski. Its SubjectKeyIdentifier extnValue is
+ * 04 09 04 84 7F FF FF FF 01 02 03 -- an inner OCTET STRING declaring
Integer.MAX_VALUE bytes.
+ */
+ private static final String MALFORMED_SKI_CERT =
+
"MIIBujCCASOgAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1tYWxmb3JtZWQtc2tp"
+ +
"MB4XDTI1MDEwMTAwMDAwMFoXDTM1MDEwMTAwMDAwMFowGDEWMBQGA1UEAwwNbWFsZm9ybWVkLXNr"
+ +
"aTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAq6urq6urq6urq6urq6urq6urq6urq6urq6ur"
+ +
"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur"
+ +
"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6sCAwEAAaMUMBIwEAYD"
+ +
"VR0OBAkEhH////8BAgMwDQYJKoZIhvcNAQELBQADgYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
+
+ /**
+ * A received certificate whose AuthorityKeyIdentifier will not decode
must fail trust
+ * verification with a WSSecurityException, not an unchecked exception.
+ */
+ @Test
+ public void testMerlinAKIRejectsMalformedAuthorityKeyIdentifier() throws
Exception {
+ X509Certificate cert = decode(MALFORMED_AKI_CERT, "2.5.29.35");
+
+ MerlinAKI crypto = new MerlinAKI();
+ crypto.setTrustStore(loadKeyStore("keys/wss40CA.jks", "security"));
+
+ WSSecurityException ex = assertThrows(
+ WSSecurityException.class,
+ () -> crypto.verifyTrust(new X509Certificate[]{cert}, false, null)
+ );
+ assertNotNull(ex);
+ }
+
+ /**
+ * A truststore entry whose SubjectKeyIdentifier will not decode is
skipped rather than
+ * failing the lookup. The certificate under test is not trusted, so every
alias is visited
+ * and the malformed entry is guaranteed to be reached; trust must then
fail with a
+ * WSSecurityException rather than an unchecked exception from the skipped
entry.
+ */
+ @Test
+ public void
testMerlinAKISkipsTruststoreEntryWithMalformedSubjectKeyIdentifier() throws
Exception {
+ X509Certificate untrusted =
(X509Certificate)loadKeyStore("keys/wss86.keystore", "security")
+ .getCertificate("wss86");
+ assertNotNull(untrusted);
+
+ KeyStore trustStore = loadKeyStore("keys/wss40CA.jks", "security");
+ trustStore.setCertificateEntry("malformed-ski",
decode(MALFORMED_SKI_CERT, "2.5.29.14"));
+
+ MerlinAKI crypto = new MerlinAKI();
+ crypto.setTrustStore(trustStore);
+
+ WSSecurityException ex = assertThrows(
+ WSSecurityException.class,
+ () -> crypto.verifyTrust(new X509Certificate[]{untrusted}, false,
null)
+ );
+ assertEquals(WSSecurityException.ErrorCode.FAILURE, ex.getErrorCode());
+ }
+
+ /**
+ * Writing an SKI key identifier for a certificate we cannot decode must
fail with a
+ * WSSecurityException rather than an unchecked exception.
+ */
+ @Test
+ public void testDOMX509SKIRejectsMalformedSubjectKeyIdentifier() throws
Exception {
+ X509Certificate cert = decode(MALFORMED_SKI_CERT, "2.5.29.14");
+
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ Document doc = factory.newDocumentBuilder().newDocument();
+
+ assertThrows(WSSecurityException.class, () -> new DOMX509SKI(doc,
cert));
+ }
+
+ private X509Certificate decode(String base64Certificate, String
expectedExtensionOid) throws Exception {
+ CertificateFactory certificateFactory =
CertificateFactory.getInstance("X.509");
+ X509Certificate cert;
+ try (InputStream input = new
ByteArrayInputStream(Base64.getDecoder().decode(base64Certificate))) {
+ cert =
(X509Certificate)certificateFactory.generateCertificate(input);
+ }
+ // a JDK that rejects the malformed extension outright leaves nothing
for us to decode
+ Assumptions.assumeTrue(cert.getExtensionValue(expectedExtensionOid) !=
null);
+ return cert;
+ }
+
+ private KeyStore loadKeyStore(String path, String password) throws
Exception {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ ClassLoader loader =
Loader.getClassLoader(MalformedKeyIdentifierTest.class);
+ try (InputStream input = Merlin.loadInputStream(loader, path)) {
+ keyStore.load(input, password.toCharArray());
+ }
+ return keyStore;
+ }
+}
diff --git
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/NameConstraintsTest.java
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/NameConstraintsTest.java
index f9b4a3b23..ed7132145 100644
---
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/NameConstraintsTest.java
+++
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/NameConstraintsTest.java
@@ -33,8 +33,10 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
+import java.util.HashSet;
import java.util.List;
import java.util.Properties;
+import java.util.Set;
import java.util.regex.Pattern;
import org.apache.wss4j.common.ext.WSSecurityException;
@@ -262,6 +264,50 @@ public class NameConstraintsTest {
getMerlinAkiBc());
}
+ /**
+ * getNameConstraints reports "no constraints" as an empty array, but
TrustAnchor reads an
+ * empty array as an undecodable extension and only accepts null. Most CA
certificates carry
+ * no NameConstraints, so building anchors for them must still work with
the
+ * cert.provider.nameconstraints property enabled.
+ */
+ @Test
+ public void testTrustAnchorsForCertificatesWithoutNameConstraints() throws
Exception {
+ assumeFalse(isIBMJdK);
+
+ Merlin merlin = merlinHandlingNameConstraints();
+ Set<TrustAnchor> anchors = new HashSet<>();
+
+ merlin.addTrustAnchors(anchors, getSelfKeyStore());
+
+ assertThat(anchors.size(), equalTo(1));
+ assertNull(anchors.iterator().next().getNameConstraints());
+ }
+
+ /**
+ * The constraints of a certificate that does carry the extension must
still reach the anchor.
+ */
+ @Test
+ public void testTrustAnchorsRetainNameConstraints() throws Exception {
+ assumeFalse(isIBMJdK);
+
+ Merlin merlin = merlinHandlingNameConstraints();
+ Set<TrustAnchor> anchors = new HashSet<>();
+
+ merlin.addTrustAnchors(anchors, getRootKeyStore());
+
+ assertThat(anchors.size(), equalTo(1));
+ TrustAnchor anchor = anchors.iterator().next();
+ assertNotNull(anchor.getNameConstraints());
+ assertThat("Tag byte is wrong", anchor.getNameConstraints()[0],
is(DERDecoder.TYPE_SEQUENCE));
+ }
+
+ private Merlin merlinHandlingNameConstraints() throws Exception {
+ Properties properties = new Properties();
+
properties.setProperty("org.apache.wss4j.crypto.merlin.cert.provider.nameconstraints",
+ "true");
+ return new Merlin(properties, this.getClass().getClassLoader(), null);
+ }
+
@Test
public void testNameConstraintsWithKeyStoreUsingMerlinBreaking() throws
Exception {
assumeFalse(isIBMJdK);
diff --git
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/SKITest.java
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/SKITest.java
index 14345287c..208a24611 100644
---
a/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/SKITest.java
+++
b/ws-security-common/src/test/java/org/apache/wss4j/common/crypto/SKITest.java
@@ -19,16 +19,23 @@
package org.apache.wss4j.common.crypto;
+import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.Security;
+import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.util.Base64;
+import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.util.Loader;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -82,6 +89,47 @@ public class SKITest {
assertTrue(knownBase64Encoding.equals(org.apache.xml.security.utils.XMLUtils.encodeToString(skiBytes)));
}
+ /**
+ * A self-signed certificate whose SubjectKeyIdentifier extension declares
a key identifier of
+ * Integer.MAX_VALUE bytes. The certificate itself is 442 bytes. Its
extnValue is
+ * 04 09 04 84 7F FF FF FF 01 02 03: an OCTET STRING wrapping an OCTET
STRING whose long-form
+ * length is 0x7FFFFFFF. Certificates reach this code path from an inbound
+ * wsse:BinarySecurityToken, so the declared length is attacker-controlled.
+ */
+ private static final String HOSTILE_SKI_CERT =
+
"MIIBtjCCAR+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtob3N0aWxlLXNraTAe"
+ +
"Fw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBaMBYxFDASBgNVBAMMC2hvc3RpbGUtc2tpMIGf"
+ +
"MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur"
+ +
"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6ur"
+ +
"q6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urqwIDAQABoxQwEjAQBgNVHQ4E"
+ +
"CQSEf////wECAzANBgkqhkiG9w0BAQsFAAOBgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ +
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+
+ /**
+ * An over-long declared length in the SubjectKeyIdentifier must produce a
WSSecurityException,
+ * not a multi-gigabyte allocation and not an unchecked exception.
+ */
+ @Test
+ public void testSKIWithOverlongDeclaredLength() throws Exception {
+ CertificateFactory certificateFactory =
CertificateFactory.getInstance("X.509");
+ X509Certificate cert;
+ try (InputStream input = new
ByteArrayInputStream(Base64.getDecoder().decode(HOSTILE_SKI_CERT))) {
+ cert =
(X509Certificate)certificateFactory.generateCertificate(input);
+ }
+
+ byte[] extensionValue = cert.getExtensionValue(CryptoBase.SKI_OID);
+ // A JDK that rejects the malformed extension outright leaves nothing
for us to decode
+ Assumptions.assumeTrue(extensionValue != null);
+ assertArrayEquals(
+ new byte[] {4, 9, 4, (byte)0x84, 0x7F, (byte)0xFF, (byte)0xFF,
(byte)0xFF, 1, 2, 3},
+ extensionValue
+ );
+
+ Crypto crypto = new Merlin();
+ assertThrows(WSSecurityException.class, () ->
crypto.getSKIBytesFromCert(cert));
+ }
+
@Test
public void testBouncyCastlePKCS12() throws Exception {
try {