8241306: Add SignatureMethodParameterSpec subclass for RSASSA-PSS params

Reviewed-by: mullan
This commit is contained in:
Weijun Wang 2021-04-19 14:29:18 +00:00
parent b14e0ee4d8
commit 8dbf7aa1f9
11 changed files with 1210 additions and 161 deletions

View file

@ -24,7 +24,7 @@
/**
* @test
* @bug 4635230 6283345 6303830 6824440 6867348 7094155 8038184 8038349 8046949
* 8046724 8079693 8177334 8205507 8210736 8217878
* 8046724 8079693 8177334 8205507 8210736 8217878 8241306
* @summary Basic unit tests for generating XML Signatures with JSR 105
* @modules java.base/sun.security.util
* java.base/sun.security.x509
@ -108,7 +108,7 @@ public class GenerationTests {
rsaSha1, rsaSha224, rsaSha256, rsaSha384, rsaSha512,
ecdsaSha1, ecdsaSha224, ecdsaSha256, ecdsaSha384, ecdsaSha512,
hmacSha1, hmacSha224, hmacSha256, hmacSha384, hmacSha512,
rsaSha1mgf1, rsaSha224mgf1, rsaSha256mgf1, rsaSha384mgf1, rsaSha512mgf1;
rsaSha1mgf1, rsaSha224mgf1, rsaSha256mgf1, rsaSha384mgf1, rsaSha512mgf1, rsaShaPSS;
private static DigestMethod sha1, sha224, sha256, sha384, sha512,
sha3_224, sha3_256, sha3_384, sha3_512;
private static KeyInfo dsa1024, dsa2048, rsa, rsa1024, rsa2048,
@ -214,7 +214,8 @@ public class GenerationTests {
SignatureMethod.RSA_SHA256,
SignatureMethod.ECDSA_SHA256,
SignatureMethod.HMAC_SHA256,
SignatureMethod.SHA256_RSA_MGF1);
SignatureMethod.SHA256_RSA_MGF1,
SignatureMethod.RSA_PSS);
private static final String[] allSignatureMethods
= Stream.of(SignatureMethod.class.getDeclaredFields())
@ -246,9 +247,9 @@ public class GenerationTests {
})
.toArray(String[]::new);
// As of JDK 11, the number of defined algorithms are...
// As of JDK 17, the number of defined algorithms are...
static {
if (allSignatureMethods.length != 22
if (allSignatureMethods.length != 23
|| allDigestMethods.length != 9) {
System.out.println(Arrays.toString(allSignatureMethods));
System.out.println(Arrays.toString(allDigestMethods));
@ -335,6 +336,7 @@ public class GenerationTests {
test_create_signature_enveloping_sha512_rsa_sha256_mgf1();
test_create_signature_enveloping_sha512_rsa_sha384_mgf1();
test_create_signature_enveloping_sha512_rsa_sha512_mgf1();
test_create_signature_enveloping_sha512_rsa_pss();
test_create_signature_reference_dependency();
test_create_signature_with_attr_in_no_namespace();
test_create_signature_with_empty_id();
@ -531,6 +533,7 @@ public class GenerationTests {
rsaSha256mgf1 = fac.newSignatureMethod(SignatureMethod.SHA256_RSA_MGF1, null);
rsaSha384mgf1 = fac.newSignatureMethod(SignatureMethod.SHA384_RSA_MGF1, null);
rsaSha512mgf1 = fac.newSignatureMethod(SignatureMethod.SHA512_RSA_MGF1, null);
rsaShaPSS = fac.newSignatureMethod(SignatureMethod. RSA_PSS, null);
ecdsaSha1 = fac.newSignatureMethod(SignatureMethod.ECDSA_SHA1, null);
ecdsaSha224 = fac.newSignatureMethod(SignatureMethod.ECDSA_SHA224, null);
@ -792,6 +795,14 @@ public class GenerationTests {
System.out.println();
}
static void test_create_signature_enveloping_sha512_rsa_pss()
throws Exception {
System.out.println("* Generating signature-enveloping-sha512_rsa_pss.xml");
test_create_signature_enveloping(sha512, rsaShaPSS, rsa1024,
getPrivateKey("RSA", 1024), kvks, false, true);
System.out.println();
}
static void test_create_signature_enveloping_p256_sha1() throws Exception {
System.out.println("* Generating signature-enveloping-p256-sha1.xml");
test_create_signature_enveloping(sha1, ecdsaSha1, p256ki,

View file

@ -0,0 +1,217 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.Utils;
import jdk.test.lib.security.XMLUtils;
import org.w3c.dom.Document;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.spec.RSAPSSParameterSpec;
import java.security.KeyPairGenerator;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
/**
* @test
* @bug 8241306
* @library /test/lib
* @modules java.xml.crypto
* @summary Testing marshal and unmarshal of RSAPSSParameterSpec
*/
public class PSSSpec {
private static final String P2SM = "//ds:Signature/ds:SignedInfo/ds:SignatureMethod";
private static final String P2PSS = P2SM + "/pss:RSAPSSParams";
private static final String P2MGF = P2PSS + "/pss:MaskGenerationFunction";
private static final PSSParameterSpec DEFAULT_SPEC
= new PSSParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), 32, PSSParameterSpec.TRAILER_FIELD_BC);
public static void main(String[] args) throws Exception {
unmarshal();
marshal();
spec();
}
static void unmarshal() throws Exception {
// Original document with all elements
Document doc = XMLUtils.string2doc("""
<?xml version="1.0" encoding="UTF-8"?>
<ds:Signature
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"
xmlns:pss="http://www.w3.org/2007/05/xmldsig-more#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#WithComments"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2007/05/xmldsig-more#rsa-pss">
<pss:RSAPSSParams>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
<pss:MaskGenerationFunction Algorithm="http://www.w3.org/2007/05/xmldsig-more#MGF1">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>
</pss:MaskGenerationFunction>
<pss:SaltLength>32</pss:SaltLength>
<pss:TrailerField>2</pss:TrailerField>
</pss:RSAPSSParams>
</ds:SignatureMethod>
<ds:Reference>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
<ds:DigestValue>abc=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>abc=</ds:SignatureValue>
</ds:Signature>
""");
// Unknown DigestMethod
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withAttribute(doc, P2PSS + "/ds:DigestMethod", "Algorithm", "http://unknown")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Invalid digest algorithm"), e.getMessage()));
// Unknown MGF algorithm
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withAttribute(doc, P2MGF, "Algorithm", "http://unknown")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Unknown MGF algorithm"), e.getMessage()));
// Unknown MGF DigestMethod
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withAttribute(doc, P2MGF + "/ds:DigestMethod", "Algorithm", "http://unknown")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Invalid digest algorithm"), e.getMessage()));
// Invalid SaltLength
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withText(doc, P2PSS + "/pss:SaltLength", "big")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Invalid salt length supplied"), e.getMessage()));
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withText(doc, P2PSS + "/pss:SaltLength", "-1")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Invalid salt length supplied"), e.getMessage()));
// Invalid TrailerField
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withText(doc, P2PSS + "/pss:TrailerField", "small")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Invalid trailer field supplied"), e.getMessage()));
Utils.runAndCheckException(
() -> getSpec(XMLUtils.withText(doc, P2PSS + "/pss:TrailerField", "-1")),
e -> Asserts.assertTrue(e instanceof MarshalException && e.getMessage().contains("Invalid trailer field supplied"), e.getMessage()));
// Spec in original doc
checkSpec(doc, new PSSParameterSpec("SHA-512", "MGF1", new MGF1ParameterSpec("SHA-384"), 32, 2));
// Default MGF1 dm is same as PSS dm
checkSpec(XMLUtils.withoutNode(doc, P2MGF + "/ds:DigestMethod"), // No dm in MGF
new PSSParameterSpec("SHA-512", "MGF1", new MGF1ParameterSpec("SHA-512"), 32, 2));
checkSpec(XMLUtils.withoutNode(doc, P2MGF), // No MGF at all
new PSSParameterSpec("SHA-512", "MGF1", new MGF1ParameterSpec("SHA-512"), 32, 2));
// Default TrailerField is 1
checkSpec(XMLUtils.withoutNode(doc, P2PSS + "/pss:TrailerField"),
new PSSParameterSpec("SHA-512", "MGF1", new MGF1ParameterSpec("SHA-384"), 32, 1));
// Default SaltLength is dm's SaltLength
checkSpec(XMLUtils.withoutNode(doc, P2PSS + "/pss:SaltLength"),
new PSSParameterSpec("SHA-512", "MGF1", new MGF1ParameterSpec("SHA-384"), 64, 2));
// Default DigestMethod is 256
checkSpec(XMLUtils.withoutNode(doc, P2PSS + "/ds:DigestMethod"),
new PSSParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-384"), 32, 2));
// Default PSS is SHA-256
checkSpec(XMLUtils.withoutNode(doc, P2PSS), DEFAULT_SPEC);
}
static void marshal() throws Exception {
var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
var signer = XMLUtils.signer(keyPairGenerator.generateKeyPair().getPrivate());
PSSParameterSpec spec;
Document doc = XMLUtils.string2doc("<a>x</a>");
Document signedDoc;
// Default sm. No need to describe at all
signer.sm(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(DEFAULT_SPEC));
signedDoc = signer.sign(doc);
Asserts.assertTrue(!XMLUtils.sub(signedDoc, P2SM).hasChildNodes());
// Special salt.
spec = new PSSParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), 40, PSSParameterSpec.TRAILER_FIELD_BC);
signer.sm(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(spec));
signedDoc = signer.sign(doc);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/pss:SaltLength").getTextContent().equals("40"));
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2MGF) == null);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/ds:DigestMethod") == null);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/pss:TrailerField") == null);
// Different MGF1 dm
spec = new PSSParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-384"), 32, PSSParameterSpec.TRAILER_FIELD_BC);
signer.sm(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(spec));
signedDoc = signer.sign(doc);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2MGF + "/ds:DigestMethod").getAttribute("Algorithm").equals(DigestMethod.SHA384));
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/ds:DigestMethod") == null);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/pss:SaltLength") == null);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/pss:TrailerField") == null);
// Non default dm only
spec = new PSSParameterSpec("SHA-384", "MGF1", new MGF1ParameterSpec("SHA-384"), 48, PSSParameterSpec.TRAILER_FIELD_BC);
signer.sm(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(spec));
signedDoc = signer.sign(doc);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/ds:DigestMethod").getAttribute("Algorithm").equals(DigestMethod.SHA384));
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2MGF) == null);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/pss:SaltLength") == null);
Asserts.assertTrue(XMLUtils.sub(signedDoc, P2PSS + "/pss:TrailerField") == null);
}
static void spec() throws Exception {
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
SignatureMethod sm = fac.newSignatureMethod(SignatureMethod.RSA_PSS, null);
Asserts.assertTrue(equals(
((RSAPSSParameterSpec)sm.getParameterSpec()).getPSSParameterSpec(),
DEFAULT_SPEC));
PSSParameterSpec special = new PSSParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-384"), 33, 2);
sm = fac.newSignatureMethod(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(special));
Asserts.assertTrue(equals(
((RSAPSSParameterSpec)sm.getParameterSpec()).getPSSParameterSpec(),
special));
}
static PSSParameterSpec getSpec(Document doc) throws Exception {
var signatureNode = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature").item(0);
DOMValidateContext valContext = new DOMValidateContext(new SecretKeySpec(new byte[1], "WHAT"), signatureNode);
valContext.setProperty("org.jcp.xml.dsig.secureValidation", false);
var signedInfo = XMLSignatureFactory.getInstance("DOM").unmarshalXMLSignature(valContext).getSignedInfo();
var spec = signedInfo.getSignatureMethod().getParameterSpec();
if (spec instanceof RSAPSSParameterSpec pspec) {
return pspec.getPSSParameterSpec();
} else {
Asserts.fail("Not PSSParameterSpec: " + spec.getClass());
return null;
}
}
static void checkSpec(Document doc, PSSParameterSpec expected) throws Exception {
Asserts.assertTrue(equals(getSpec(doc), expected));
}
static boolean equals(PSSParameterSpec p1, PSSParameterSpec p2) {
return p1.getDigestAlgorithm().equals(p2.getDigestAlgorithm())
&& p1.getSaltLength() == p2.getSaltLength()
&& p1.getTrailerField() == p2.getTrailerField()
&& p1.getMGFAlgorithm().equals(p2.getMGFAlgorithm())
&& ((MGF1ParameterSpec) p1.getMGFParameters()).getDigestAlgorithm()
.equals(((MGF1ParameterSpec) p2.getMGFParameters()).getDigestAlgorithm());
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/**
* @test
* @bug 8241306
* @summary Tests for the jdk.xml.dsig.secureValidationPolicy security property
* on the RSASSA-PSS signature method
* @library /test/lib
* @modules java.base/sun.security.tools.keytool
* java.base/sun.security.x509
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.security.XMLUtils;
import jdk.test.lib.Utils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import sun.security.tools.keytool.CertAndKeyGen;
import sun.security.x509.X500Name;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.spec.RSAPSSParameterSpec;
import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.Iterator;
import java.util.Objects;
import static java.security.spec.PSSParameterSpec.TRAILER_FIELD_BC;
public class SecureValidation {
public static void main(String[] args) throws Exception {
Document doc = XMLUtils.string2doc("<a><b>Text</b>Raw</a>");
CertAndKeyGen g = new CertAndKeyGen("RSASSA-PSS", "RSASSA-PSS");
g.generate(2048);
X509Certificate cert = g.getSelfCertificate(new X500Name("CN=Me"), 100);
PrivateKey privateKey = g.getPrivateKey();
PSSParameterSpec pspec = new PSSParameterSpec("SHA-384", "MGF1",
MGF1ParameterSpec.SHA512, 48, TRAILER_FIELD_BC);
// Sign with PSS with SHA-384 and SHA-512
Document signed = XMLUtils.signer(privateKey, cert)
.dm(DigestMethod.SHA384)
.sm(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(pspec))
.sign(doc);
XPath xp = XPathFactory.newInstance().newXPath();
xp.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
return switch (prefix) {
case "ds" -> "http://www.w3.org/2000/09/xmldsig#";
case "pss" -> "http://www.w3.org/2007/05/xmldsig-more#";
default -> throw new IllegalArgumentException();
};
}
@Override
public String getPrefix(String namespaceURI) {
return null;
}
@Override
public Iterator<String> getPrefixes(String namespaceURI) {
return null;
}
});
var validator = XMLUtils.validator();
XMLUtils.addPolicy("disallowAlg " + DigestMethod.SHA256);
Element e;
// 1. Modify the MGF1 digest algorithm in PSSParams to SHA-256
e = (Element) xp.evaluate(
"/a/ds:Signature/ds:SignedInfo/ds:SignatureMethod" +
"/pss:RSAPSSParams/pss:MaskGenerationFunction/ds:DigestMethod",
signed, XPathConstants.NODE);
e.setAttribute("Algorithm", DigestMethod.SHA256);
// When secureValidation is true, validate() throws an exception
Utils.runAndCheckException(() -> validator.secureValidation(true).validate(signed),
t -> Asserts.assertTrue(t instanceof MarshalException
&& t.getMessage().contains("in MGF1")
&& t.getMessage().contains(DigestMethod.SHA256), Objects.toString(t)));
// When secureValidation is false, validate() returns false
Asserts.assertFalse(validator.secureValidation(false).validate(signed));
// Revert the change and confirm validate() returns true
e.setAttribute("Algorithm", DigestMethod.SHA512);
Asserts.assertTrue(validator.secureValidation(true).validate(signed));
// 2. Modify the digest algorithm in PSSParams to SHA-256
e = (Element) xp.evaluate(
"/a/ds:Signature/ds:SignedInfo/ds:SignatureMethod" +
"/pss:RSAPSSParams/ds:DigestMethod",
signed, XPathConstants.NODE);
e.setAttribute("Algorithm", DigestMethod.SHA256);
// When secureValidation is true, validate() throws an exception
Utils.runAndCheckException(() -> validator.secureValidation(true).validate(signed),
t -> Asserts.assertTrue(t instanceof MarshalException
&& t.getMessage().contains("in PSS")
&& t.getMessage().contains(DigestMethod.SHA256), Objects.toString(t)));
// When secureValidation is false, validate() returns false
Asserts.assertFalse(validator.secureValidation(false).validate(signed));
// 3. Modify the digest algorithm in PSSParams to SHA-512
e.setAttribute("Algorithm", DigestMethod.SHA512);
// No matter if secureValidation is true or false, validate()
// returns false. This means the policy allows it.
Asserts.assertFalse(validator.secureValidation(true).validate(signed));
Asserts.assertFalse(validator.secureValidation(false).validate(signed));
}
}

View file

@ -0,0 +1,565 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.test.lib.security;
import jdk.test.lib.Asserts;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.crypto.*;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.*;
import javax.xml.crypto.dsig.spec.*;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAKey;
import java.security.spec.PSSParameterSpec;
import java.util.*;
// A collection of test utility methods for parsing, validating and
// generating XML Signatures.
public class XMLUtils {
private static final XMLSignatureFactory FAC =
XMLSignatureFactory.getInstance("DOM");
//////////// MAIN as TEST ////////////
public static void main(String[] args) throws Exception {
var x = "<a><b>c</b>x</a>";
var p = Files.write(Path.of("x.xml"), List.of(x));
var b = Path.of("").toUri().toString();
var d = string2doc(x);
// keytool -keystore ks -keyalg ec -storepass changeit -genkeypair -alias a -dname CN=a
var pass = "changeit".toCharArray();
var ks = KeyStore.getInstance(new File("ks"), pass);
var c = (X509Certificate) ks.getCertificate("a");
var pr = (PrivateKey) ks.getKey("a", pass);
var pu = c.getPublicKey();
var s0 = signer(pr); // No KeyInfo
var s1 = signer(pr, pu); // KeyInfo is PublicKey
var s2 = signer(pr, c); // KeyInfo is X509Data
var s3 = signer(ks, "a", pass); // KeyInfo is KeyName
var v1 = validator(); // knows nothing
var v2 = validator(ks); // knows KeyName
Asserts.assertTrue(v1.validate(s0.sign(d), pu)); // need PublicKey
Asserts.assertTrue(v1.validate(s1.sign(d))); // can read KeyInfo
Asserts.assertTrue(v1.validate(s2.sign(d))); // can read KeyInfo
Asserts.assertTrue(v2.validate(s3.sign(d))); // can read KeyInfo
Asserts.assertTrue(v2.secureValidation(false).validate(s3.sign(p.toUri()))); // can read KeyInfo
Asserts.assertTrue(v2.secureValidation(false).baseURI(b).validate(
s3.sign(p.getParent().toUri(), p.getFileName().toUri()))); // can read KeyInfo
Asserts.assertTrue(v1.validate(s1.sign("text"))); // plain text
Asserts.assertTrue(v1.validate(s1.sign("binary".getBytes()))); // raw data
}
//////////// CONVERT ////////////
// Converts a Document object to string
public static String doc2string(Document doc) throws Exception {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
// Indentation would invalidate the signature
// transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
return writer.getBuffer().toString();
}
// Parses a string into a Document object
public static Document string2doc(String input) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
return factory.newDocumentBuilder().
parse(new InputSource(new StringReader(input)));
}
// Clones a document
public static Document clone(Document d) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document copiedDocument = db.newDocument();
Node copiedRoot = copiedDocument.importNode(d.getDocumentElement(), true);
copiedDocument.appendChild(copiedRoot);
return copiedDocument;
}
//////////// QUERY AND EDIT ////////////
// An XPath object that recognizes ds and pss names
public static final XPath XPATH;
static {
XPATH = XPathFactory.newInstance().newXPath();
XPATH.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
return switch (prefix) {
case "ds" -> "http://www.w3.org/2000/09/xmldsig#";
case "pss" -> "http://www.w3.org/2007/05/xmldsig-more#";
default -> throw new IllegalArgumentException();
};
}
@Override
public String getPrefix(String namespaceURI) {
return null;
}
@Override
public Iterator<String> getPrefixes(String namespaceURI) {
return null;
}
});
}
// Returns an Element inside a Document
public static Element sub(Document d, String path) throws Exception {
return (Element) XMLUtils.XPATH.evaluate(path, d, XPathConstants.NODE);
}
// Returns a new document with an attribute modified
public static Document withAttribute(Document d, String path, String attr, String value) throws Exception {
d = clone(d);
sub(d, path).setAttribute(attr, value);
return d;
}
// Returns a new document with a text modified
public static Document withText(Document d, String path, String value) throws Exception {
d = clone(d);
sub(d, path).setTextContent(value);
return d;
}
// Returns a new document without a child element
public static Document withoutNode(Document d, String... paths) throws Exception {
d = clone(d);
for (String path : paths) {
Element e = sub(d, path);
e.getParentNode().removeChild(e);
}
return d;
}
//////////// SIGN ////////////
// Creates a signer from a private key and a certificate
public static Signer signer(PrivateKey privateKey, X509Certificate cert)
throws Exception {
return signer(privateKey).cert(cert);
}
// Creates a signer from a private key and a public key
public static Signer signer(PrivateKey privateKey, PublicKey publicKey)
throws Exception {
return signer(privateKey).publicKey(publicKey);
}
// Creates a signer from a private key entry in a keystore. The KeyInfo
// inside the signature will only contain a KeyName to alias
public static Signer signer(KeyStore ks, String alias, char[] password)
throws Exception {
return signer((PrivateKey) ks.getKey(alias, password))
.keyName(alias);
}
// Creates a signer from a private key. There will be no KeyInfo inside
// the signature and must be validated with a given public key.
public static Signer signer(PrivateKey privateKey)
throws Exception {
return new Signer(privateKey);
}
public static class Signer {
PrivateKey privateKey; // signer key, never null
X509Certificate cert; // certificate, optional
PublicKey publicKey; // public key, optional
String keyName; // alias, optional
SignatureMethod sm; // default determined by privateKey
DigestMethod dm; // default SHA-256
CanonicalizationMethod cm; // default EXCLUSIVE
Transform tr; // default ENVELOPED
public Signer(PrivateKey privateKey) throws Exception {
this.privateKey = privateKey;
dm(DigestMethod.SHA256);
tr(Transform.ENVELOPED);
cm(CanonicalizationMethod.EXCLUSIVE);
String alg = privateKey.getAlgorithm();
if (alg.equals("RSASSA-PSS")) {
PSSParameterSpec pspec
= (PSSParameterSpec) ((RSAKey) privateKey).getParams();
if (pspec != null) {
sm(SignatureMethod.RSA_PSS, new RSAPSSParameterSpec(pspec));
} else {
sm(SignatureMethod.RSA_PSS);
}
} else {
sm(switch (privateKey.getAlgorithm()) {
case "RSA" -> SignatureMethod.RSA_SHA256;
case "DSA" -> SignatureMethod.DSA_SHA256;
case "EC" -> SignatureMethod.ECDSA_SHA256;
default -> throw new InvalidKeyException();
});
}
}
// Change KeyInfo source
public Signer cert(X509Certificate cert) {
this.cert = cert;
return this;
}
public Signer publicKey(PublicKey key) {
this.publicKey = key;
return this;
}
public Signer keyName(String n) {
keyName = n;
return this;
}
// Change various methods
public Signer tr(String transform) throws Exception {
TransformParameterSpec params = null;
switch (transform) {
case Transform.XPATH:
params = new XPathFilterParameterSpec("//.");
break;
case Transform.XPATH2:
params = new XPathFilter2ParameterSpec(
Collections.singletonList(new XPathType("//.",
XPathType.Filter.INTERSECT)));
break;
}
tr = FAC.newTransform(transform, params);
return this;
}
public Signer sm(String method) throws Exception {
sm = FAC.newSignatureMethod(method, null);
return this;
}
public Signer dm(String method) throws Exception {
dm = FAC.newDigestMethod(method, null);
return this;
}
public Signer cm(String method) throws Exception {
cm = FAC.newCanonicalizationMethod(method, (C14NMethodParameterSpec) null);
return this;
}
public Signer sm(String method, SignatureMethodParameterSpec spec)
throws Exception {
sm = FAC.newSignatureMethod(method, spec);
return this;
}
public Signer dm(String method, DigestMethodParameterSpec spec)
throws Exception {
dm = FAC.newDigestMethod(method, spec);
return this;
}
// Signs different sources
// Signs an XML file in detached mode
public Document sign(URI uri) throws Exception {
Document newDocument = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().newDocument();
FAC.newXMLSignature(buildSignedInfo(uri.toString()), buildKeyInfo()).sign(
new DOMSignContext(privateKey, newDocument));
return newDocument;
}
// Signs an XML file in a relative reference in detached mode
public Document sign(URI base, URI ref) throws Exception {
Document newDocument = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().newDocument();
DOMSignContext ctxt = new DOMSignContext(privateKey, newDocument);
ctxt.setBaseURI(base.toString());
FAC.newXMLSignature(buildSignedInfo(ref.toString()), buildKeyInfo()).sign(ctxt);
return newDocument;
}
// Signs a document in enveloped mode
public Document sign(Document document) throws Exception {
DOMResult result = new DOMResult();
TransformerFactory.newInstance().newTransformer()
.transform(new DOMSource(document), result);
Document newDocument = (Document) result.getNode();
FAC.newXMLSignature(buildSignedInfo(""), buildKeyInfo()).sign(
new DOMSignContext(privateKey, newDocument.getDocumentElement()));
return newDocument;
}
// Signs a document in enveloping mode
public Document signEnveloping(Document document) throws Exception {
Document newDocument = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().newDocument();
FAC.newXMLSignature(
buildSignedInfo(FAC.newReference("#object", dm)),
buildKeyInfo(),
List.of(FAC.newXMLObject(List.of(new DOMStructure(document.getDocumentElement())),
"object", null, null)),
null,
null)
.sign(new DOMSignContext(privateKey, newDocument));
return newDocument;
}
// Signs a raw byte array
public Document sign(byte[] data) throws Exception {
Document newDocument = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().newDocument();
FAC.newXMLSignature(
buildSignedInfo(FAC.newReference("#object", dm, List.of
(FAC.newTransform(Transform.BASE64,
(TransformParameterSpec) null)), null, null)),
buildKeyInfo(),
List.of(FAC.newXMLObject(List.of(new DOMStructure(
newDocument.createTextNode(Base64.getEncoder().encodeToString(data)))),
"object", null, null)),
null,
null)
.sign(new DOMSignContext(privateKey, newDocument));
return newDocument;
}
// Signs a plain string
public Document sign(String str) throws Exception {
Document newDocument = DocumentBuilderFactory.newInstance()
.newDocumentBuilder().newDocument();
FAC.newXMLSignature(
buildSignedInfo(FAC.newReference("#object", dm)),
buildKeyInfo(),
List.of(FAC.newXMLObject(List.of(new DOMStructure(newDocument.createTextNode(str))),
"object", null, null)),
null,
null)
.sign(new DOMSignContext(privateKey, newDocument));
return newDocument;
}
// Builds a SignedInfo for a string reference
private SignedInfo buildSignedInfo(String ref) {
return FAC.newSignedInfo(
cm,
sm,
List.of(FAC.newReference(
ref,
dm,
List.of(tr),
null, null)));
}
// Builds a SignedInfo for a Reference
private SignedInfo buildSignedInfo(Reference ref) {
return FAC.newSignedInfo(
cm,
sm,
List.of(ref));
}
// Builds a KeyInfo from different sources
private KeyInfo buildKeyInfo() throws Exception {
KeyInfoFactory keyInfoFactory = FAC.getKeyInfoFactory();
if (cert != null) {
return keyInfoFactory.newKeyInfo(List.of(
keyInfoFactory.newX509Data(List.of(cert))));
} else if (publicKey != null) {
return keyInfoFactory.newKeyInfo(List.of(
keyInfoFactory.newKeyValue(publicKey)));
} else if (keyName != null) {
return keyInfoFactory.newKeyInfo(List.of(
keyInfoFactory.newKeyName(keyName)));
} else {
return null;
}
}
}
//////////// VALIDATE ////////////
// Create a Validator, ks will be used if there is a KeyName
public static Validator validator(KeyStore ks)
throws Exception {
return new Validator(ks);
}
// Create a Validator, key will either be inside KeyInfo or
// a key will be provided when validate() is called
public static Validator validator()
throws Exception {
return new Validator(null);
}
public static class Validator {
private Boolean secureValidation = null;
private String baseURI = null;
private final KeyStore ks;
public Validator(KeyStore ks) {
this.ks = ks;
}
public Validator secureValidation(boolean v) {
this.secureValidation = v;
return this;
}
public Validator baseURI(String base) {
this.baseURI = base;
return this;
}
public boolean validate(Document document) throws Exception {
return validate(document, null);
}
// If key is not null, any key from the signature will be ignored
public boolean validate(Document document, PublicKey key)
throws Exception {
NodeList nodeList = document.getElementsByTagName("Signature");
if (nodeList.getLength() == 1) {
Node signatureNode = nodeList.item(0);
if (signatureNode != null) {
KeySelector ks = key == null ? new MyKeySelector(this.ks) : new KeySelector() {
@Override
public KeySelectorResult select(KeyInfo ki, Purpose p,
AlgorithmMethod m, XMLCryptoContext c) {
return () -> key;
}
};
DOMValidateContext valContext
= new DOMValidateContext(ks, signatureNode);
if (baseURI != null) {
valContext.setBaseURI(baseURI);
}
if (secureValidation != null) {
valContext.setProperty("org.jcp.xml.dsig.secureValidation",
secureValidation);
valContext.setProperty("org.apache.jcp.xml.dsig.secureValidation",
secureValidation);
}
return XMLSignatureFactory.getInstance("DOM")
.unmarshalXMLSignature(valContext).validate(valContext);
}
}
return false;
}
// Find public key from KeyInfo, ks will be used if it's KeyName
private static class MyKeySelector extends KeySelector {
private final KeyStore ks;
public MyKeySelector(KeyStore ks) {
this.ks = ks;
}
public KeySelectorResult select(KeyInfo keyInfo,
KeySelector.Purpose purpose,
AlgorithmMethod method,
XMLCryptoContext context)
throws KeySelectorException {
Objects.requireNonNull(keyInfo, "Null KeyInfo object!");
for (XMLStructure xmlStructure : keyInfo.getContent()) {
PublicKey pk;
if (xmlStructure instanceof KeyValue kv) {
try {
pk = kv.getPublicKey();
} catch (KeyException ke) {
throw new KeySelectorException(ke);
}
return () -> pk;
} else if (xmlStructure instanceof X509Data x509) {
for (Object data : x509.getContent()) {
if (data instanceof X509Certificate) {
pk = ((X509Certificate) data).getPublicKey();
return () -> pk;
}
}
} else if (xmlStructure instanceof KeyName kn) {
try {
pk = ks.getCertificate(kn.getName()).getPublicKey();
} catch (KeyStoreException e) {
throw new KeySelectorException(e);
}
return () -> pk;
}
}
throw new KeySelectorException("No KeyValue element found!");
}
}
}
//////////// MISC ////////////
/**
* Adds a new rule to "jdk.xml.dsig.secureValidationPolicy"
*/
public static void addPolicy(String rule) {
String value = Security.getProperty("jdk.xml.dsig.secureValidationPolicy");
value = rule + "," + value;
Security.setProperty("jdk.xml.dsig.secureValidationPolicy", value);
}
private XMLUtils() {
assert false : "No one instantiates me";
}
}