From 0d52db80a7a48674bed02e160a896982c0946155 Mon Sep 17 00:00:00 2001 From: Freeman Fang Date: Thu, 6 Aug 2026 16:35:12 -0400 Subject: [PATCH] Add PQC (ML-DSA and ML-KEM) support for XML Digital Signature and Encryption - ML-DSA (FIPS 204): signature algorithm constants for ML-DSA-44/65/87, JCA mappings and whitelist URIs in XMLSignature, DOM and JSR-105 support. - ML-KEM (FIPS 203): key transport support using the W3C "XML Security: Generic Hybrid Cipher" structure (https://www.w3.org/TR/xmlsec-generic-hybrid/, closing SANTUARIO-633): KeyUtils.kemEncapsulate/kemDecapsulate perform real javax.crypto.KEM (JEP 452) encapsulation/decapsulation plus HKDF key derivation, with the AES-KeyWrap output and KEM encapsulation concatenated into CipherValue. New ALGO_ID_KEYTRANSPORT_GENERIC_HYBRID top-level algorithm and GenericHybridCipherMethod/KeyEncapsulationMethod/ DataEncapsulationMethod XML structure, wired into both the DOM XMLCipher and STAX (XMLEncryptOutputProcessor/XMLEncryptedKeyInputHandler) paths. - Both features require BouncyCastle 1.84+ and skip gracefully without it; ML-KEM additionally requires Java 21+ for javax.crypto.KEM, accessed via reflection so the module still compiles under its Java 11 target. Algorithm URIs follow the RFC 9231 provisional naming pattern under http://www.w3.org/tbd# per draft-eastlake-rfc9231bis-xmlsec-uris, pending final URI assignment. --- pom.xml | 6 + .../dom/AbstractDOMSignatureMethod.java | 2 +- .../dsig/internal/dom/DOMSignatureMethod.java | 99 ++++++ .../internal/dom/DOMXMLSignatureFactory.java | 8 +- .../xml/security/algorithms/JCEMapper.java | 29 ++ .../algorithms/SignatureAlgorithm.java | 10 + .../implementations/SignatureMLDSA.java | 208 +++++++++++++ .../security/encryption/EncryptionMethod.java | 65 ++++ .../xml/security/encryption/XMLCipher.java | 283 +++++++++++++++++- .../security/encryption/XMLCipherUtil.java | 50 ++++ .../params/KeyEncapsulationParameters.java | 69 +++++ .../keys/content/DEREncodedKeyValue.java | 2 + .../xml/security/signature/XMLSignature.java | 12 + .../stax/ext/XMLSecurityConstants.java | 19 ++ .../stax/ext/XMLSecurityProperties.java | 45 +++ .../algorithms/PKISignatureAlgorithm.java | 4 +- .../input/XMLEncryptedKeyInputHandler.java | 122 +++++++- .../output/XMLEncryptOutputProcessor.java | 134 ++++++++- .../AbstractInboundSecurityToken.java | 10 +- .../security/utils/EncryptionConstants.java | 49 +++ .../apache/xml/security/utils/KeyUtils.java | 148 +++++++++ .../bindings/schemas/xenc-schema-11.xsd | 2 +- .../bindings/schemas/xenc-schema.xsd | 2 +- src/main/resources/security-config.xml | 56 ++++ .../encryption/XMLEncryptionMLKEMTest.java | 216 +++++++++++++ .../crypto/dsig/XMLSignatureMLDSATest.java | 117 ++++++++ .../encryption/StaxMLKEMEncryptionTest.java | 273 +++++++++++++++++ .../signature/StaxMLDSASignatureTest.java | 102 +++++++ .../xml/security/samples/input/mldsa.p12 | Bin 0 -> 29805 bytes 29 files changed, 2113 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/apache/xml/security/algorithms/implementations/SignatureMLDSA.java create mode 100644 src/main/java/org/apache/xml/security/encryption/params/KeyEncapsulationParameters.java create mode 100644 src/test/java/org/apache/xml/security/test/dom/encryption/XMLEncryptionMLKEMTest.java create mode 100644 src/test/java/org/apache/xml/security/test/javax/xml/crypto/dsig/XMLSignatureMLDSATest.java create mode 100644 src/test/java/org/apache/xml/security/test/stax/encryption/StaxMLKEMEncryptionTest.java create mode 100644 src/test/java/org/apache/xml/security/test/stax/signature/StaxMLDSASignatureTest.java create mode 100644 src/test/resources/org/apache/xml/security/samples/input/mldsa.p12 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 0000000000000000000000000000000000000000..3c13c9b21c6d362f6008c16288e0a73f4b2e5945 GIT binary patch literal 29805 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 Mw|W8t0gwOy0GcmDH~;_u literal 0 HcmV?d00001