diff --git a/pom.xml b/pom.xml index 5851d1eee..f7b022c7f 100644 --- a/pom.xml +++ b/pom.xml @@ -745,6 +745,12 @@ ${bcprov.version} test + + org.bouncycastle + bcpkix-jdk18on + ${bcprov.version} + test + diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java index 9728d20aa..9e2f7c0ac 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/AbstractDOMSignatureMethod.java @@ -47,7 +47,7 @@ abstract class AbstractDOMSignatureMethod extends DOMStructure implements SignatureMethod { // denotes the type of signature algorithm - enum Type { DSA, RSA, ECDSA, EDDSA, HMAC } + enum Type { DSA, RSA, ECDSA, EDDSA, MLDSA, HMAC } /** * Verifies the passed-in signature with the specified key, using the diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java index 688ab7668..83d8eb179 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMSignatureMethod.java @@ -94,6 +94,16 @@ public abstract class DOMSignatureMethod extends AbstractDOMSignatureMethod { "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed25519"; static final String ED448 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"; + + // Provisional URIs for ML-DSA (FIPS 204) per draft-eastlake-rfc9231bis-xmlsec-uris + // section 3.3.15. These use the draft's "tbd" placeholder namespace and will need + // to be updated once final URIs are assigned (see SANTUARIO-634). + static final String ML_DSA_44 = + "http://www.w3.org/tbd#ml-dsa-44"; + static final String ML_DSA_65 = + "http://www.w3.org/tbd#ml-dsa-65"; + static final String ML_DSA_87 = + "http://www.w3.org/tbd#ml-dsa-87"; static final String ECDSA_SHA3_224 = "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-224"; static final String ECDSA_SHA3_256 = @@ -269,6 +279,12 @@ static SignatureMethod unmarshal(Element smElem) throws MarshalException { return new EDDSA_ED25519(smElem); } else if (alg.equals(ED448)) { return new EDDSA_ED448(smElem); + } else if (alg.equals(ML_DSA_44)) { + return new MLDSA_44(smElem); + } else if (alg.equals(ML_DSA_65)) { + return new MLDSA_65(smElem); + } else if (alg.equals(ML_DSA_87)) { + return new MLDSA_87(smElem); } else { throw new MarshalException ("unsupported SignatureMethod algorithm: " + alg); @@ -1291,4 +1307,87 @@ String getJCAAlgorithm() { return "Ed448"; } } + + abstract static class AbstractMLDSASignatureMethod extends DOMSignatureMethod { + + AbstractMLDSASignatureMethod(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + + AbstractMLDSASignatureMethod(Element dmElem) throws MarshalException { + super(dmElem); + } + + /** ML-DSA signatures are raw bytes; no reformatting needed. */ + @Override + byte[] postSignFormat(Key key, byte[] sig) { + return sig; + } + + /** ML-DSA signatures are raw bytes; no reformatting needed. */ + @Override + byte[] preVerifyFormat(Key key, byte[] sig) { + return sig; + } + + @Override + Type getAlgorithmType() { + return Type.MLDSA; + } + } + + static final class MLDSA_44 extends AbstractMLDSASignatureMethod { + MLDSA_44(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + MLDSA_44(Element dmElem) throws MarshalException { + super(dmElem); + } + @Override + public String getAlgorithm() { + return ML_DSA_44; + } + @Override + String getJCAAlgorithm() { + return "ML-DSA-44"; + } + } + + static final class MLDSA_65 extends AbstractMLDSASignatureMethod { + MLDSA_65(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + MLDSA_65(Element dmElem) throws MarshalException { + super(dmElem); + } + @Override + public String getAlgorithm() { + return ML_DSA_65; + } + @Override + String getJCAAlgorithm() { + return "ML-DSA-65"; + } + } + + static final class MLDSA_87 extends AbstractMLDSASignatureMethod { + MLDSA_87(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + super(params); + } + MLDSA_87(Element dmElem) throws MarshalException { + super(dmElem); + } + @Override + public String getAlgorithm() { + return ML_DSA_87; + } + @Override + String getJCAAlgorithm() { + return "ML-DSA-87"; + } + } } diff --git a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java index 49f93514c..94074a211 100644 --- a/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java +++ b/src/main/java/org/apache/jcp/xml/dsig/internal/dom/DOMXMLSignatureFactory.java @@ -355,7 +355,13 @@ public SignatureMethod newSignatureMethod(String algorithm, return new DOMSignatureMethod.EDDSA_ED25519(params); } else if (algorithm.equals(DOMSignatureMethod.ED448)) { return new DOMSignatureMethod.EDDSA_ED448(params); - }else { + } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_44)) { + return new DOMSignatureMethod.MLDSA_44(params); + } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_65)) { + return new DOMSignatureMethod.MLDSA_65(params); + } else if (algorithm.equals(DOMSignatureMethod.ML_DSA_87)) { + return new DOMSignatureMethod.MLDSA_87(params); + } else { throw new NoSuchAlgorithmException("unsupported algorithm"); } } diff --git a/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java b/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java index 1dc09759d..d82f8bfc0 100644 --- a/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java +++ b/src/main/java/org/apache/xml/security/algorithms/JCEMapper.java @@ -25,6 +25,7 @@ import org.apache.xml.security.encryption.XMLCipher; import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.utils.EncryptionConstants; import org.apache.xml.security.utils.JavaUtils; import org.w3c.dom.Element; @@ -233,6 +234,18 @@ public static void registerDefaultAlgorithms() { XMLSignature.ALGO_ID_SIGNATURE_EDDSA_ED448, new Algorithm("Ed448", "Ed448", "Signature") ); + algorithmsMap.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44, + new Algorithm("ML-DSA-44", "ML-DSA-44", "Signature") + ); + algorithmsMap.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, + new Algorithm("ML-DSA-65", "ML-DSA-65", "Signature") + ); + algorithmsMap.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87, + new Algorithm("ML-DSA-87", "ML-DSA-87", "Signature") + ); algorithmsMap.put( XMLSignature.ALGO_ID_MAC_HMAC_NOT_RECOMMENDED_MD5, new Algorithm("", "HmacMD5", "Mac", 0, 0) @@ -318,6 +331,22 @@ public static void registerDefaultAlgorithms() { XMLCipher.RSA_OAEP_11, new Algorithm("RSA", "RSA/ECB/OAEPPadding", "KeyTransport") ); + algorithmsMap.put( + EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_512, + new Algorithm("ML-KEM-512", "ML-KEM-512", "KeyTransport") + ); + algorithmsMap.put( + EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_768, + new Algorithm("ML-KEM-768", "ML-KEM-768", "KeyTransport") + ); + algorithmsMap.put( + EncryptionConstants.ALGO_ID_KEYTRANSPORT_MLKEM_1024, + new Algorithm("ML-KEM-1024", "ML-KEM-1024", "KeyTransport") + ); + algorithmsMap.put( + EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID, + new Algorithm("", "", "KeyTransport") + ); algorithmsMap.put( XMLCipher.DIFFIE_HELLMAN, new Algorithm("", "", "KeyAgreement") diff --git a/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java b/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java index 578e1eb1b..a0ae148a6 100644 --- a/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java +++ b/src/main/java/org/apache/xml/security/algorithms/SignatureAlgorithm.java @@ -34,6 +34,7 @@ import org.apache.xml.security.algorithms.implementations.SignatureDSA; import org.apache.xml.security.algorithms.implementations.SignatureECDSA; import org.apache.xml.security.algorithms.implementations.SignatureEDDSA; +import org.apache.xml.security.algorithms.implementations.SignatureMLDSA; import org.apache.xml.security.exceptions.AlgorithmAlreadyRegisteredException; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.signature.XMLSignature; @@ -513,6 +514,15 @@ public static void registerDefaultAlgorithms() { algorithmHash.put( XMLSignature.ALGO_ID_SIGNATURE_EDDSA_ED448, SignatureEDDSA.SignatureEd448.class ); + algorithmHash.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44, SignatureMLDSA.SignatureMLDSA44.class + ); + algorithmHash.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65, SignatureMLDSA.SignatureMLDSA65.class + ); + algorithmHash.put( + XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87, SignatureMLDSA.SignatureMLDSA87.class + ); algorithmHash.put( XMLSignature.ALGO_ID_MAC_HMAC_NOT_RECOMMENDED_MD5, IntegrityHmac.IntegrityHmacMD5.class ); diff --git a/src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java b/src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java new file mode 100644 index 000000000..daeef2db0 --- /dev/null +++ b/src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java @@ -0,0 +1,208 @@ +/** + * 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.xml.security.algorithms.implementations; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.AlgorithmParameterSpec; + +import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.algorithms.SignatureAlgorithmSpi; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.signature.XMLSignatureException; +import org.apache.xml.security.utils.XMLUtils; + +/** + * ML-DSA (FIPS 204) signature algorithm implementation for XML-Dsig. + * Supports ML-DSA-44 (NIST security level 2), ML-DSA-65 (level 3), + * and ML-DSA-87 (level 5). Requires BouncyCastle 1.81+ as the JCA provider. + */ +public abstract class SignatureMLDSA extends SignatureAlgorithmSpi { + + private static final Logger LOG = System.getLogger(SignatureMLDSA.class.getName()); + + private final Signature signatureAlgorithm; + + public SignatureMLDSA() throws XMLSignatureException { + this(null); + } + + public SignatureMLDSA(Provider provider) throws XMLSignatureException { + String algorithmID = JCEMapper.translateURItoJCEID(this.engineGetURI()); + LOG.log(Level.DEBUG, "Created SignatureMLDSA using {0}", algorithmID); + + try { + if (provider == null) { + String providerId = JCEMapper.getProviderId(); + if (providerId == null) { + this.signatureAlgorithm = Signature.getInstance(algorithmID); + } else { + this.signatureAlgorithm = Signature.getInstance(algorithmID, providerId); + } + } else { + this.signatureAlgorithm = Signature.getInstance(algorithmID, provider); + } + } catch (NoSuchAlgorithmException | NoSuchProviderException ex) { + Object[] exArgs = { algorithmID, ex.getLocalizedMessage() }; + throw new XMLSignatureException("algorithms.NoSuchAlgorithm", exArgs); + } + } + + @Override + protected void engineSetParameter(AlgorithmParameterSpec params) throws XMLSignatureException { + try { + this.signatureAlgorithm.setParameter(params); + } catch (InvalidAlgorithmParameterException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected boolean engineVerify(byte[] signature) throws XMLSignatureException { + try { + LOG.log(Level.DEBUG, () -> "Called SignatureMLDSA.verify() on " + XMLUtils.encodeToString(signature)); + return this.signatureAlgorithm.verify(signature); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineInitVerify(Key publicKey) throws XMLSignatureException { + engineInitVerify(publicKey, signatureAlgorithm); + } + + @Override + protected byte[] engineSign() throws XMLSignatureException { + try { + return this.signatureAlgorithm.sign(); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineInitSign(Key privateKey, SecureRandom secureRandom) + throws XMLSignatureException { + engineInitSign(privateKey, secureRandom, this.signatureAlgorithm); + } + + @Override + protected void engineInitSign(Key privateKey) throws XMLSignatureException { + engineInitSign(privateKey, (SecureRandom) null); + } + + @Override + protected void engineUpdate(byte[] input) throws XMLSignatureException { + try { + this.signatureAlgorithm.update(input); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineUpdate(byte input) throws XMLSignatureException { + try { + this.signatureAlgorithm.update(input); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected void engineUpdate(byte[] buf, int offset, int len) throws XMLSignatureException { + try { + this.signatureAlgorithm.update(buf, offset, len); + } catch (SignatureException ex) { + throw new XMLSignatureException(ex); + } + } + + @Override + protected String engineGetJCEAlgorithmString() { + return this.signatureAlgorithm.getAlgorithm(); + } + + @Override + protected String engineGetJCEProviderName() { + return this.signatureAlgorithm.getProvider().getName(); + } + + @Override + protected void engineSetHMACOutputLength(int HMACOutputLength) throws XMLSignatureException { + throw new XMLSignatureException("algorithms.HMACOutputLengthOnlyForHMAC"); + } + + @Override + protected void engineInitSign(Key signingKey, AlgorithmParameterSpec algorithmParameterSpec) + throws XMLSignatureException { + throw new XMLSignatureException("algorithms.CannotUseAlgorithmParameterSpecOnEdDSA"); + } + + /** ML-DSA-44 — NIST security level 2. */ + public static class SignatureMLDSA44 extends SignatureMLDSA { + public SignatureMLDSA44() throws XMLSignatureException { + super(); + } + public SignatureMLDSA44(Provider provider) throws XMLSignatureException { + super(provider); + } + @Override + public String engineGetURI() { + return XMLSignature.ALGO_ID_SIGNATURE_MLDSA_44; + } + } + + /** ML-DSA-65 — NIST security level 3. */ + public static class SignatureMLDSA65 extends SignatureMLDSA { + public SignatureMLDSA65() throws XMLSignatureException { + super(); + } + public SignatureMLDSA65(Provider provider) throws XMLSignatureException { + super(provider); + } + @Override + public String engineGetURI() { + return XMLSignature.ALGO_ID_SIGNATURE_MLDSA_65; + } + } + + /** ML-DSA-87 — NIST security level 5. */ + public static class SignatureMLDSA87 extends SignatureMLDSA { + public SignatureMLDSA87() throws XMLSignatureException { + super(); + } + public SignatureMLDSA87(Provider provider) throws XMLSignatureException { + super(provider); + } + @Override + public String engineGetURI() { + return XMLSignature.ALGO_ID_SIGNATURE_MLDSA_87; + } + } +} diff --git a/src/main/java/org/apache/xml/security/encryption/EncryptionMethod.java b/src/main/java/org/apache/xml/security/encryption/EncryptionMethod.java index 4b512e7d9..a6f835977 100644 --- a/src/main/java/org/apache/xml/security/encryption/EncryptionMethod.java +++ b/src/main/java/org/apache/xml/security/encryption/EncryptionMethod.java @@ -102,6 +102,71 @@ public interface EncryptionMethod { */ String getMGFAlgorithm(); + /** + * Returns the Key Encapsulation Method algorithm URI used for KEM-based key transport + * (W3C "XML Security: Generic Hybrid Cipher", https://www.w3.org/TR/xmlsec-generic-hybrid/), + * i.e. the {@code Algorithm} attribute of the {@code ghc:KeyEncapsulationMethod} element nested + * inside {@code ghc:GenericHybridCipherMethod}. + * + * @return the key encapsulation algorithm, or {@code null} if this is not a Generic Hybrid + * Cipher {@code EncryptionMethod}. + */ + String getKeyEncapsulationAlgorithm(); + + /** + * Sets the Key Encapsulation Method algorithm URI. See {@link #getKeyEncapsulationAlgorithm()}. + * + * @param algorithm the key encapsulation algorithm. + */ + void setKeyEncapsulationAlgorithm(String algorithm); + + /** + * Returns the {@code xenc11:KeyDerivationMethod} nested inside {@code ghc:KeyEncapsulationMethod}, + * used to derive the data-encapsulation (AES key-wrap) key from the KEM shared secret. + * + * @return the key derivation method, or {@code null} if not set. + */ + KeyDerivationMethod getKeyEncapsulationKeyDerivationMethod(); + + /** + * Sets the key derivation method. See {@link #getKeyEncapsulationKeyDerivationMethod()}. + * + * @param keyDerivationMethod the key derivation method. + */ + void setKeyEncapsulationKeyDerivationMethod(KeyDerivationMethod keyDerivationMethod); + + /** + * Returns the {@code ghc:KeyLen} value nested inside {@code ghc:KeyEncapsulationMethod}: the + * length, in bytes, of the derived data-encapsulation key. + * + * @return the key length in bytes, or a non-positive value if not set. + */ + int getKeyEncapsulationKeyLength(); + + /** + * Sets the derived key length in bytes. See {@link #getKeyEncapsulationKeyLength()}. + * + * @param keyLength the key length in bytes. + */ + void setKeyEncapsulationKeyLength(int keyLength); + + /** + * Returns the Data Encapsulation Method algorithm URI, i.e. the {@code Algorithm} attribute of + * the {@code ghc:DataEncapsulationMethod} element nested inside {@code ghc:GenericHybridCipherMethod} + * (typically an AES-KeyWrap algorithm URI). + * + * @return the data encapsulation algorithm, or {@code null} if this is not a Generic Hybrid + * Cipher {@code EncryptionMethod}. + */ + String getDataEncapsulationAlgorithm(); + + /** + * Sets the Data Encapsulation Method algorithm URI. See {@link #getDataEncapsulationAlgorithm()}. + * + * @param algorithm the data encapsulation algorithm. + */ + void setDataEncapsulationAlgorithm(String algorithm); + /** * Returns an iterator over all the additional elements contained in the * EncryptionMethod. diff --git a/src/main/java/org/apache/xml/security/encryption/XMLCipher.java b/src/main/java/org/apache/xml/security/encryption/XMLCipher.java index de85efb5a..aca9a51ad 100644 --- a/src/main/java/org/apache/xml/security/encryption/XMLCipher.java +++ b/src/main/java/org/apache/xml/security/encryption/XMLCipher.java @@ -55,9 +55,12 @@ import org.apache.xml.security.c14n.InvalidCanonicalizerException; import org.apache.xml.security.encryption.keys.KeyInfoEnc; import org.apache.xml.security.encryption.params.KeyAgreementParameters; +import org.apache.xml.security.encryption.params.KeyDerivationParameters; +import org.apache.xml.security.encryption.params.KeyEncapsulationParameters; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.keys.KeyInfo; import org.apache.xml.security.encryption.keys.content.AgreementMethodImpl; +import org.apache.xml.security.encryption.keys.content.derivedKey.KeyDerivationMethodImpl; import org.apache.xml.security.keys.keyresolver.KeyResolverException; import org.apache.xml.security.keys.keyresolver.KeyResolverSpi; import org.apache.xml.security.keys.keyresolver.implementations.EncryptedKeyResolver; @@ -1382,6 +1385,7 @@ public EncryptedKey encryptKey( AlgorithmParameterSpec cipherSpec = null; Key wrapKey = this.key; + byte[] kemEncapsulation = null; if (params instanceof OAEPParameterSpec) { cipherSpec = params; } else if (params instanceof KeyAgreementParameters) { @@ -1389,6 +1393,16 @@ public EncryptedKey encryptKey( validateAndUpdateKeyAgreementParameterKeys(keyAgreementParameter); // Generate a key using the key Agreement Parameters for the wrap algorithm wrapKey = KeyUtils.aesWrapKeyWithDHGeneratedKey(keyAgreementParameter); + } else if (params instanceof KeyEncapsulationParameters) { + KeyEncapsulationParameters keyEncapsulationParameter = (KeyEncapsulationParameters) params; + validateAndUpdateKeyEncapsulationParameterKeys(keyEncapsulationParameter); + // Encapsulate a shared secret to the recipient's KEM public key and derive the wrap key from it + KeyUtils.KemEncapsulation kemResult = KeyUtils.kemEncapsulate( + keyEncapsulationParameter.getRecipientPublicKey(), + keyEncapsulationParameter.getKeyEncapsulationAlgorithm(), + keyEncapsulationParameter.getKeyDerivationParameter()); + wrapKey = kemResult.getWrapKey(); + kemEncapsulation = kemResult.getEncapsulation(); } else if (params != null) { throw new XMLEncryptionException("encryption.UnsupportedAlgorithmParameterSpec", params.getClass().getName()); } @@ -1413,6 +1427,15 @@ public EncryptedKey encryptKey( throw new XMLEncryptionException(e); } + if (kemEncapsulation != null) { + // Per the W3C Generic Hybrid Cipher spec, CipherValue holds the concatenation of the + // KEM encapsulation (C0) and the AES-wrapped CEK (C1) + byte[] combined = new byte[kemEncapsulation.length + encryptedBytes.length]; + System.arraycopy(kemEncapsulation, 0, combined, 0, kemEncapsulation.length); + System.arraycopy(encryptedBytes, 0, combined, kemEncapsulation.length, encryptedBytes.length); + encryptedBytes = combined; + } + String base64EncodedEncryptedOctets = XMLUtils.encodeToString(encryptedBytes); LOG.log(Level.DEBUG, "Encrypted key octets:\n{0}", base64EncodedEncryptedOctets); LOG.log(Level.DEBUG, "Encrypted key octets length = {0}", base64EncodedEncryptedOctets.length()); @@ -1421,7 +1444,9 @@ public EncryptedKey encryptKey( cv.setValue(base64EncodedEncryptedOctets); try { - EncryptionMethod method = factory.newEncryptionMethod(new URI(algorithm).toString()); + String encryptionMethodAlgorithm = params instanceof KeyEncapsulationParameters + ? EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID : algorithm; + EncryptionMethod method = factory.newEncryptionMethod(new URI(encryptionMethodAlgorithm).toString()); method.setDigestAlgorithm(digestAlg); ek.setEncryptionMethod(method); if (params instanceof OAEPParameterSpec) { @@ -1439,6 +1464,14 @@ public EncryptedKey encryptKey( KeyInfoEnc keyInfo = new KeyInfoEnc(contextDocument); keyInfo.add(agreementMethod); ek.setKeyInfo(keyInfo); + } else if (params instanceof KeyEncapsulationParameters) { + KeyEncapsulationParameters keyEncapsulationParameter = (KeyEncapsulationParameters) params; + KeyDerivationParameters kdp = keyEncapsulationParameter.getKeyDerivationParameter(); + method.setKeyEncapsulationAlgorithm(keyEncapsulationParameter.getKeyEncapsulationAlgorithm()); + method.setKeyEncapsulationKeyDerivationMethod( + XMLCipherUtil.constructKeyDerivationMethod(contextDocument, kdp)); + method.setKeyEncapsulationKeyLength(kdp.getKeyLength()); + method.setDataEncapsulationAlgorithm(algorithm); } } catch (URISyntaxException ex) { @@ -1447,6 +1480,35 @@ public EncryptedKey encryptKey( return ek; } + /** + * Method validates and updates if needed the KeyEncapsulationParameters with the required keys. + * + * @param keyEncapsulationParameter KeyEncapsulationParameters to be validated and updated + * with the required key if needed + */ + public void validateAndUpdateKeyEncapsulationParameterKeys(KeyEncapsulationParameters keyEncapsulationParameter) + throws XMLEncryptionException { + if (keyEncapsulationParameter == null) { + return; + } + // check if the recipient's public key is set, if not, use the recipient's public key + // specified in the XMLCipher instance init method. + if (keyEncapsulationParameter.getRecipientPublicKey() == null && this.key != null) { + if (this.key instanceof PublicKey) { + LOG.log(Level.DEBUG, "Recipient's public key is not set in keyEncapsulationParameter, " + + "use the recipient's public key specified in XMLCipher instance init method."); + keyEncapsulationParameter.setRecipientPublicKey((PublicKey) this.key); + } else { + throw new XMLEncryptionException("algorithms.WrongKeyForThisOperation", + this.key.getClass().getName(), "java.security.PublicKey"); + } + } + if (keyEncapsulationParameter.getRecipientPublicKey() == null) { + // recipient's public key is mandatory for key encapsulation. + throw new XMLEncryptionException("encryption.nokey"); + } + } + /** * Decrypt a key from a passed in EncryptedKey structure * @@ -1479,7 +1541,9 @@ public Key decryptKey(EncryptedKey encryptedKey, String algorithm) try { String keyWrapAlg = encryptedKey.getEncryptionMethod().getAlgorithm(); String keyType = JCEMapper.getJCEKeyAlgorithmFromURI(keyWrapAlg); - if ( "RSA".equals(keyType) || "EC".equals(keyType)) { + if ("RSA".equals(keyType) || "EC".equals(keyType) + || (keyType != null && keyType.startsWith("ML-KEM")) + || EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID.equals(keyWrapAlg)) { key = ki.getPrivateKey(); } else { key = ki.getSecretKey(); @@ -1503,14 +1567,19 @@ public Key decryptKey(EncryptedKey encryptedKey, String algorithm) String jceKeyAlgorithm = JCEMapper.getJCEKeyAlgorithmFromURI(algorithm); LOG.log(Level.DEBUG, "JCE Key Algorithm: {0}", jceKeyAlgorithm); + boolean genericHybrid = EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID.equals( + encryptedKey.getEncryptionMethod().getAlgorithm()); + Cipher c; if (contextCipher == null) { - // Now create the working cipher - c = - constructCipher( - encryptedKey.getEncryptionMethod().getAlgorithm(), - encryptedKey.getEncryptionMethod().getDigestAlgorithm() - ); + // Now create the working cipher. For Generic Hybrid Cipher (KEM) key transport, the + // top-level EncryptionMethod algorithm is the generic "generic-hybrid" URI, not a JCE + // cipher algorithm - the actual data-encapsulation (AES-KeyWrap) algorithm is nested + // inside GenericHybridCipherMethod/DataEncapsulationMethod. + String cipherAlgorithm = genericHybrid + ? encryptedKey.getEncryptionMethod().getDataEncapsulationAlgorithm() + : encryptedKey.getEncryptionMethod().getAlgorithm(); + c = constructCipher(cipherAlgorithm, encryptedKey.getEncryptionMethod().getDigestAlgorithm()); } else { c = contextCipher; } @@ -1534,6 +1603,17 @@ public Key decryptKey(EncryptedKey encryptedKey, String algorithm) if (params instanceof KeyAgreementParameters) { Key wrapKey = KeyUtils.aesWrapKeyWithDHGeneratedKey((KeyAgreementParameters) params); c.init(Cipher.UNWRAP_MODE, wrapKey); + } else if (params instanceof KeyEncapsulationParameters) { + // Split the leading KEM encapsulation (C0) off the combined ciphertext, decapsulate + // it to derive the wrap key, and continue unwrapping only the remaining AES-wrapped + // CEK bytes (C1) + KeyUtils.KemDecapsulation kemResult = KeyUtils.kemDecapsulate( + ((KeyEncapsulationParameters) params).getRecipientPrivateKey(), + ((KeyEncapsulationParameters) params).getKeyEncapsulationAlgorithm(), + encryptedBytes, + ((KeyEncapsulationParameters) params).getKeyDerivationParameter()); + c.init(Cipher.UNWRAP_MODE, kemResult.getWrapKey()); + encryptedBytes = kemResult.getWrappedKey(); } ret = c.unwrap(encryptedBytes, jceKeyAlgorithm, Cipher.SECRET_KEY); } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { @@ -1608,10 +1688,49 @@ private AlgorithmParameterSpec getAlgorithmParameters(EncryptedKey encryptedKey) encMethod.getMGFAlgorithm(), encMethod.getOAEPparams()); } + if (EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID.equals(encryptionAlgorithm)) { + LOG.log(Level.DEBUG,"EncryptedKey key algorithm is Generic Hybrid Cipher (KEM) key transport"); + return constructKeyEncapsulationParameters(encMethod); + } + KeyInfoEnc keyInfo = encryptedKey.getKeyInfo() instanceof KeyInfoEnc ? (KeyInfoEnc) encryptedKey.getKeyInfo(): null; return constructKeyAgreementParameters(keyInfo, encryptionAlgorithm); } + /** + * The method validates whether the provided key is of type PrivateKey and the EncryptionMethod + * carries the Generic Hybrid Cipher key-encapsulation data. If both conditions are met, it + * proceeds to extract the KeyEncapsulationParameters for key derivation; otherwise, it returns null. + * + * @param encMethod the EncryptionMethod containing the Generic Hybrid Cipher key encapsulation data + * @return KeyEncapsulationParameters object containing the key encapsulation data + * or null if the provided key is not a PrivateKey + */ + private KeyEncapsulationParameters constructKeyEncapsulationParameters(EncryptionMethod encMethod) + throws XMLSecurityException { + + if (!(this.key instanceof PrivateKey)) { + LOG.log(Level.INFO,"The EncryptedKey key is using Generic Hybrid Cipher key encapsulation data, " + + "but provided key is not a PrivateKey. Skipping Key Encapsulation data processing."); + return null; + } + + String kemAlgorithm = encMethod.getKeyEncapsulationAlgorithm(); + KeyDerivationMethod keyDerivationMethod = encMethod.getKeyEncapsulationKeyDerivationMethod(); + if (kemAlgorithm == null || keyDerivationMethod == null) { + throw new XMLEncryptionException("Key Encapsulation Algorithm or Key Derivation Method is not specified"); + } + + int keyLength = encMethod.getKeyEncapsulationKeyLength() > 0 + ? encMethod.getKeyEncapsulationKeyLength() * 8 + : KeyUtils.getAESKeyBitSizeForWrapAlgorithm(encMethod.getDataEncapsulationAlgorithm()); + KeyDerivationParameters kdp = XMLCipherUtil.constructKeyDerivationParameter(keyDerivationMethod, keyLength); + + KeyEncapsulationParameters keyEncapsulationParameters = new KeyEncapsulationParameters(kemAlgorithm, kdp); + keyEncapsulationParameters.setRecipientPrivateKey((PrivateKey) this.key); + return keyEncapsulationParameters; + } + /** * The method validates whether key agreement data is present and checks if * the provided key is of type PrivateKey. If both conditions are met, it @@ -1920,7 +2039,11 @@ public byte[] decryptToByteArray(Element element) throws XMLEncryptionException } private void validateEncryptionMethodAlgorithm(String encryptionMethodAlgorithm) throws XMLEncryptionException { - if (algorithm != null && !algorithm.equals(encryptionMethodAlgorithm)) { + // Generic Hybrid Cipher (KEM) key transport always uses the "generic-hybrid" URI as the + // top-level EncryptionMethod algorithm, regardless of the AES-KeyWrap algorithm the + // XMLCipher instance was initialised with (which is nested as DataEncapsulationMethod). + if (algorithm != null && !algorithm.equals(encryptionMethodAlgorithm) + && !EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID.equals(encryptionMethodAlgorithm)) { throw new XMLEncryptionException("empty", "EncryptionMethod algorithm \"" + encryptionMethodAlgorithm + "\" does not match the algorithm this XMLCipher was initialised with: \"" @@ -2514,6 +2637,52 @@ EncryptionMethod newEncryptionMethod(Element element) { result.setMGFAlgorithm(mgfAlgorithm); } + Element genericHybridCipherMethodElement = + (Element) element.getElementsByTagNameNS( + EncryptionConstants.EncryptionSpecGHCNS, + EncryptionConstants._TAG_GENERICHYBRIDCIPHERMETHOD).item(0); + if (genericHybridCipherMethodElement != null) { + Element keyEncapsulationMethodElement = + (Element) genericHybridCipherMethodElement.getElementsByTagNameNS( + EncryptionConstants.EncryptionSpecGHCNS, + EncryptionConstants._TAG_KEYENCAPSULATIONMETHOD).item(0); + if (keyEncapsulationMethodElement != null) { + result.setKeyEncapsulationAlgorithm( + keyEncapsulationMethodElement.getAttributeNS(null, "Algorithm")); + + Element keyDerivationMethodElement = + (Element) keyEncapsulationMethodElement.getElementsByTagNameNS( + EncryptionConstants.EncryptionSpec11NS, + EncryptionConstants._TAG_KEYDERIVATIONMETHOD).item(0); + if (keyDerivationMethodElement != null) { + try { + result.setKeyEncapsulationKeyDerivationMethod( + new KeyDerivationMethodImpl(keyDerivationMethodElement, null)); + } catch (XMLSecurityException xse) { + throw new RuntimeException(xse); + } + } + + Element keyLenElement = + (Element) keyEncapsulationMethodElement.getElementsByTagNameNS( + EncryptionConstants.EncryptionSpecGHCNS, + EncryptionConstants._TAG_KEYLEN).item(0); + if (keyLenElement != null) { + result.setKeyEncapsulationKeyLength( + Integer.parseInt(keyLenElement.getFirstChild().getNodeValue())); + } + } + + Element dataEncapsulationMethodElement = + (Element) genericHybridCipherMethodElement.getElementsByTagNameNS( + EncryptionConstants.EncryptionSpecGHCNS, + EncryptionConstants._TAG_DATAENCAPSULATIONMETHOD).item(0); + if (dataEncapsulationMethodElement != null) { + result.setDataEncapsulationAlgorithm( + dataEncapsulationMethodElement.getAttributeNS(null, "Algorithm")); + } + } + // TODO: Make this mess work // @@ -3120,6 +3289,10 @@ private class EncryptionMethodImpl implements EncryptionMethod { private List encryptionMethodInformation; private String digestAlgorithm; private String mgfAlgorithm; + private String keyEncapsulationAlgorithm; + private KeyDerivationMethod keyEncapsulationKeyDerivationMethod; + private int keyEncapsulationKeyLength = Integer.MIN_VALUE; + private String dataEncapsulationAlgorithm; /** * Constructor. @@ -3191,6 +3364,54 @@ public String getMGFAlgorithm() { return mgfAlgorithm; } + /** {@inheritDoc} */ + @Override + public String getKeyEncapsulationAlgorithm() { + return keyEncapsulationAlgorithm; + } + + /** {@inheritDoc} */ + @Override + public void setKeyEncapsulationAlgorithm(String algorithm) { + keyEncapsulationAlgorithm = algorithm; + } + + /** {@inheritDoc} */ + @Override + public KeyDerivationMethod getKeyEncapsulationKeyDerivationMethod() { + return keyEncapsulationKeyDerivationMethod; + } + + /** {@inheritDoc} */ + @Override + public void setKeyEncapsulationKeyDerivationMethod(KeyDerivationMethod keyDerivationMethod) { + keyEncapsulationKeyDerivationMethod = keyDerivationMethod; + } + + /** {@inheritDoc} */ + @Override + public int getKeyEncapsulationKeyLength() { + return keyEncapsulationKeyLength; + } + + /** {@inheritDoc} */ + @Override + public void setKeyEncapsulationKeyLength(int keyLength) { + keyEncapsulationKeyLength = keyLength; + } + + /** {@inheritDoc} */ + @Override + public String getDataEncapsulationAlgorithm() { + return dataEncapsulationAlgorithm; + } + + /** {@inheritDoc} */ + @Override + public void setDataEncapsulationAlgorithm(String algorithm) { + dataEncapsulationAlgorithm = algorithm; + } + /** {@inheritDoc} */ @Override public Iterator getEncryptionMethodInformation() { @@ -3255,6 +3476,50 @@ Element toElement() { ); result.appendChild(mgfElement); } + if (keyEncapsulationAlgorithm != null) { + Element genericHybridCipherMethodElement = + contextDocument.createElementNS( + EncryptionConstants.EncryptionSpecGHCNS, + "ghc:" + EncryptionConstants._TAG_GENERICHYBRIDCIPHERMETHOD + ); + genericHybridCipherMethodElement.setAttributeNS( + Constants.NamespaceSpecNS, "xmlns:ghc", EncryptionConstants.EncryptionSpecGHCNS + ); + + Element keyEncapsulationMethodElement = + contextDocument.createElementNS( + EncryptionConstants.EncryptionSpecGHCNS, + "ghc:" + EncryptionConstants._TAG_KEYENCAPSULATIONMETHOD + ); + keyEncapsulationMethodElement.setAttributeNS(null, "Algorithm", keyEncapsulationAlgorithm); + if (keyEncapsulationKeyDerivationMethod instanceof ElementProxy) { + keyEncapsulationMethodElement.appendChild( + ((ElementProxy) keyEncapsulationKeyDerivationMethod).getElement() + ); + } + if (keyEncapsulationKeyLength > 0) { + Element keyLenElement = + contextDocument.createElementNS( + EncryptionConstants.EncryptionSpecGHCNS, + "ghc:" + EncryptionConstants._TAG_KEYLEN + ); + keyLenElement.appendChild( + contextDocument.createTextNode(String.valueOf(keyEncapsulationKeyLength)) + ); + keyEncapsulationMethodElement.appendChild(keyLenElement); + } + genericHybridCipherMethodElement.appendChild(keyEncapsulationMethodElement); + + Element dataEncapsulationMethodElement = + contextDocument.createElementNS( + EncryptionConstants.EncryptionSpecGHCNS, + "ghc:" + EncryptionConstants._TAG_DATAENCAPSULATIONMETHOD + ); + dataEncapsulationMethodElement.setAttributeNS(null, "Algorithm", dataEncapsulationAlgorithm); + genericHybridCipherMethodElement.appendChild(dataEncapsulationMethodElement); + + result.appendChild(genericHybridCipherMethodElement); + } for (Element element : encryptionMethodInformation) { result.appendChild(element); } diff --git a/src/main/java/org/apache/xml/security/encryption/XMLCipherUtil.java b/src/main/java/org/apache/xml/security/encryption/XMLCipherUtil.java index 95842b139..31d95fb51 100644 --- a/src/main/java/org/apache/xml/security/encryption/XMLCipherUtil.java +++ b/src/main/java/org/apache/xml/security/encryption/XMLCipherUtil.java @@ -22,6 +22,7 @@ import org.apache.xml.security.encryption.keys.content.derivedKey.ConcatKDFParamsImpl; import org.apache.xml.security.encryption.keys.content.derivedKey.HKDFParamsImpl; import org.apache.xml.security.encryption.keys.content.derivedKey.KDFParams; +import org.apache.xml.security.encryption.keys.content.derivedKey.KeyDerivationMethodImpl; import org.apache.xml.security.encryption.params.ConcatKDFParams; import org.apache.xml.security.encryption.params.HKDFParams; import org.apache.xml.security.encryption.params.KeyAgreementParameters; @@ -29,6 +30,7 @@ import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.utils.EncryptionConstants; import org.apache.xml.security.utils.KeyUtils; +import org.w3c.dom.Document; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; @@ -284,6 +286,54 @@ public static KeyDerivationParameters constructKeyDerivationParameter(KeyDerivat throw new XMLEncryptionException("unknownAlgorithm", keyDerivationAlgorithm); } + /** + * Construct a {@code KeyDerivationMethod} DOM element from the given {@link KeyDerivationParameters}. + * The inverse of {@link #constructKeyDerivationParameter(KeyDerivationMethod, int)}. Supports the same + * two key derivation functions as the ECDH-ES/X25519/X448 key-agreement path: ConcatKDF and HKDF. + * + * @param doc the {@link Document} in which the {@code KeyDerivationMethod} element will be created + * @param keyDerivationParameter the key derivation parameters (e.g. {@link HKDFParams} or {@link ConcatKDFParams}) + * @return the constructed {@code KeyDerivationMethod} + * @throws XMLEncryptionException if the key derivation algorithm is not supported + */ + public static KeyDerivationMethod constructKeyDerivationMethod(Document doc, KeyDerivationParameters keyDerivationParameter) + throws XMLEncryptionException { + KeyDerivationMethodImpl keyDerivationMethod = new KeyDerivationMethodImpl(doc); + keyDerivationMethod.setAlgorithm(keyDerivationParameter.getAlgorithm()); + + KDFParams kdfParams; + if (keyDerivationParameter instanceof ConcatKDFParams) { + ConcatKDFParams kdfParameters = (ConcatKDFParams) keyDerivationParameter; + ConcatKDFParamsImpl concatKDFParams = new ConcatKDFParamsImpl(doc); + concatKDFParams.setDigestMethod(kdfParameters.getDigestAlgorithm()); + concatKDFParams.setAlgorithmId(kdfParameters.getAlgorithmID()); + concatKDFParams.setPartyUInfo(kdfParameters.getPartyUInfo()); + concatKDFParams.setPartyVInfo(kdfParameters.getPartyVInfo()); + concatKDFParams.setSuppPubInfo(kdfParameters.getSuppPubInfo()); + concatKDFParams.setSuppPrivInfo(kdfParameters.getSuppPrivInfo()); + kdfParams = concatKDFParams; + } else if (keyDerivationParameter instanceof HKDFParams) { + HKDFParams kdfParameters = (HKDFParams) keyDerivationParameter; + HKDFParamsImpl hkdfParams = new HKDFParamsImpl(doc); + hkdfParams.setPRFAlgorithm(kdfParameters.getHmacHashAlgorithm()); + Base64.Encoder base64Encoder = Base64.getEncoder(); + if (kdfParameters.getSalt() != null) { + hkdfParams.setSalt(base64Encoder.encodeToString(kdfParameters.getSalt())); + } + if (kdfParameters.getInfo() != null) { + hkdfParams.setInfo(base64Encoder.encodeToString(kdfParameters.getInfo())); + } + hkdfParams.setKeyLength(kdfParameters.getKeyBitLength() / 8); + kdfParams = hkdfParams; + } else { + throw new XMLEncryptionException("KeyDerivation.UnsupportedAlgorithm", + keyDerivationParameter.getAlgorithm(), keyDerivationParameter.getClass().getName()); + } + + keyDerivationMethod.setKDFParams(kdfParams); + return keyDerivationMethod; + } + /** * Method hexStringToByteArray converts hex string to byte array. * diff --git a/src/main/java/org/apache/xml/security/encryption/params/KeyEncapsulationParameters.java b/src/main/java/org/apache/xml/security/encryption/params/KeyEncapsulationParameters.java new file mode 100644 index 000000000..6592968fa --- /dev/null +++ b/src/main/java/org/apache/xml/security/encryption/params/KeyEncapsulationParameters.java @@ -0,0 +1,69 @@ +/** + * 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.xml.security.encryption.params; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.AlgorithmParameterSpec; + +/** + * This class is used to pass parameters to the Key Encapsulation Mechanism (KEM) based key + * transport, as specified in the W3C "XML Security: Generic Hybrid Cipher" note + * (https://www.w3.org/TR/xmlsec-generic-hybrid/). Unlike Diffie-Hellman key agreement + * ({@link KeyAgreementParameters}), a KEM has no ephemeral originator key pair: the + * encapsulating party only needs the recipient's public key, and the decapsulating party + * only needs the recipient's private key. + */ +public class KeyEncapsulationParameters implements AlgorithmParameterSpec { + + private final String keyEncapsulationAlgorithm; + private final KeyDerivationParameters keyDerivationParameter; + + private PublicKey recipientPublicKey; + private PrivateKey recipientPrivateKey; + + public KeyEncapsulationParameters(String keyEncapsulationAlgorithm, KeyDerivationParameters keyDerivationParameter) { + this.keyEncapsulationAlgorithm = keyEncapsulationAlgorithm; + this.keyDerivationParameter = keyDerivationParameter; + } + + public String getKeyEncapsulationAlgorithm() { + return keyEncapsulationAlgorithm; + } + + public KeyDerivationParameters getKeyDerivationParameter() { + return keyDerivationParameter; + } + + public PublicKey getRecipientPublicKey() { + return recipientPublicKey; + } + + public void setRecipientPublicKey(PublicKey recipientPublicKey) { + this.recipientPublicKey = recipientPublicKey; + } + + public PrivateKey getRecipientPrivateKey() { + return recipientPrivateKey; + } + + public void setRecipientPrivateKey(PrivateKey recipientPrivateKey) { + this.recipientPrivateKey = recipientPrivateKey; + } +} diff --git a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java index a5c402578..13fab9a72 100644 --- a/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java +++ b/src/main/java/org/apache/xml/security/keys/content/DEREncodedKeyValue.java @@ -40,6 +40,8 @@ public class DEREncodedKeyValue extends Signature11ElementProxy implements KeyIn private static final String[] supportedKeyTypes = { "RSA", "DSA", "EC", "DiffieHellman", "DH", "XDH", "X25519", "X448", "EdDSA", "Ed25519", "Ed448", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", + "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "RSASSA-PSS"}; /** diff --git a/src/main/java/org/apache/xml/security/signature/XMLSignature.java b/src/main/java/org/apache/xml/security/signature/XMLSignature.java index 8e1fa9e9d..9110db529 100644 --- a/src/main/java/org/apache/xml/security/signature/XMLSignature.java +++ b/src/main/java/org/apache/xml/security/signature/XMLSignature.java @@ -211,6 +211,18 @@ public final class XMLSignature extends SignatureElementProxy { public static final String ALGO_ID_SIGNATURE_EDDSA_ED448 = "http://www.w3.org/2021/04/xmldsig-more#eddsa-ed448"; + /**Signature - ML-DSA-44 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + public static final String ALGO_ID_SIGNATURE_MLDSA_44 = + "http://www.w3.org/tbd#ml-dsa-44"; + + /**Signature - ML-DSA-65 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + public static final String ALGO_ID_SIGNATURE_MLDSA_65 = + "http://www.w3.org/tbd#ml-dsa-65"; + + /**Signature - ML-DSA-87 (FIPS 204, provisional URI per draft-eastlake-rfc9231bis-xmlsec-uris section 3.3.15; not yet finalized, see SANTUARIO-634) */ + public static final String ALGO_ID_SIGNATURE_MLDSA_87 = + "http://www.w3.org/tbd#ml-dsa-87"; + /**Signature - SHA3-224withECDSA */ public static final String ALGO_ID_SIGNATURE_ECDSA_SHA3_224 = diff --git a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java index 3368c97a3..dc6308926 100644 --- a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java +++ b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityConstants.java @@ -144,9 +144,12 @@ public enum DIRECTION { public static final String NS_DSIG = "http://www.w3.org/2000/09/xmldsig#"; public static final String NS_DSIG_MORE ="http://www.w3.org/2001/04/xmldsig-more#"; public static final String NS_DSIG_MORE_2007_05 = "http://www.w3.org/2007/05/xmldsig-more#"; + public static final String NS_DSIG_MORE_2021_04 = "http://www.w3.org/2021/04/xmldsig-more#"; public static final String NS_DSIG11 = "http://www.w3.org/2009/xmldsig11#"; public static final String NS_WSSE11 = "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd"; public static final String NS_XOP = "http://www.w3.org/2004/08/xop/include"; + /** W3C "XML Security: Generic Hybrid Cipher" namespace (https://www.w3.org/TR/xmlsec-generic-hybrid/) */ + public static final String NS_GHC = "http://www.w3.org/2010/xmlsec-ghc#"; public static final String PREFIX_XENC = "xenc"; public static final String PREFIX_XENC11 = "xenc11"; @@ -162,6 +165,22 @@ public enum DIRECTION { public static final QName TAG_xenc_OAEPparams = new QName(NS_XMLENC, "OAEPparams", PREFIX_XENC); public static final QName TAG_xenc11_MGF = new QName(NS_XMLENC11, "MGF", PREFIX_XENC11); + public static final QName TAG_xenc11_KeyDerivationMethod = new QName(NS_XMLENC11, "KeyDerivationMethod", PREFIX_XENC11); + + public static final String PREFIX_GHC = "ghc"; + public static final QName TAG_ghc_GenericHybridCipherMethod = new QName(NS_GHC, "GenericHybridCipherMethod", PREFIX_GHC); + public static final QName TAG_ghc_KeyEncapsulationMethod = new QName(NS_GHC, "KeyEncapsulationMethod", PREFIX_GHC); + public static final QName TAG_ghc_DataEncapsulationMethod = new QName(NS_GHC, "DataEncapsulationMethod", PREFIX_GHC); + public static final QName TAG_ghc_KeyLen = new QName(NS_GHC, "KeyLen", PREFIX_GHC); + + public static final String PREFIX_HKDF = "hkdf"; + public static final QName TAG_hkdf_HKDFParams = new QName(NS_DSIG_MORE_2021_04, "HKDFParams", PREFIX_HKDF); + public static final QName TAG_hkdf_PRF = new QName(NS_DSIG_MORE_2021_04, "PRF", PREFIX_HKDF); + public static final QName TAG_hkdf_Salt = new QName(NS_DSIG_MORE_2021_04, "Salt", PREFIX_HKDF); + public static final QName TAG_hkdf_Info = new QName(NS_DSIG_MORE_2021_04, "Info", PREFIX_HKDF); + public static final QName TAG_hkdf_KeyLength = new QName(NS_DSIG_MORE_2021_04, "KeyLength", PREFIX_HKDF); + /** HKDF key derivation algorithm URI (RFC 9231 provisional naming pattern) */ + public static final String NS_HKDF = NS_DSIG_MORE_2021_04 + "hkdf"; public static final String PREFIX_DSIG = "dsig"; public static final String PREFIX_DSIG_MORE_PSS = "pss"; diff --git a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java index f04ac09aa..42de29640 100644 --- a/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java +++ b/src/main/java/org/apache/xml/security/stax/ext/XMLSecurityProperties.java @@ -52,6 +52,11 @@ public class XMLSecurityProperties { private String encryptionKeyTransportDigestAlgorithm; private String encryptionKeyTransportMGFAlgorithm; private byte[] encryptionKeyTransportOAEPParams; + // Generic Hybrid Cipher (W3C xmlsec-generic-hybrid) KEM-based key transport, e.g. ML-KEM (SANTUARIO-633). + // Used when encryptionKeyTransportAlgorithm is EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID. + private String encryptionKeyEncapsulationAlgorithm; + private String encryptionDataEncapsulationAlgorithm; + private String encryptionKeyEncapsulationHmacAlgorithm; private final List encryptionParts = new LinkedList<>(); private Key encryptionKey; private Key encryptionTransportKey; @@ -100,6 +105,9 @@ protected XMLSecurityProperties(XMLSecurityProperties xmlSecurityProperties) { this.encryptionKeyTransportDigestAlgorithm = xmlSecurityProperties.encryptionKeyTransportDigestAlgorithm; this.encryptionKeyTransportMGFAlgorithm = xmlSecurityProperties.encryptionKeyTransportMGFAlgorithm; this.encryptionKeyTransportOAEPParams = xmlSecurityProperties.encryptionKeyTransportOAEPParams; + this.encryptionKeyEncapsulationAlgorithm = xmlSecurityProperties.encryptionKeyEncapsulationAlgorithm; + this.encryptionDataEncapsulationAlgorithm = xmlSecurityProperties.encryptionDataEncapsulationAlgorithm; + this.encryptionKeyEncapsulationHmacAlgorithm = xmlSecurityProperties.encryptionKeyEncapsulationHmacAlgorithm; this.encryptionParts.addAll(xmlSecurityProperties.encryptionParts); this.encryptionKey = xmlSecurityProperties.encryptionKey; this.encryptionTransportKey = xmlSecurityProperties.encryptionTransportKey; @@ -333,6 +341,43 @@ public void setEncryptionKeyTransportOAEPParams(byte[] encryptionKeyTransportOAE this.encryptionKeyTransportOAEPParams = encryptionKeyTransportOAEPParams; } + /** + * Returns the Key Encapsulation Method algorithm URI (e.g. an ML-KEM algorithm URI) used when + * {@link #getEncryptionKeyTransportAlgorithm()} is the Generic Hybrid Cipher algorithm + * (see {@code EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID}, SANTUARIO-633). + */ + public String getEncryptionKeyEncapsulationAlgorithm() { + return encryptionKeyEncapsulationAlgorithm; + } + + public void setEncryptionKeyEncapsulationAlgorithm(String encryptionKeyEncapsulationAlgorithm) { + this.encryptionKeyEncapsulationAlgorithm = encryptionKeyEncapsulationAlgorithm; + } + + /** + * Returns the Data Encapsulation Method algorithm URI (an AES-KeyWrap algorithm) used when + * {@link #getEncryptionKeyTransportAlgorithm()} is the Generic Hybrid Cipher algorithm. + */ + public String getEncryptionDataEncapsulationAlgorithm() { + return encryptionDataEncapsulationAlgorithm; + } + + public void setEncryptionDataEncapsulationAlgorithm(String encryptionDataEncapsulationAlgorithm) { + this.encryptionDataEncapsulationAlgorithm = encryptionDataEncapsulationAlgorithm; + } + + /** + * Returns the HMAC hash algorithm URI used as the HKDF PRF when deriving the data-encapsulation + * (AES-KeyWrap) key from the KEM shared secret. Defaults to HMAC-SHA256 if unset. + */ + public String getEncryptionKeyEncapsulationHmacAlgorithm() { + return encryptionKeyEncapsulationHmacAlgorithm; + } + + public void setEncryptionKeyEncapsulationHmacAlgorithm(String encryptionKeyEncapsulationHmacAlgorithm) { + this.encryptionKeyEncapsulationHmacAlgorithm = encryptionKeyEncapsulationHmacAlgorithm; + } + public X509Certificate getEncryptionUseThisCertificate() { return encryptionUseThisCertificate; } diff --git a/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java b/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java index 5cc5ddbd7..13a406c69 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java +++ b/src/main/java/org/apache/xml/security/stax/impl/algorithms/PKISignatureAlgorithm.java @@ -126,7 +126,7 @@ public byte[] engineSign() throws XMLSecurityException { byte[] jcebytes = signature.sign(); if (this.jceName.contains("ECDSA")) { return ECDSAUtils.convertASN1toXMLDSIG(jcebytes, signIntLen); - } else if (this.jceName.contains("DSA")) { + } else if (this.jceName.contains("DSA") && !this.jceName.startsWith("ML-DSA")) { return JavaUtils.convertDsaASN1toXMLDSIG(jcebytes, 20); } return jcebytes; @@ -152,7 +152,7 @@ public boolean engineVerify(byte[] signature) throws XMLSecurityException { byte[] jcebytes = signature; if (this.jceName.contains("ECDSA")) { jcebytes = ECDSAUtils.convertXMLDSIGtoASN1(jcebytes); - } else if (this.jceName.contains("DSA")) { + } else if (this.jceName.contains("DSA") && !this.jceName.startsWith("ML-DSA")) { jcebytes = JavaUtils.convertDsaXMLDSIGtoASN1(jcebytes, 20); } return this.signature.verify(jcebytes); diff --git a/src/main/java/org/apache/xml/security/stax/impl/processor/input/XMLEncryptedKeyInputHandler.java b/src/main/java/org/apache/xml/security/stax/impl/processor/input/XMLEncryptedKeyInputHandler.java index 1bcf1bf5e..ac21e0c95 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/processor/input/XMLEncryptedKeyInputHandler.java +++ b/src/main/java/org/apache/xml/security/stax/impl/processor/input/XMLEncryptedKeyInputHandler.java @@ -28,9 +28,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; +import java.security.PrivateKey; import java.security.spec.MGF1ParameterSpec; import java.util.Base64; import java.util.Deque; +import java.util.List; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; @@ -45,6 +47,9 @@ import org.apache.xml.security.binding.xmlenc.EncryptedKeyType; import org.apache.xml.security.binding.xmlenc11.MGFType; import org.apache.xml.security.binding.xop.Include; +import org.apache.xml.security.encryption.XMLCipherUtil; +import org.apache.xml.security.encryption.keys.content.derivedKey.KeyDerivationMethodImpl; +import org.apache.xml.security.encryption.params.KeyDerivationParameters; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.stax.ext.AbstractInputSecurityHeaderHandler; import org.apache.xml.security.stax.ext.InboundSecurityContext; @@ -61,7 +66,10 @@ import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; import org.apache.xml.security.stax.securityToken.SecurityTokenFactory; import org.apache.xml.security.stax.securityToken.SecurityTokenProvider; +import org.apache.xml.security.utils.EncryptionConstants; +import org.apache.xml.security.utils.KeyUtils; import org.apache.xml.security.utils.XMLUtils; +import org.w3c.dom.Element; /** * An input handler for the EncryptedKey XML Structure @@ -170,6 +178,17 @@ private byte[] getSecret(InboundSecurityToken wrappedSecurityToken, String corre if (algorithmURI == null) { throw new XMLSecurityException("stax.encryption.noEncAlgo"); } + + final InboundSecurityToken wrappingSecurityToken = getWrappingSecurityToken(wrappedSecurityToken); + XMLSecurityConstants.AlgorithmUsage algorithmUsage = + wrappingSecurityToken.isAsymmetric() + ? XMLSecurityConstants.Asym_Key_Wrap : XMLSecurityConstants.Sym_Key_Wrap; + + if (EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID.equals(algorithmURI)) { + return this.decryptedKey = getGenericHybridSecret( + wrappingSecurityToken, correlationID, algorithmUsage, symmetricAlgorithmURI); + } + String jceName = JCEMapper.translateURItoJCEID(algorithmURI); String jceProvider = JCEMapper.getJCEProviderFromURI(algorithmURI); if (jceName == null) { @@ -177,17 +196,8 @@ private byte[] getSecret(InboundSecurityToken wrappedSecurityToken, String corre new Object[] {algorithmURI}); } - final InboundSecurityToken wrappingSecurityToken = getWrappingSecurityToken(wrappedSecurityToken); - Cipher cipher; try { - XMLSecurityConstants.AlgorithmUsage algorithmUsage; - if (wrappingSecurityToken.isAsymmetric()) { - algorithmUsage = XMLSecurityConstants.Asym_Key_Wrap; - } else { - algorithmUsage = XMLSecurityConstants.Sym_Key_Wrap; - } - if (jceProvider == null) { cipher = Cipher.getInstance(jceName); } else { @@ -260,6 +270,100 @@ private byte[] getSecret(InboundSecurityToken wrappedSecurityToken, String corre return this.decryptedKey; } } + + /** + * Decapsulate/unwrap a CEK transported via KEM-based (Generic Hybrid Cipher) key + * transport, per https://www.w3.org/TR/xmlsec-generic-hybrid/ (see SANTUARIO-633). + * The {@code ghc:GenericHybridCipherMethod} element has no JAXB binding, so it is + * read back as a raw DOM {@link Element} from the wildcard EncryptionMethod content + * and parsed with the same DOM classes ({@link KeyDerivationMethodImpl}, + * {@link XMLCipherUtil}) used by the DOM {@code XMLCipher} API for the same structure. + */ + private byte[] getGenericHybridSecret(InboundSecurityToken wrappingSecurityToken, String correlationID, + XMLSecurityConstants.AlgorithmUsage algorithmUsage, + String symmetricAlgorithmURI) throws XMLSecurityException { + try { + Element genericHybridCipherMethodElement = findAnyElement( + encryptedKeyType.getEncryptionMethod().getContent(), + XMLSecurityConstants.TAG_ghc_GenericHybridCipherMethod); + if (genericHybridCipherMethodElement == null) { + throw new XMLSecurityException("stax.unsupportedKeyTransp"); + } + + Element keyEncapsulationMethodElement = (Element) genericHybridCipherMethodElement + .getElementsByTagNameNS(XMLSecurityConstants.NS_GHC, "KeyEncapsulationMethod").item(0); + Element dataEncapsulationMethodElement = (Element) genericHybridCipherMethodElement + .getElementsByTagNameNS(XMLSecurityConstants.NS_GHC, "DataEncapsulationMethod").item(0); + if (keyEncapsulationMethodElement == null || dataEncapsulationMethodElement == null) { + throw new XMLSecurityException("stax.unsupportedKeyTransp"); + } + + String kemAlgorithm = keyEncapsulationMethodElement.getAttributeNS(null, "Algorithm"); + String dataEncapsulationAlgorithm = dataEncapsulationMethodElement.getAttributeNS(null, "Algorithm"); + + Element keyDerivationMethodElement = (Element) keyEncapsulationMethodElement + .getElementsByTagNameNS(XMLSecurityConstants.NS_XMLENC11, "KeyDerivationMethod").item(0); + if (keyDerivationMethodElement == null) { + throw new XMLSecurityException("stax.unsupportedKeyTransp"); + } + + int wrapKeyBitLength = KeyUtils.getAESKeyBitSizeForWrapAlgorithm(dataEncapsulationAlgorithm); + KeyDerivationMethodImpl keyDerivationMethod = new KeyDerivationMethodImpl(keyDerivationMethodElement, null); + KeyDerivationParameters kdp = XMLCipherUtil.constructKeyDerivationParameter(keyDerivationMethod, wrapKeyBitLength); + + Key wrapKeyToken = wrappingSecurityToken.getSecretKey(kemAlgorithm, algorithmUsage, correlationID); + if (!(wrapKeyToken instanceof PrivateKey)) { + throw new XMLSecurityException("stax.unsupportedKeyTransp"); + } + + if (encryptedKeyType.getCipherData() == null + || encryptedKeyType.getCipherData().getCipherValue() == null + || encryptedKeyType.getCipherData().getCipherValue().getContent() == null + || encryptedKeyType.getCipherData().getCipherValue().getContent().isEmpty()) { + throw new XMLSecurityException("stax.encryption.noCipherValue"); + } + + byte[] encryptedBytes = getEncryptedBytes(encryptedKeyType.getCipherData().getCipherValue()); + byte[] sha1Bytes = generateDigest(encryptedBytes); + String sha1Identifier = XMLUtils.encodeToString(sha1Bytes); + super.setSha1Identifier(sha1Identifier); + + try { + KeyUtils.KemDecapsulation kemResult = KeyUtils.kemDecapsulate( + (PrivateKey) wrapKeyToken, kemAlgorithm, encryptedBytes, kdp); + String jceWrapId = JCEMapper.translateURItoJCEID(dataEncapsulationAlgorithm); + Cipher cipher = Cipher.getInstance(jceWrapId); + cipher.init(Cipher.UNWRAP_MODE, kemResult.getWrapKey()); + Key key = cipher.unwrap(kemResult.getWrappedKey(), "AES", Cipher.SECRET_KEY); + return key.getEncoded(); + } catch (IllegalStateException e) { + throw new XMLSecurityException(e); + } catch (Exception e) { + LOG.log(Level.WARNING, "Unwrapping of the encrypted key failed with error: " + + e.getMessage() + ". Generating a faked one to mitigate timing attacks."); + + int keyLength = JCEMapper.getKeyLengthFromURI(symmetricAlgorithmURI); + return XMLSecurityConstants.generateBytes(keyLength / 8); + } + } catch (XMLSecurityException e) { + throw e; + } catch (Exception e) { + throw new XMLSecurityException(e); + } + } + + private Element findAnyElement(List content, javax.xml.namespace.QName qname) { + for (Object o : content) { + if (o instanceof Element) { + Element el = (Element) o; + if (qname.getNamespaceURI().equals(el.getNamespaceURI()) + && qname.getLocalPart().equals(el.getLocalName())) { + return el; + } + } + } + return null; + } }; this.securityToken.setElementPath(responsibleXMLSecStartXMLEvent.getElementPath()); this.securityToken.setXMLSecEvent(responsibleXMLSecStartXMLEvent); diff --git a/src/main/java/org/apache/xml/security/stax/impl/processor/output/XMLEncryptOutputProcessor.java b/src/main/java/org/apache/xml/security/stax/impl/processor/output/XMLEncryptOutputProcessor.java index f6ccd8ce9..2e6d32e77 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/processor/output/XMLEncryptOutputProcessor.java +++ b/src/main/java/org/apache/xml/security/stax/impl/processor/output/XMLEncryptOutputProcessor.java @@ -40,6 +40,8 @@ import javax.xml.stream.XMLStreamException; import org.apache.xml.security.algorithms.JCEMapper; +import org.apache.xml.security.encryption.params.HKDFParams; +import org.apache.xml.security.encryption.params.KeyDerivationParameters; import org.apache.xml.security.exceptions.XMLSecurityException; import org.apache.xml.security.stax.ext.OutputProcessorChain; import org.apache.xml.security.stax.ext.SecurePart; @@ -53,6 +55,8 @@ import org.apache.xml.security.stax.securityToken.OutboundSecurityToken; import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; import org.apache.xml.security.stax.securityToken.SecurityTokenProvider; +import org.apache.xml.security.utils.EncryptionConstants; +import org.apache.xml.security.utils.KeyUtils; import org.apache.xml.security.utils.XMLUtils; /** @@ -149,7 +153,30 @@ protected void createKeyInfoStructure(OutputProcessorChain outputProcessorChain) attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Id, keyId)); createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_EncryptedKey, true, attributes); - attributes = new ArrayList<>(1); + if (EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID.equals(encryptionKeyTransportAlgorithm)) { + if (pubKey == null) { + throw new XMLSecurityException("stax.unsupportedKeyTransp"); + } + createGenericHybridCipherKeyInfoStructure(outputProcessorChain, pubKey); + } else { + createFlatKeyTransportKeyInfoStructure( + outputProcessorChain, encryptionKeyTransportAlgorithm, pubKey, secretKey); + } + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_EncryptedKey); + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig_KeyInfo); + } + + /** + * Writes the EncryptionMethod/KeyInfo/CipherData for a flat (RSA-OAEP-style) key + * transport algorithm, unchanged from before Generic Hybrid Cipher support was added. + */ + private void createFlatKeyTransportKeyInfoStructure( + OutputProcessorChain outputProcessorChain, String encryptionKeyTransportAlgorithm, + PublicKey pubKey, Key secretKey) throws XMLStreamException, XMLSecurityException { + + List attributes = new ArrayList<>(1); attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Algorithm, encryptionKeyTransportAlgorithm)); createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_EncryptionMethod, false, attributes); @@ -265,10 +292,111 @@ protected void createKeyInfoStructure(OutputProcessorChain outputProcessorChain) createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_CipherValue); createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_CipherData); + } - createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_EncryptedKey); + /** + * Writes the EncryptionMethod/KeyInfo/CipherData for KEM-based (Generic Hybrid Cipher) + * key transport, per https://www.w3.org/TR/xmlsec-generic-hybrid/ section 6.1 + * "Key Transport Example" (see SANTUARIO-633). + */ + private void createGenericHybridCipherKeyInfoStructure( + OutputProcessorChain outputProcessorChain, PublicKey pubKey) + throws XMLStreamException, XMLSecurityException { - createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_dsig_KeyInfo); + String kemAlgorithm = getSecurityProperties().getEncryptionKeyEncapsulationAlgorithm(); + String dataEncapsulationAlgorithm = getSecurityProperties().getEncryptionDataEncapsulationAlgorithm(); + if (kemAlgorithm == null || dataEncapsulationAlgorithm == null) { + throw new XMLSecurityException("stax.unsupportedKeyTransp"); + } + String hmacAlgorithm = getSecurityProperties().getEncryptionKeyEncapsulationHmacAlgorithm(); + if (hmacAlgorithm == null) { + hmacAlgorithm = XMLSecurityConstants.NS_XMLDSIG_HMACSHA256; + } + + int wrapKeyBitLength = KeyUtils.getAESKeyBitSizeForWrapAlgorithm(dataEncapsulationAlgorithm); + KeyDerivationParameters kdfParams = HKDFParams.createBuilder(wrapKeyBitLength, hmacAlgorithm).build(); + + // EncryptionMethod/ghc:GenericHybridCipherMethod/ghc:KeyEncapsulationMethod/... + List attributes = new ArrayList<>(1); + attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Algorithm, + EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID)); + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_EncryptionMethod, false, attributes); + + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_GenericHybridCipherMethod, true, null); + + attributes = new ArrayList<>(1); + attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Algorithm, kemAlgorithm)); + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_KeyEncapsulationMethod, true, attributes); + + attributes = new ArrayList<>(1); + attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Algorithm, XMLSecurityConstants.NS_HKDF)); + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc11_KeyDerivationMethod, true, attributes); + + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_hkdf_HKDFParams, true, null); + + attributes = new ArrayList<>(1); + attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Algorithm, hmacAlgorithm)); + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_hkdf_PRF, true, attributes); + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_hkdf_PRF); + + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_hkdf_KeyLength, false, null); + createCharactersAndOutputAsEvent(outputProcessorChain, String.valueOf(wrapKeyBitLength / 8)); + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_hkdf_KeyLength); + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_hkdf_HKDFParams); + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc11_KeyDerivationMethod); + + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_KeyLen, false, null); + createCharactersAndOutputAsEvent(outputProcessorChain, String.valueOf(wrapKeyBitLength / 8)); + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_KeyLen); + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_KeyEncapsulationMethod); + + attributes = new ArrayList<>(1); + attributes.add(createAttribute(XMLSecurityConstants.ATT_NULL_Algorithm, dataEncapsulationAlgorithm)); + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_DataEncapsulationMethod, true, attributes); + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_DataEncapsulationMethod); + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_ghc_GenericHybridCipherMethod); + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_EncryptionMethod); + + createKeyInfoStructureForEncryptedKey(outputProcessorChain, keyWrappingToken); + + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_CipherData, false, null); + createStartElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_CipherValue, false, null); + + String tokenId = outputProcessorChain.getSecurityContext().get( + XMLSecurityConstants.PROP_USE_THIS_TOKEN_ID_FOR_ENCRYPTION); + SecurityTokenProvider securityTokenProvider = + outputProcessorChain.getSecurityContext().getSecurityTokenProvider(tokenId); + final OutboundSecurityToken securityToken = securityTokenProvider.getSecurityToken(); + Key sessionKey = securityToken.getSecretKey(getSecurityProperties().getEncryptionSymAlgorithm()); + + // Encapsulate a shared secret to the recipient's KEM public key, derive the + // AES key-wrap key from it, and wrap the CEK with that key. CipherValue holds + // the concatenation of the KEM encapsulation (C0) and the wrapped CEK (C1). + KeyUtils.KemEncapsulation kemResult = KeyUtils.kemEncapsulate(pubKey, kemAlgorithm, kdfParams); + try { + String jceWrapId = JCEMapper.translateURItoJCEID(dataEncapsulationAlgorithm); + Cipher wrapCipher = Cipher.getInstance(jceWrapId); + wrapCipher.init(Cipher.WRAP_MODE, kemResult.getWrapKey()); + byte[] wrappedKey = wrapCipher.wrap(sessionKey); + + byte[] encapsulation = kemResult.getEncapsulation(); + byte[] combined = new byte[encapsulation.length + wrappedKey.length]; + System.arraycopy(encapsulation, 0, combined, 0, encapsulation.length); + System.arraycopy(wrappedKey, 0, combined, encapsulation.length, wrappedKey.length); + + createCharactersAndOutputAsEvent(outputProcessorChain, XMLUtils.encodeToString(combined)); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException + | IllegalBlockSizeException e) { + throw new XMLSecurityException(e); + } + + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_CipherValue); + createEndElementAndOutputAsEvent(outputProcessorChain, XMLSecurityConstants.TAG_xenc_CipherData); } protected void createKeyInfoStructureForEncryptedKey( diff --git a/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java b/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java index 9aacd281d..66d737093 100644 --- a/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java +++ b/src/main/java/org/apache/xml/security/stax/impl/securityToken/AbstractInboundSecurityToken.java @@ -19,6 +19,7 @@ package org.apache.xml.security.stax.impl.securityToken; import java.security.Key; +import java.security.PrivateKey; import java.security.PublicKey; import java.security.interfaces.DSAKey; import java.security.interfaces.ECKey; @@ -139,6 +140,10 @@ public final Key getSecretKey(String algorithmURI, XMLSecurityConstants.Algorith algorithmSuiteSecurityEvent.setKeyLength(((ECKey) key).getParams().getOrder().bitLength()); } else if (key instanceof SecretKey) { algorithmSuiteSecurityEvent.setKeyLength(key.getEncoded().length * 8); + } else if (key instanceof PrivateKey) { + // PQC or other asymmetric key types (e.g. ML-KEM): key length not classically defined + byte[] encoded = key.getEncoded(); + algorithmSuiteSecurityEvent.setKeyLength(encoded != null ? encoded.length * 8 : 0); } else { throw new XMLSecurityException("java.security.UnknownKeyType", new Object[] {key.getClass().getName()}); @@ -174,8 +179,9 @@ public final PublicKey getPublicKey(String algorithmURI, XMLSecurityConstants.Al } else if (publicKey instanceof ECKey) { algorithmSuiteSecurityEvent.setKeyLength(((ECKey) publicKey).getParams().getOrder().bitLength()); } else { - throw new XMLSecurityException("java.security.UnknownKeyType", - new Object[] {publicKey.getClass().getName()}); + // PQC or other asymmetric public key types (e.g. ML-DSA, ML-KEM): key length not classically defined + byte[] encoded = publicKey.getEncoded(); + algorithmSuiteSecurityEvent.setKeyLength(encoded != null ? encoded.length * 8 : 0); } inboundSecurityContext.registerSecurityEvent(algorithmSuiteSecurityEvent); } diff --git a/src/main/java/org/apache/xml/security/utils/EncryptionConstants.java b/src/main/java/org/apache/xml/security/utils/EncryptionConstants.java index c752c0427..15a93cee4 100644 --- a/src/main/java/org/apache/xml/security/utils/EncryptionConstants.java +++ b/src/main/java/org/apache/xml/security/utils/EncryptionConstants.java @@ -138,6 +138,18 @@ public final class EncryptionConstants { /** Tag of Element KEY LENGTH **/ public static final String _TAG_KEYLENGTH = "KeyLength"; + /** Tag of Element GenericHybridCipherMethod **/ + public static final String _TAG_GENERICHYBRIDCIPHERMETHOD = "GenericHybridCipherMethod"; + + /** Tag of Element KeyEncapsulationMethod **/ + public static final String _TAG_KEYENCAPSULATIONMETHOD = "KeyEncapsulationMethod"; + + /** Tag of Element DataEncapsulationMethod **/ + public static final String _TAG_DATAENCAPSULATIONMETHOD = "DataEncapsulationMethod"; + + /** Tag of Element KeyLen **/ + public static final String _TAG_KEYLEN = "KeyLen"; + /** Field ENCRYPTIONSPECIFICATION_URL */ public static final String ENCRYPTIONSPECIFICATION_URL = "http://www.w3.org/TR/2001/WD-xmlenc-core-20010626/"; @@ -154,6 +166,13 @@ public final class EncryptionConstants { public static final String EncryptionSpec11NS = "http://www.w3.org/2009/xmlenc11#"; + /** + * The namespace of the W3C XML Security: Generic Hybrid Cipher specification + * (https://www.w3.org/TR/xmlsec-generic-hybrid/) + */ + public static final String EncryptionSpecGHCNS = + "http://www.w3.org/2010/xmlsec-ghc#"; + /** URI for content*/ public static final String TYPE_CONTENT = EncryptionSpecNS + "Content"; @@ -220,6 +239,36 @@ public final class EncryptionConstants { public static final String ALGO_ID_KEYTRANSPORT_RSAOAEP_11 = EncryptionConstants.EncryptionSpec11NS + "rsa-oaep"; + /** + * Key Transport - Generic Hybrid Cipher (W3C xmlsec-generic-hybrid). Used as the + * top-level {@code xenc:EncryptionMethod} algorithm for KEM-based key transport (e.g. + * ML-KEM, see SANTUARIO-633); the actual key encapsulation algorithm is identified by + * the {@code KeyEncapsulationMethod/@Algorithm} attribute of the nested + * {@code ghc:GenericHybridCipherMethod} element (see {@link #ALGO_ID_KEYTRANSPORT_MLKEM_512} + * and friends below). + */ + public static final String ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID = + EncryptionConstants.EncryptionSpecGHCNS + "generic-hybrid"; + + // Provisional URIs for ML-KEM (FIPS 203) key encapsulation, per + // draft-eastlake-rfc9231bis-xmlsec-uris section 3.6.9. These use the draft's "tbd" + // placeholder namespace; no official W3C URI has been assigned yet, update once + // standardised (see SANTUARIO-634). Used as the value of + // ghc:GenericHybridCipherMethod/ghc:KeyEncapsulationMethod/@Algorithm, not as a + // top-level xenc:EncryptionMethod algorithm (see SANTUARIO-633, ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID). + + /** Key Encapsulation - ML-KEM-512 (FIPS 203, NIST security level 1) */ + public static final String ALGO_ID_KEYTRANSPORT_MLKEM_512 = + "http://www.w3.org/tbd#ml-kem-512"; + + /** Key Encapsulation - ML-KEM-768 (FIPS 203, NIST security level 3) */ + public static final String ALGO_ID_KEYTRANSPORT_MLKEM_768 = + "http://www.w3.org/tbd#ml-kem-768"; + + /** Key Encapsulation - ML-KEM-1024 (FIPS 203, NIST security level 5) */ + public static final String ALGO_ID_KEYTRANSPORT_MLKEM_1024 = + "http://www.w3.org/tbd#ml-kem-1024"; + /** Key Agreement - OPTIONAL Diffie-Hellman */ public static final String ALGO_ID_KEYAGREEMENT_DH = EncryptionConstants.EncryptionSpecNS + "dh"; diff --git a/src/main/java/org/apache/xml/security/utils/KeyUtils.java b/src/main/java/org/apache/xml/security/utils/KeyUtils.java index 7481597a9..913a53686 100644 --- a/src/main/java/org/apache/xml/security/utils/KeyUtils.java +++ b/src/main/java/org/apache/xml/security/utils/KeyUtils.java @@ -18,6 +18,7 @@ */ package org.apache.xml.security.utils; +import org.apache.xml.security.algorithms.JCEMapper; import org.apache.xml.security.algorithms.implementations.ECDSAUtils; import org.apache.xml.security.encryption.XMLEncryptionException; import org.apache.xml.security.encryption.keys.content.derivedKey.ConcatKDF; @@ -33,6 +34,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.lang.System.Logger.Level; +import java.lang.reflect.Method; import java.security.*; import java.security.interfaces.ECPublicKey; import java.security.spec.ECGenParameterSpec; @@ -312,4 +314,150 @@ public static byte[] deriveKeyWithConcatKDF(byte[] sharedSecret, ConcatKDFParams ConcatKDF concatKDF = new ConcatKDF(); return concatKDF.deriveKey(sharedSecret, ckdfParameter); } + + // Fully-qualified javax.crypto.KEM class names. This module targets Java 11 + // (see maven.compiler.release), but the KEM API (JEP 452) is only available since + // Java 21, so it is accessed via reflection rather than a compile-time import - the + // same reason the ML-DSA/ML-KEM JCA algorithm names are looked up dynamically rather + // than depending on BouncyCastle at compile time. + private static final String KEM_CLASS = "javax.crypto.KEM"; + private static final String KEM_ENCAPSULATOR_CLASS = "javax.crypto.KEM$Encapsulator"; + private static final String KEM_DECAPSULATOR_CLASS = "javax.crypto.KEM$Decapsulator"; + private static final String KEM_ENCAPSULATED_CLASS = "javax.crypto.KEM$Encapsulated"; + + /** + * The result of a KEM encapsulation: the encapsulation/ciphertext (traditionally called "C0") + * to be sent to the recipient, and the AES key-wrap key derived from the KEM shared secret. + */ + public static final class KemEncapsulation { + private final byte[] encapsulation; + private final SecretKey wrapKey; + + KemEncapsulation(byte[] encapsulation, SecretKey wrapKey) { + this.encapsulation = encapsulation; + this.wrapKey = wrapKey; + } + + public byte[] getEncapsulation() { + return encapsulation; + } + + public SecretKey getWrapKey() { + return wrapKey; + } + } + + /** + * The result of a KEM decapsulation: the AES key-wrap key derived from the KEM shared secret, + * and the remainder of the ciphertext (traditionally called "C1", the AES-wrapped CEK) once the + * leading KEM encapsulation octets have been stripped off. + */ + public static final class KemDecapsulation { + private final SecretKey wrapKey; + private final byte[] wrappedKey; + + KemDecapsulation(SecretKey wrapKey, byte[] wrappedKey) { + this.wrapKey = wrapKey; + this.wrappedKey = wrappedKey; + } + + public SecretKey getWrapKey() { + return wrapKey; + } + + public byte[] getWrappedKey() { + return wrappedKey; + } + } + + /** + * Encapsulate a fresh shared secret to the recipient's KEM public key (e.g. ML-KEM, FIPS 203) and + * derive an AES key-wrap key from it, per the W3C "XML Security: Generic Hybrid Cipher" note + * (https://www.w3.org/TR/xmlsec-generic-hybrid/, section 5 "Using Key Encapsulation Algorithms for + * Key Transport"). Uses the JDK's {@code javax.crypto.KEM} API via reflection - see {@link #KEM_CLASS}. + * + * @param recipientPublicKey the recipient's KEM public key + * @param kemAlgorithmURI the KEM algorithm URI (e.g. {@code ALGO_ID_KEYTRANSPORT_MLKEM_512}) + * @param keyDerivationParameter the key derivation parameters used to derive the AES key-wrap key + * from the KEM shared secret + * @return the KEM encapsulation (C0) and the derived AES key-wrap key + * @throws XMLEncryptionException if the KEM API is unavailable (requires Java 21+), the KEM + * algorithm is not supported by the configured JCE provider, or key derivation fails + */ + public static KemEncapsulation kemEncapsulate(PublicKey recipientPublicKey, String kemAlgorithmURI, + KeyDerivationParameters keyDerivationParameter) + throws XMLEncryptionException { + try { + String jceKemName = JCEMapper.translateURItoJCEID(kemAlgorithmURI); + Object kem = kemGetInstance(jceKemName); + Object encapsulator = invoke(kem, kem.getClass(), "newEncapsulator", + new Class[]{PublicKey.class}, recipientPublicKey); + Object encapsulated = invoke(encapsulator, Class.forName(KEM_ENCAPSULATOR_CLASS), "encapsulate", + new Class[0]); + Class encapsulatedClass = Class.forName(KEM_ENCAPSULATED_CLASS); + byte[] c0 = (byte[]) invoke(encapsulated, encapsulatedClass, "encapsulation", new Class[0]); + SecretKey sharedSecret = (SecretKey) invoke(encapsulated, encapsulatedClass, "key", new Class[0]); + byte[] kek = deriveKeyEncryptionKey(sharedSecret.getEncoded(), keyDerivationParameter); + return new KemEncapsulation(c0, new SecretKeySpec(kek, "AES")); + } catch (ReflectiveOperationException e) { + throw new XMLEncryptionException(e); + } catch (XMLSecurityException e) { + throw new XMLEncryptionException(e); + } + } + + /** + * Decapsulate a shared secret using the recipient's KEM private key and derive the AES key-wrap + * key from it, splitting the leading KEM encapsulation octets (C0) off the combined ciphertext + * first (its length is algorithm-specific and obtained from the KEM API itself, so no hardcoded + * per-algorithm length table is required). See {@link #kemEncapsulate}. + * + * @param recipientPrivateKey the recipient's KEM private key + * @param kemAlgorithmURI the KEM algorithm URI (e.g. {@code ALGO_ID_KEYTRANSPORT_MLKEM_512}) + * @param combinedCiphertext the concatenation of the KEM encapsulation (C0) and the AES-wrapped + * CEK (C1), as read from {@code xenc:CipherValue} + * @param keyDerivationParameter the key derivation parameters used to derive the AES key-wrap key + * from the KEM shared secret + * @return the derived AES key-wrap key, and the remaining AES-wrapped CEK bytes (C1) + * @throws XMLEncryptionException if the KEM API is unavailable (requires Java 21+), the KEM + * algorithm is not supported by the configured JCE provider, the ciphertext is shorter + * than the algorithm's expected encapsulation size, or key derivation fails + */ + public static KemDecapsulation kemDecapsulate(PrivateKey recipientPrivateKey, String kemAlgorithmURI, + byte[] combinedCiphertext, KeyDerivationParameters keyDerivationParameter) + throws XMLEncryptionException { + try { + String jceKemName = JCEMapper.translateURItoJCEID(kemAlgorithmURI); + Object kem = kemGetInstance(jceKemName); + Object decapsulator = invoke(kem, kem.getClass(), "newDecapsulator", + new Class[]{PrivateKey.class}, recipientPrivateKey); + Class decapsulatorClass = Class.forName(KEM_DECAPSULATOR_CLASS); + int encapsulationSize = (int) invoke(decapsulator, decapsulatorClass, "encapsulationSize", new Class[0]); + if (combinedCiphertext.length < encapsulationSize) { + throw new XMLEncryptionException("KeyDerivation.MissingParameters"); + } + byte[] c0 = Arrays.copyOfRange(combinedCiphertext, 0, encapsulationSize); + byte[] c1 = Arrays.copyOfRange(combinedCiphertext, encapsulationSize, combinedCiphertext.length); + SecretKey sharedSecret = (SecretKey) invoke(decapsulator, decapsulatorClass, "decapsulate", + new Class[]{byte[].class}, (Object) c0); + byte[] kek = deriveKeyEncryptionKey(sharedSecret.getEncoded(), keyDerivationParameter); + return new KemDecapsulation(new SecretKeySpec(kek, "AES"), c1); + } catch (ReflectiveOperationException e) { + throw new XMLEncryptionException(e); + } catch (XMLSecurityException e) { + throw new XMLEncryptionException(e); + } + } + + private static Object kemGetInstance(String jceKemName) throws ReflectiveOperationException { + Class kemClass = Class.forName(KEM_CLASS); + Method getInstance = kemClass.getMethod("getInstance", String.class); + return getInstance.invoke(null, jceKemName); + } + + private static Object invoke(Object target, Class declaringClass, String methodName, Class[] paramTypes, + Object... args) throws ReflectiveOperationException { + Method method = declaringClass.getMethod(methodName, paramTypes); + return method.invoke(target, args); + } } diff --git a/src/main/resources/bindings/schemas/xenc-schema-11.xsd b/src/main/resources/bindings/schemas/xenc-schema-11.xsd index 0550c3b25..27264deb9 100644 --- a/src/main/resources/bindings/schemas/xenc-schema-11.xsd +++ b/src/main/resources/bindings/schemas/xenc-schema-11.xsd @@ -55,7 +55,7 @@ - + diff --git a/src/main/resources/bindings/schemas/xenc-schema.xsd b/src/main/resources/bindings/schemas/xenc-schema.xsd index d8ea060f2..3db7f443b 100644 --- a/src/main/resources/bindings/schemas/xenc-schema.xsd +++ b/src/main/resources/bindings/schemas/xenc-schema.xsd @@ -30,7 +30,7 @@ - + diff --git a/src/main/resources/security-config.xml b/src/main/resources/security-config.xml index f6c91db07..921a6d421 100644 --- a/src/main/resources/security-config.xml +++ b/src/main/resources/security-config.xml @@ -321,6 +321,28 @@ RequiredKey="EC" JCEName="RIPEMD160withECDSA"/> + + + + + + + + + + + + + + + + + Key pairs are generated on the fly in {@code @BeforeAll}; no pre-generated + * key material is committed to the repository. + * + *

Run with the Maven {@code bouncycastle} profile on Java 21+ (the {@code javax.crypto.KEM} + * API, JEP 452, is used internally via reflection - see {@link KeyUtils#kemEncapsulate}): + *

mvn test -Dtest=XMLEncryptionMLKEMTest -P bouncycastle
+ */ +class XMLEncryptionMLKEMTest { + + private static boolean mlKemAvailable; + private static boolean bcAddedForTheTest; + + private static java.util.Map keyPairs = new java.util.HashMap<>(); + + @BeforeAll + static void setUp() { + org.apache.xml.security.Init.init(); + + if (Security.getProvider("BC") == null) { + try { + Class cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider bc = (Provider) cls.getConstructor().newInstance(); + Security.addProvider(bc); + bcAddedForTheTest = true; + } catch (ReflectiveOperationException e) { + mlKemAvailable = false; + return; + } + } + + try { + for (String alg : new String[]{"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg, "BC"); + keyPairs.put(alg, kpg.generateKeyPair()); + } + // javax.crypto.KEM (JEP 452) is only available since Java 21; the KemEncapsulation + // helper is used via reflection, so probe it here rather than failing deep inside + // encryptKey. + Class.forName("javax.crypto.KEM"); + mlKemAvailable = true; + } catch (Exception | LinkageError e) { + mlKemAvailable = false; + } + } + + @AfterAll + static void tearDown() { + if (bcAddedForTheTest) { + Security.removeProvider("BC"); + } + } + + @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 testMLKEMEncryptDecrypt(String keyEncapsulationUri, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)"); + + PublicKey pubKey = keyPairs.get(jcaAlgorithm).getPublic(); + PrivateKey privKey = keyPairs.get(jcaAlgorithm).getPrivate(); + + // Build a minimal XML document to encrypt + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + Document doc = dbf.newDocumentBuilder().newDocument(); + Element root = doc.createElement("PaymentInfo"); + root.setTextContent("CardNumber:4019111111111111"); + doc.appendChild(root); + + // Generate a random AES-256 content-encryption key (CEK) + KeyGenerator kg = KeyGenerator.getInstance("AES"); + kg.init(256); + SecretKey cek = kg.generateKey(); + + // --- ENCRYPT --- + // Encapsulate a shared secret to the recipient's ML-KEM public key, derive an + // AES-256 key-wrap key from it via HKDF-SHA256, and wrap the CEK with that key. + 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, pubKey); + EncryptedKey encryptedKey = keyCipher.encryptKey(doc, cek, keyEncapsulationParameters, null); + + // Verify the produced EncryptedKey uses the Generic Hybrid Cipher structure, not an + // opaque flat key-transport blob + assertEquals(EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID, + encryptedKey.getEncryptionMethod().getAlgorithm()); + assertEquals(keyEncapsulationUri, encryptedKey.getEncryptionMethod().getKeyEncapsulationAlgorithm()); + assertEquals(kwAlgorithm, encryptedKey.getEncryptionMethod().getDataEncapsulationAlgorithm()); + assertTrue(encryptedKey.getEncryptionMethod().getKeyEncapsulationKeyLength() > 0); + + // Encrypt the document content with AES-256-GCM + 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); + + // Serialise to bytes to simulate wire transfer + java.io.ByteArrayOutputStream bos = new java.io.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)); + + // Verify the serialised XML carries the spec's element names, per + // https://www.w3.org/TR/xmlsec-generic-hybrid/ section 6.1 "Key Transport Example" + String serialized = bos.toString(java.nio.charset.StandardCharsets.UTF_8); + assertTrue(serialized.contains("GenericHybridCipherMethod"), "Missing GenericHybridCipherMethod element"); + assertTrue(serialized.contains("KeyEncapsulationMethod"), "Missing KeyEncapsulationMethod element"); + assertTrue(serialized.contains("DataEncapsulationMethod"), "Missing DataEncapsulationMethod element"); + assertTrue(serialized.contains("http://www.w3.org/2010/xmlsec-ghc#generic-hybrid"), + "Missing Generic Hybrid Cipher EncryptionMethod algorithm"); + + // --- DECRYPT --- + Document encDoc = dbf.newDocumentBuilder() + .parse(new java.io.ByteArrayInputStream(bos.toByteArray())); + + 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); + + // Unwrap the CEK using the recipient's ML-KEM private key + EncryptedKey ek = encData.getKeyInfo().itemEncryptedKey(0); + XMLCipher unwrapCipher = XMLCipher.getInstance(); + unwrapCipher.init(XMLCipher.UNWRAP_MODE, privKey); + Key recoveredCek = unwrapCipher.decryptKey( + ek, encData.getEncryptionMethod().getAlgorithm()); + + // Decrypt document content + decryptCipher.init(XMLCipher.DECRYPT_MODE, recoveredCek); + Document decryptedDoc = decryptCipher.doFinal(encDoc, encDataElem); + + Element decryptedRoot = decryptedDoc.getDocumentElement(); + assertEquals("PaymentInfo", decryptedRoot.getLocalName()); + assertEquals("CardNumber:4019111111111111", decryptedRoot.getTextContent()); + } +} diff --git a/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java new file mode 100644 index 000000000..5db2e4a1d --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java @@ -0,0 +1,117 @@ +/** + * 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.xml.security.test.javax.xml.crypto.dsig; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.Provider; +import java.security.Security; + +import org.apache.xml.security.signature.XMLSignature; +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; + +/** + * Tests for ML-DSA (FIPS 204) XML digital signatures via the + * {@code javax.xml.crypto.dsig.XMLSignatureFactory} DOM API. + * + *

The key material is stored in {@code mldsa.p12} (PKCS12, password "security"), + * pre-generated with BouncyCastle 1.81+ and committed as a test resource. + * The test requires BouncyCastle on the runtime classpath to supply the ML-DSA + * JCA provider; compile-time BC classes are deliberately avoided so the default + * build (without {@code -P bouncycastle}) still compiles cleanly. + * + *

Run with the Maven {@code bouncycastle} profile: + *

mvn test -Dtest=XMLSignatureMLDSATest -P bouncycastle
+ */ +class XMLSignatureMLDSATest extends XMLSignatureAbstract { + + static final String MLDSA_KS = + "src/test/resources/org/apache/xml/security/samples/input/mldsa.p12"; + static final String MLDSA_KS_PASSWORD = "security"; + static final String MLDSA_KS_TYPE = "PKCS12"; + + private static boolean mlDsaAvailable; + private static boolean bcAddedForTheTest; + private static KeyStore keyStore; + + @BeforeAll + static void setUp() { + Security.insertProviderAt( + new org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI(), 1); + + if (Security.getProvider("BC") == null) { + try { + Class cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider bc = (Provider) cls.getConstructor().newInstance(); + Security.addProvider(bc); + bcAddedForTheTest = true; + } catch (ReflectiveOperationException e) { + mlDsaAvailable = false; + return; + } + } + + try { + keyStore = KeyStore.getInstance(MLDSA_KS_TYPE); + keyStore.load(Files.newInputStream(Path.of(MLDSA_KS)), + MLDSA_KS_PASSWORD.toCharArray()); + // probe that ML-DSA is actually supported by the loaded provider + keyStore.getKey("ml-dsa-65", MLDSA_KS_PASSWORD.toCharArray()); + mlDsaAvailable = true; + } catch (Exception e) { + mlDsaAvailable = false; + } + } + + @AfterAll + static void tearDown() { + if (bcAddedForTheTest) { + Security.removeProvider("BC"); + } + } + + @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 testMLDSASignAndVerify(String signatureAlgorithmURI, String alias) throws Exception { + Assumptions.assumeTrue(mlDsaAvailable, "ML-DSA requires BouncyCastle 1.81+"); + byte[] signedXml = doSignWithJcpApi(signatureAlgorithmURI, alias, false); + Assertions.assertNotNull(signedXml); + assertValidSignatureWithJcpApi(signedXml, false); + } + + @Override + KeyStore getKeyStore() { + return keyStore; + } + + @Override + char[] getKeyPassword() { + return MLDSA_KS_PASSWORD.toCharArray(); + } +} diff --git a/src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java b/src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java new file mode 100644 index 000000000..783943f9c --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java @@ -0,0 +1,273 @@ +/** + * 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.xml.security.test.stax.encryption; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Provider; +import java.security.Security; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.xml.security.encryption.EncryptedData; +import org.apache.xml.security.encryption.EncryptedKey; +import org.apache.xml.security.encryption.XMLCipher; +import org.apache.xml.security.keys.KeyInfo; +import org.apache.xml.security.stax.ext.InboundXMLSec; +import org.apache.xml.security.stax.ext.OutboundXMLSec; +import org.apache.xml.security.stax.ext.SecurePart; +import org.apache.xml.security.stax.ext.XMLSec; +import org.apache.xml.security.stax.ext.XMLSecurityConstants; +import org.apache.xml.security.stax.ext.XMLSecurityProperties; +import org.apache.xml.security.test.stax.utils.StAX2DOM; +import org.apache.xml.security.test.stax.utils.XMLSecEventAllocator; +import org.apache.xml.security.test.stax.utils.XmlReaderToWriter; +import org.apache.xml.security.utils.EncryptionConstants; +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.assertTrue; + +/** + * StAX-path tests for ML-KEM key transport with AES-256-GCM content encryption, using the W3C + * "XML Security: Generic Hybrid Cipher" key transport structure + * (https://www.w3.org/TR/xmlsec-generic-hybrid/, see SANTUARIO-633) - the same structure exercised + * by the DOM {@code XMLCipher} API in {@code XMLEncryptionMLKEMTest}. + */ +class StaxMLKEMEncryptionTest { + + private static boolean mlKemAvailable; + private static boolean bcAddedForTheTest; + private static final Map keyPairs = new HashMap<>(); + private final XMLInputFactory xmlInputFactory; + + @BeforeAll + static void setUp() { + org.apache.xml.security.Init.init(); + if (Security.getProvider("BC") == null) { + try { + Class cls = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + Provider bc = (Provider) cls.getConstructor().newInstance(); + Security.insertProviderAt(bc, 2); + bcAddedForTheTest = true; + } catch (ReflectiveOperationException e) { + mlKemAvailable = false; + return; + } + } + try { + for (String alg : new String[]{"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024"}) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg, "BC"); + keyPairs.put(alg, kpg.generateKeyPair()); + } + // javax.crypto.KEM (JEP 452) is only available since Java 21 + Class.forName("javax.crypto.KEM"); + mlKemAvailable = true; + } catch (Exception | LinkageError e) { + mlKemAvailable = false; + } + } + + @AfterAll + static void cleanup() { + if (bcAddedForTheTest) { + Security.removeProvider("BC"); + } + } + + public StaxMLKEMEncryptionTest() throws Exception { + org.apache.xml.security.Init.init(); + xmlInputFactory = XMLInputFactory.newInstance(); + xmlInputFactory.setEventAllocator(new XMLSecEventAllocator()); + } + + @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 testMLKEMEncryptDecrypt(String keyEncapsulationUri, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)"); + + XMLSecurityProperties properties = new XMLSecurityProperties(); + List actions = new ArrayList<>(); + actions.add(XMLSecurityConstants.ENCRYPTION); + properties.setActions(actions); + + KeyGenerator keygen = KeyGenerator.getInstance("AES"); + keygen.init(256); + SecretKey cek = keygen.generateKey(); + properties.setEncryptionKey(cek); + properties.setEncryptionSymAlgorithm("http://www.w3.org/2009/xmlenc11#aes256-gcm"); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + properties.setEncryptionKeyTransportAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID); + properties.setEncryptionKeyEncapsulationAlgorithm(keyEncapsulationUri); + properties.setEncryptionDataEncapsulationAlgorithm(EncryptionConstants.ALGO_ID_KEYWRAP_AES256); + properties.setEncryptionTransportKey(kp.getPublic()); + + SecurePart securePart = new SecurePart( + new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Element); + properties.addEncryptionPart(securePart); + + byte[] output = process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", properties); + + // Verify the produced XML carries the spec's element names, per + // https://www.w3.org/TR/xmlsec-generic-hybrid/ section 6.1 "Key Transport Example" + String serialized = new String(output, StandardCharsets.UTF_8); + assertTrue(serialized.contains("GenericHybridCipherMethod"), "Missing GenericHybridCipherMethod element"); + assertTrue(serialized.contains("KeyEncapsulationMethod"), "Missing KeyEncapsulationMethod element"); + assertTrue(serialized.contains("DataEncapsulationMethod"), "Missing DataEncapsulationMethod element"); + assertTrue(serialized.contains("http://www.w3.org/2010/xmlsec-ghc#generic-hybrid"), + "Missing Generic Hybrid Cipher EncryptionMethod algorithm"); + + Document document; + try (InputStream is = new ByteArrayInputStream(output)) { + document = XMLUtils.read(is, false); + } + + NodeList nodeList = document.getElementsByTagNameNS("urn:example:po", "PaymentInfo"); + assertEquals(0, nodeList.getLength()); + + nodeList = document.getElementsByTagNameNS("urn:example:po", "CreditCard"); + assertEquals(0, nodeList.getLength()); + + nodeList = document.getElementsByTagNameNS( + XMLSecurityConstants.TAG_xenc_EncryptedData.getNamespaceURI(), + XMLSecurityConstants.TAG_xenc_EncryptedData.getLocalPart() + ); + assertEquals(1, nodeList.getLength()); + + Document decrypted = decryptUsingDOM(document, kp.getPrivate()); + + nodeList = decrypted.getElementsByTagNameNS("urn:example:po", "CreditCard"); + assertEquals(1, nodeList.getLength()); + } + + @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 testMLKEMStaxEncryptStaxDecrypt(String keyEncapsulationUri, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(mlKemAvailable, "ML-KEM requires BouncyCastle 1.84+ and Java 21+ (javax.crypto.KEM)"); + + XMLSecurityProperties encryptProperties = new XMLSecurityProperties(); + List actions = new ArrayList<>(); + actions.add(XMLSecurityConstants.ENCRYPTION); + encryptProperties.setActions(actions); + + KeyGenerator keygen = KeyGenerator.getInstance("AES"); + keygen.init(256); + SecretKey cek = keygen.generateKey(); + encryptProperties.setEncryptionKey(cek); + encryptProperties.setEncryptionSymAlgorithm("http://www.w3.org/2009/xmlenc11#aes256-gcm"); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + encryptProperties.setEncryptionKeyTransportAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID); + encryptProperties.setEncryptionKeyEncapsulationAlgorithm(keyEncapsulationUri); + encryptProperties.setEncryptionDataEncapsulationAlgorithm(EncryptionConstants.ALGO_ID_KEYWRAP_AES256); + encryptProperties.setEncryptionTransportKey(kp.getPublic()); + + SecurePart securePart = new SecurePart( + new QName("urn:example:po", "PaymentInfo"), SecurePart.Modifier.Element); + encryptProperties.addEncryptionPart(securePart); + + byte[] encrypted = process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", encryptProperties); + + XMLSecurityProperties decryptProperties = new XMLSecurityProperties(); + decryptProperties.setDecryptionKey(kp.getPrivate()); + InboundXMLSec inboundXMLSec = XMLSec.getInboundWSSec(decryptProperties); + XMLStreamReader xmlStreamReader = + xmlInputFactory.createXMLStreamReader(new ByteArrayInputStream(encrypted)); + XMLStreamReader securityStreamReader = inboundXMLSec.processInMessage(xmlStreamReader, null, null); + + Document decrypted = StAX2DOM.readDoc(securityStreamReader); + + NodeList nodeList = decrypted.getElementsByTagNameNS("urn:example:po", "CreditCard"); + assertEquals(1, nodeList.getLength()); + } + + private byte[] process(String inputXmlFile, XMLSecurityProperties properties) throws Exception { + OutboundXMLSec outboundXMLSec = XMLSec.getOutboundXMLSec(properties); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLStreamWriter xmlStreamWriter = outboundXMLSec.processOutMessage(baos, StandardCharsets.UTF_8.name()); + try (InputStream sourceDocument = this.getClass().getClassLoader().getResourceAsStream(inputXmlFile)) { + XMLStreamReader xmlStreamReader = null; + try { + xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument); + XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter); + return baos.toByteArray(); + } finally { + if (xmlStreamReader != null) { + xmlStreamReader.close(); + } + } + } finally { + xmlStreamWriter.close(); + } + } + + private Document decryptUsingDOM(Document document, Key privateKey) throws Exception { + NodeList nodeList = document.getElementsByTagNameNS( + XMLSecurityConstants.TAG_xenc_EncryptedData.getNamespaceURI(), + XMLSecurityConstants.TAG_xenc_EncryptedData.getLocalPart() + ); + Element ee = (Element) nodeList.item(0); + + XMLCipher cipher = XMLCipher.getInstance(); + cipher.init(XMLCipher.DECRYPT_MODE, null); + EncryptedData encryptedData = cipher.loadEncryptedData(document, ee); + + XMLCipher kwCipher = XMLCipher.getInstance(); + kwCipher.init(XMLCipher.UNWRAP_MODE, privateKey); + KeyInfo ki = encryptedData.getKeyInfo(); + EncryptedKey encryptedKey = ki.itemEncryptedKey(0); + Key symmetricKey = kwCipher.decryptKey( + encryptedKey, encryptedData.getEncryptionMethod().getAlgorithm() + ); + + cipher.init(XMLCipher.DECRYPT_MODE, symmetricKey); + return cipher.doFinal(document, ee); + } +} diff --git a/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java new file mode 100644 index 000000000..4798f7126 --- /dev/null +++ b/src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java @@ -0,0 +1,102 @@ +/** + * 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.xml.security.test.stax.signature; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +import org.apache.xml.security.stax.ext.SecurePart; +import org.apache.xml.security.stax.ext.XMLSecurityConstants; +import org.apache.xml.security.stax.ext.XMLSecurityProperties; +import org.apache.xml.security.stax.securityToken.SecurityTokenConstants; +import org.apache.xml.security.utils.XMLUtils; +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; + +/** + * StAX-path tests for ML-DSA (FIPS 204) XML digital signatures. + */ +class StaxMLDSASignatureTest extends AbstractSignatureCreationTest { + + private static final Map keyPairs = new HashMap<>(); + + @BeforeAll + static void generateKeys() throws Exception { + if (!isBcInstalled()) { + return; + } + try { + for (String alg : new String[]{"ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(alg, "BC"); + keyPairs.put(alg, kpg.generateKeyPair()); + } + } catch (Exception e) { + // ML-DSA not available with this BC version + } + } + + @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 testMLDSASign(String sigAlgorithm, String jcaAlgorithm) throws Exception { + Assumptions.assumeTrue(isBcInstalled() && keyPairs.containsKey(jcaAlgorithm), + "ML-DSA requires BouncyCastle 1.81+"); + + XMLSecurityProperties properties = new XMLSecurityProperties(); + List actions = new ArrayList<>(); + actions.add(XMLSecurityConstants.SIGNATURE); + properties.setActions(actions); + properties.setSignatureKeyIdentifier(SecurityTokenConstants.KeyIdentifier_KeyValue); + properties.setSignatureAlgorithm(sigAlgorithm); + + KeyPair kp = keyPairs.get(jcaAlgorithm); + properties.setSignatureKey(kp.getPrivate()); + properties.setSignatureVerificationKey(kp.getPublic()); + + SecurePart securePart = new SecurePart( + new QName("urn:example:po", "PaymentInfo"), + SecurePart.Modifier.Content, + new String[]{"http://www.w3.org/2001/10/xml-exc-c14n#"}, + "http://www.w3.org/2001/04/xmlenc#sha256"); + properties.addSignaturePart(securePart); + + byte[] output = process("ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml", properties, null); + + Document document; + try (InputStream is = new ByteArrayInputStream(output)) { + document = XMLUtils.read(is, false); + } + + verifyUsingDOM(document, kp.getPublic(), properties.getSignatureSecureParts()); + } +} diff --git a/src/test/resources/org/apache/xml/security/samples/input/mldsa.p12 b/src/test/resources/org/apache/xml/security/samples/input/mldsa.p12 new file mode 100644 index 000000000..3c13c9b21 Binary files /dev/null and b/src/test/resources/org/apache/xml/security/samples/input/mldsa.p12 differ