Reject malformed ML-KEM key-transport metadata with XMLEncryptionException - #6
Conversation
…ption Parsing the EncryptedKey's key-transport metadata could escape the declared XMLEncryptionException with an unchecked exception before any private-key operation: a childless or non-numeric ghc:KeyLen threw NullPointerException or NumberFormatException from an unguarded Integer.parseInt, malformed base64 in the HKDF Salt or Info threw IllegalArgumentException from Base64.Decoder, and a KeyDerivationMethod that failed to parse was rethrown wrapped in a RuntimeException. Guard the KeyLen parse and the base64 decode, report all three through XMLEncryptionException (reusing the KeyDerivation.InvalidParameter message), and let newEncryptionMethod(Element) declare the checked exception instead of wrapping it; both of its callers already declare XMLEncryptionException. Adds DOM negative tests for the reproduced cases; each fails against the previous code.
|
Rebased-status note: this branch still applies cleanly on the current PQC-ENCRYPTION tip after #5 merged (test-merges with no conflicts), so no rebase is needed. It stays green with the earlier verification (14/14 in XMLEncryptionMLKEMTest plus the encryption regression set). Happy to rebase or split it further if that helps review. |
|
Hi @ffang small nudge on this one, no rush. If the metadata hardening here looks right, I'm glad to rebase it against the current apache#652 branch or split it however is easiest to review. And if you'd rather fold these checks into your own commit, I'm happy to close this in favor of that. Thanks again for merging the earlier test PRs. |
Merged, thanks @Arpan0995 ! |
Following the inbound-path note on apache#652, I ran a small robustness pass over the DOM decrypt path: one valid ML-KEM-768 document, then a batch of malformed variants through
XMLCipher.loadEncryptedData/decryptKey, checking what type of exception escapes. The KEM ciphertext handling itself is solid (every truncation and byte-flip of theEncryptedKeyCipherValuecame back asXMLSecurityException). Three of the parsed metadata fields, though, escaped the declaredXMLEncryptionExceptionwith an unchecked exception, all triggered before any private-key operation:ghc:KeyLenwas parsed withInteger.parseInt(keyLenElement.getFirstChild().getNodeValue()): a childless<ghc:KeyLen/>threwNullPointerException, and empty, non-numeric or overflowing text threwNumberFormatException.SaltorInfothrewIllegalArgumentExceptionfromBase64.getDecoder().decode(...)inXMLCipherUtil#constructKeyDerivationParameter. That method is shared with the existing ECDH-ES path, so the decode is not new here, but it is newly reachable through ML-KEM key transport.KeyDerivationMethodthat failed to parse was rethrown asthrow new RuntimeException(xse), wrapping a declared exception as unchecked. (Noted by inspection; my inputs did not trigger it.)None of these are memory-safety or authentication issues, and a caller gets a clean rejection for a corrupted ciphertext already. The concern is narrower: a caller catching
XMLEncryptionException, the documented contract, will not catch an NPE,NumberFormatExceptionorIllegalArgumentException, so on a service decrypting untrusted XML these surface as uncaught errors rather than clean rejections.The change. Guard the
KeyLenparse (null child, empty text,NumberFormatException) and wrap the base64 decode, reporting both throughXMLEncryptionExceptionwith the existingKeyDerivation.InvalidParametermessage; and letnewEncryptionMethod(Element)declare the checked exception instead of wrapping it, which is safe because the factory is package-internal and both of its callers (newEncryptedData,newEncryptedKey) already declareXMLEncryptionException.Tests. Adds two DOM negative tests to
XMLEncryptionMLKEMTest: a parameterized one over the malformed-metadata cases (non-numeric and emptyKeyLen, malformedSalt, malformedInfo) and one for a childlessKeyLen, each assertingXMLEncryptionExceptionfrom the full decrypt path. I confirmed they are not vacuous by running them against the previous code: all five fail there and pass with this change.Verification.
mvn test -P bouncycastleoverXMLEncryptionMLKEMTest(14 executions: the nine existing plus the five new, 0 failures),StaxMLKEMEncryptionTest,XMLCipherTest,XMLEncryption11Test,BaltimoreEncTest,EncryptionFormattingTestand the twoKeyWrapEncryption*classes: 148 executions with no regressions. (One pre-existing error,XMLCipherTest#testEncryptForDataExceeding8192bytes, fails identically on the untouched branch on my machine and is unrelated.) Without the profile the new tests compile and skip via the existingassumeTrueguard.Two things deliberately left alone: the pre-existing
KeySizeparse a few lines aboveKeyLenhas the same unguarded shape but predates this PR and would need its own message key, so I did not fold it in; and no behavior changes for well-formed input, since the same strictBase64.getDecoder()is used.