Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.xml.security.test.dom.encryption;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
Expand All @@ -40,15 +42,18 @@
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.utils.EncryptionConstants;
import org.apache.xml.security.utils.KeyUtils;
import org.apache.xml.security.utils.XMLUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
Expand Down Expand Up @@ -213,4 +218,113 @@ void testMLKEMEncryptDecrypt(String keyEncapsulationUri, String jcaAlgorithm) th
assertEquals("PaymentInfo", decryptedRoot.getLocalName());
assertEquals("CardNumber:4019111111111111", decryptedRoot.getTextContent());
}

@ParameterizedTest
@CsvSource({
EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_512 + ",ML-KEM-512",
EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_768 + ",ML-KEM-768",
EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_1024 + ",ML-KEM-1024",
})
void testMLKEMWrongRecipientKeyFailsDecryption(String keyEncapsulationUri, String jcaAlgorithm) throws Exception {
Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)");
byte[] encryptedXml = encryptToRecipient(keyEncapsulationUri, keyPairs.get(jcaAlgorithm).getPublic());
// a different recipient key pair of the same ML-KEM parameter set
KeyPair wrongKeyPair = KeyPairGenerator.getInstance(jcaAlgorithm, "BC").generateKeyPair();
assertThrows(Exception.class, () -> decryptWith(encryptedXml, wrongKeyPair.getPrivate()),
"decryption with a non-matching ML-KEM private key must fail rather than return content");
}

@ParameterizedTest
@CsvSource({
EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_512 + ",ML-KEM-512",
EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_768 + ",ML-KEM-768",
EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_1024 + ",ML-KEM-1024",
})
void testMLKEMCorruptEncapsulationRejected(String keyEncapsulationUri, String jcaAlgorithm) throws Exception {
Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)");
byte[] encryptedXml = encryptToRecipient(keyEncapsulationUri, keyPairs.get(jcaAlgorithm).getPublic());
// ML-KEM implicit rejection means a corrupt encapsulation yields a different shared secret,
// so the failure surfaces at the AES key-unwrap integrity check rather than at decapsulation.
byte[] corrupted = corruptEncryptedKeyCipherValue(encryptedXml);
assertThrows(Exception.class, () -> decryptWith(corrupted, keyPairs.get(jcaAlgorithm).getPrivate()),
"decryption of a corrupted ML-KEM encapsulation must fail rather than return content");
}

private static byte[] encryptToRecipient(String keyEncapsulationUri, PublicKey recipientPublicKey) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc = dbf.newDocumentBuilder().newDocument();
Element root = doc.createElement("PaymentInfo");
root.setTextContent("CardNumber:4019111111111111");
doc.appendChild(root);

KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256);
SecretKey cek = kg.generateKey();

String kwAlgorithm = EncryptionConstants.ALGO_ID_KEYWRAP_AES256;
int wrapKeyBitLength = KeyUtils.getAESKeyBitSizeForWrapAlgorithm(kwAlgorithm);
HKDFParams kdfParams = HKDFParams.createBuilder(wrapKeyBitLength, XMLSignature.ALGO_ID_MAC_HMAC_SHA256).build();
AlgorithmParameterSpec keyEncapsulationParameters =
new KeyEncapsulationParameters(keyEncapsulationUri, kdfParams);

XMLCipher keyCipher = XMLCipher.getInstance(kwAlgorithm);
keyCipher.init(XMLCipher.WRAP_MODE, recipientPublicKey);
EncryptedKey encryptedKey = keyCipher.encryptKey(doc, cek, keyEncapsulationParameters, null);

XMLCipher dataCipher = XMLCipher.getInstance(XMLCipher.AES_256_GCM);
dataCipher.init(XMLCipher.ENCRYPT_MODE, cek);
EncryptedData encryptedData = dataCipher.getEncryptedData();
KeyInfo keyInfo = new KeyInfo(doc);
keyInfo.add(encryptedKey);
encryptedData.setKeyInfo(keyInfo);
doc = dataCipher.doFinal(doc, root, false);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
javax.xml.transform.Transformer t =
javax.xml.transform.TransformerFactory.newInstance().newTransformer();
t.transform(new javax.xml.transform.dom.DOMSource(doc),
new javax.xml.transform.stream.StreamResult(bos));
return bos.toByteArray();
}

private static String decryptWith(byte[] encryptedXml, PrivateKey recipientPrivateKey) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document encDoc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(encryptedXml));
Element encDataElem = (Element) encDoc.getElementsByTagNameNS(
EncryptionConstants.EncryptionSpecNS, "EncryptedData").item(0);

XMLCipher decryptCipher = XMLCipher.getInstance();
decryptCipher.init(XMLCipher.DECRYPT_MODE, null);
EncryptedData encData = decryptCipher.loadEncryptedData(encDoc, encDataElem);

EncryptedKey ek = encData.getKeyInfo().itemEncryptedKey(0);
XMLCipher unwrapCipher = XMLCipher.getInstance();
unwrapCipher.init(XMLCipher.UNWRAP_MODE, recipientPrivateKey);
Key recoveredCek = unwrapCipher.decryptKey(ek, encData.getEncryptionMethod().getAlgorithm());

decryptCipher.init(XMLCipher.DECRYPT_MODE, recoveredCek);
Document decryptedDoc = decryptCipher.doFinal(encDoc, encDataElem);
return decryptedDoc.getDocumentElement().getTextContent();
}

private static byte[] corruptEncryptedKeyCipherValue(byte[] encryptedXml) throws Exception {
Document doc = XMLUtils.read(new ByteArrayInputStream(encryptedXml), false);
// the EncryptedKey's CipherValue holds (KEM encapsulation || wrapped CEK); flip a bit near the start
Element encKey = (Element) doc.getElementsByTagNameNS(
EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_ENCRYPTEDKEY).item(0);
NodeList cvs = encKey.getElementsByTagNameNS(
EncryptionConstants.EncryptionSpecNS, EncryptionConstants._TAG_CIPHERVALUE);
Element cipherValue = (Element) cvs.item(0);
byte[] blob = XMLUtils.decode(cipherValue.getTextContent().trim());
blob[0] ^= 0x01;
cipherValue.setTextContent(XMLUtils.encodeToString(blob));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
javax.xml.transform.Transformer t =
javax.xml.transform.TransformerFactory.newInstance().newTransformer();
t.transform(new javax.xml.transform.dom.DOMSource(doc),
new javax.xml.transform.stream.StreamResult(bos));
return bos.toByteArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,38 @@
*/
package org.apache.xml.security.test.javax.xml.crypto.dsig;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.util.Locale;

import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;

import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.test.javax.xml.crypto.KeySelectors;
import org.apache.xml.security.utils.Constants;
import org.apache.xml.security.utils.XMLUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
* Tests for ML-DSA (FIPS 204) XML digital signatures via the
Expand Down Expand Up @@ -105,6 +124,67 @@ void testMLDSASignAndVerify(String signatureAlgorithmURI, String alias) throws E
assertValidSignatureWithJcpApi(signedXml, false);
}

@ParameterizedTest
@CsvSource({
XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44 + ",ml-dsa-44",
XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65 + ",ml-dsa-65",
XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87",
})
void testMLDSATamperedSignatureRejected(String signatureAlgorithmURI, String alias) throws Exception {
Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+");
byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false);
byte[] tampered = flipSignatureValueBit(signedXml);
Assertions.assertFalse(isValidSignature(tampered, new KeySelectors.RawX509KeySelector()),
"verification must fail when the signature value is altered");
}

@ParameterizedTest
@CsvSource({
XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44 + ",ml-dsa-44",
XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65 + ",ml-dsa-65",
XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87 + ",ml-dsa-87",
})
void testMLDSAWrongKeyFailsVerification(String signatureAlgorithmURI, String alias) throws Exception {
Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+");
byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false);
// a different key pair of the same ML-DSA parameter set
KeyPairGenerator kpg = KeyPairGenerator.getInstance(alias.toUpperCase(Locale.ROOT), "BC");
PublicKey wrongKey = kpg.generateKeyPair().getPublic();
Assertions.assertFalse(isValidSignature(signedXml, fixedKeySelector(wrongKey)),
"verification must fail against a public key that did not produce the signature");
}

private boolean isValidSignature(byte[] signedXml, KeySelector keySelector) throws Exception {
SignatureValidator validator = new SignatureValidator();
try (InputStream is = new ByteArrayInputStream(signedXml)) {
DOMValidateContext vc = validator.getValidateContext(is, keySelector, false);
updateIdReferences(vc, "SignedElement", "id");
return validator.validate(vc);
}
}

private static byte[] flipSignatureValueBit(byte[] signedXml) throws Exception {
Document doc = XMLUtils.read(new ByteArrayInputStream(signedXml), false);
NodeList nl = doc.getElementsByTagNameNS(Constants.SignatureSpecNS, Constants._TAG_SIGNATUREVALUE);
Element sigValue = (Element) nl.item(0);
byte[] sig = XMLUtils.decode(sigValue.getTextContent().trim());
sig[sig.length / 2] ^= 0x01;
sigValue.setTextContent(XMLUtils.encodeToString(sig));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
XMLUtils.outputDOMc14nWithComments(doc, bos);
return bos.toByteArray();
}

private static KeySelector fixedKeySelector(final PublicKey key) {
return new KeySelector() {
@Override
public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose,
AlgorithmMethod method, XMLCryptoContext context) {
return () -> key;
}
};
}

@Override
KeyStore getKeyStore() {
return keyStore;
Expand Down