Resolve dsig11:DEREncodedKeyValue on the StAX inbound path - #7
Conversation
The StAX inbound processor resolved a KeyValue only through its RSA, DSA and EC forms, so a dsig11:DEREncodedKeyValue (the KeyValue form for key types without a structured element, such as ML-DSA) failed at key resolution with "No or unsupported key in KeyValue" even though the outbound side now emits it. Add DEREncodedKeyValueSecurityToken, which rebuilds the public key from the DER SubjectPublicKeyInfo by trying the same key types as the DOM DEREncodedKeyValue, and resolve it from both placements: nested inside ds:KeyValue (as emitted) and as a direct ds:KeyInfo child (the XML Signature 1.1 placement). With this a StAX-signed ML-DSA document verifies through the StAX inbound path with no out-of-band key. Adds StaxMLDSAKeyValueInboundTest covering both placements (asserting the resolved key is the signer's) and tampered signature rejection, parameterized across ML-DSA-44/65/87; all nine cases fail without the factory changes.
|
Thanks Arpan — nice follow-up, and the KeyInfo-direct-child placement is a good addition, XML Signature 1.1 does define it that way. Looks like we each wrote our own I checked out this branch and reproduced it end-to-end: signed a real ML-DSA document, corrupted the
Could you:
For reference, here's what my own (unpublished) version of } catch (NoSuchAlgorithmException | InvalidKeySpecException | RuntimeException e) { //NOPMD
// Do nothing, try the next type. Some providers (e.g. BC's XDH/EdDSA KeyFactorySpi)
// throw an unchecked exception like ArrayIndexOutOfBoundsException instead of
// InvalidKeySpecException for malformed/short input, which must not propagate
// since encodedKey here is untrusted, attacker-controlled input.
}And for the test, something like @ParameterizedTest
@CsvSource({
"http://www.w3.org/tbd#ml-dsa-44,ML-DSA-44",
"http://www.w3.org/tbd#ml-dsa-65,ML-DSA-65",
"http://www.w3.org/tbd#ml-dsa-87,ML-DSA-87"
})
void testInboundGarbageDerContentRejectedCleanly(String sigAlgorithm, String jcaAlgorithm) throws Exception {
Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm),
"ML-DSA requires BouncyCastle 1.81+");
KeyPair kp = keyPairs.get(jcaAlgorithm);
byte[] corrupted = corruptDerEncodedKeyValue(signWithKeyValue(sigAlgorithm, kp));
// Garbage bytes decode to no known SubjectPublicKeyInfo, so this must fail cleanly at
// key resolution with a well-defined error, not an uncaught RuntimeException.
XMLStreamException ex = Assertions.assertThrows(XMLStreamException.class,
() -> verifyInbound(corrupted, new ArrayList<>()));
String chain = messageChain(ex);
Assertions.assertFalse(chain.contains("ArrayIndexOutOfBoundsException"),
"Malformed DEREncodedKeyValue content must not surface as an uncaught RuntimeException: " + chain);
}
/** Replaces the DEREncodedKeyValue's base64 content with bytes that decode to no known SubjectPublicKeyInfo. */
private byte[] corruptDerEncodedKeyValue(byte[] signed) throws Exception {
Document document;
try (InputStream is = new ByteArrayInputStream(signed)) {
document = XMLUtils.read(is, false);
}
Element der = (Element) document.getElementsByTagNameNS(
"http://www.w3.org/2009/xmldsig11#", "DEREncodedKeyValue").item(0);
byte[] garbage = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
NodeList children = der.getChildNodes();
for (int i = children.getLength() - 1; i >= 0; i--) {
der.removeChild(children.item(i));
}
der.appendChild(document.createTextNode(Base64.getEncoder().encodeToString(garbage)));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
javax.xml.transform.TransformerFactory.newInstance().newTransformer().transform(
new javax.xml.transform.dom.DOMSource(document),
new javax.xml.transform.stream.StreamResult(bos));
return bos.toByteArray();
}(Dropped both into your branch to check: 12/12 pass with the fix, and the new test fails 3/3 with the exact Everything else here looks good to me. Best Regards |
buildPublicKey() caught only NoSuchAlgorithmException and InvalidKeySpecException, but some providers (BouncyCastle's XDH/EdDSA KeyFactorySpi) throw an unchecked ArrayIndexOutOfBoundsException for malformed or short input rather than InvalidKeySpecException. Because the DEREncodedKeyValue content is untrusted, attacker-controlled inbound data, that exception propagated out of processInMessage instead of being rejected cleanly. Also catch RuntimeException in the key-type loop so a malformed encoding falls through to a clean stax.unsupportedKeyValue rejection, and add a garbage-content case to StaxMLDSAKeyValueInboundTest (parameterized across ML-DSA-44/65/87) that fails without this change.
|
Good catch, and thanks for reproducing it end to end. You are right that the untrusted DEREncodedKeyValue content can drive an unchecked exception out of a KeyFactorySpi (BC's XDH/EdDSA throw ArrayIndexOutOfBoundsException on short input), which is exactly the kind of thing the key-resolution loop must contain. Both changes are in:
I verified the test is not vacuous the same way you did: with only the catch reverted it fails 3/3 with One related note, only for your awareness and not part of this PR: the DOM |
Sure, please send another commit in this PR as well to address the same issue in the DOM path, thanks! |
The DOM DEREncodedKeyValue#getPublicKey() has the same narrow exception handling as the StAX token fixed in the previous commit: it caught only NoSuchAlgorithmException and InvalidKeySpecException while iterating the supported key types, so an unchecked exception from a KeyFactorySpi (BouncyCastle 1.85's XDH/EdDSA throw ArrayIndexOutOfBoundsException for malformed or short input) propagated out instead of a clean rejection. Because DEREncodedKeyValueResolver is a default KeyResolver and a DEREncodedKeyValue in an inbound document is untrusted, attacker-controlled content, this could crash key resolution reached via KeyInfo#getPublicKey() with an uncontrolled runtime exception. Catch RuntimeException in the loop so a malformed encoding falls through to the declared XMLSecurityException. Adds a test that fails without the fix (BouncyCastle at first provider position, skipped otherwise).
|
Added as a third commit. It's reachable on the DOM path via
|
|
Thanks @Arpan0995 , merged! |
Follow-up to #4 (now merged into this branch), completing the KeyValue round trip discussed on apache#651.
What #4 left open. With #4 the outbound side emits a schema-valid
dsig11:DEREncodedKeyValuefor ML-DSA, but the StAX inbound processor resolved aKeyValueonly through its RSA, DSA and EC forms, so the document got past schema validation and then failed at key resolution with "No or unsupported key in KeyValue". Full StAX sign to StAX verify still did not work.The change.
DEREncodedKeyValueSecurityToken(mirrorsECKeyValueSecurityToken): rebuilds the public key from the DER SubjectPublicKeyInfo by trying each supported key type'sKeyFactory, using the same key-type list as the DOMDEREncodedKeyValue.SecurityTokenFactoryImplresolves it from both placements: nested insideds:KeyValue(what this library emits) and as a directds:KeyInfochild, which is where XML Signature 1.1 defines it, so documents from other implementations resolve too.With this, a StAX-signed ML-DSA document verifies through
InboundXMLSec#processInMessagewith no out-of-band verification key.Tests.
StaxMLDSAKeyValueInboundTest, parameterized across ML-DSA-44/65/87, with nosetSignatureVerificationKeyso the key can only come from the document:KeyValueTokenSecurityEvent) is byte-identical to the signer's public key;SignatureValue: rejected, asserting the failure is signature validation ("INVALID signature") rather than key resolution.I confirmed the tests are not vacuous by running them without the factory changes: all nine fail there and pass with this change.
Verification.
mvn test -P bouncycastleover the new class plusStaxMLDSAKeyValueTest,StaxMLDSASignatureTestandSignatureVerificationTest: no regressions, so the existing RSA/DSA/EC KeyValue resolution is unaffected. Without the profile the new tests compile and skip via the existingassumeTrueguard.One observation, not changed here: the DOM
DEREncodedKeyValue#getPublicKeyreports failures with the message keyDEREncodedKeyValue.UnsupportedEncodedKey, which is not present inxmlsecurity_en.properties, so that path would surface the raw key. The StAX token uses the existingstax.unsupportedKeyValueinstead.