XML-Signature (XMLDSig) is set of rules and syntax for signing XML documents.
In short, the rules describe:
-
How to normalize (canonicalize) XML document, so a digest (hash) of the document stays the same even if the document is reformatted.
-
How to calculate digest of the document.
-
How to sign the digest.
-
And where to place the signature.
- Enveloped signature is placed inside the signed document.
- Enveloping signature holds the signed document in its child elements.
- Detached signature is located in an external file.
The XMLDSig does sign a document only, not the signing certificate. Because a single public key can be used in multiple certificates, XMLDSig contains inherent risk that certificate may be replaced after signing of a document.
This issue is solved by XML Advanced Electronic Signatures (XAdES), which extends XMLDSig by adding signed details of the signing certificate into the extensible XMLDSig's <Object>
element.
XMLDSig
Structure of a document with enveloped XMLDSig signature
Let's have a schema of a singable XML document.
<xs:schema version="1.1"
xmlns="https://github.com/vkuzel/XAdES-Demo"
targetNamespace="https://github.com/vkuzel/XAdES-Demo"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
elementFormDefault="qualified">
<xs:element name="singableDocument" type="SingableDocumentType"/>
<xs:complexType name="SingableDocumentType">
<xs:sequence>
<xs:element name="someElement" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>Element holding a string value</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element ref="ds:Signature"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
- Document will have root element
<signableDocument>
and one child element<someElement>
. - Enveloped signature elements (namespace
ds
) will be added into the document, when document is signed.
<singableDocument xmlns="https://github.com/vkuzel/XAdES-Demo">
<someElement>some-text</someElement>
</singableDocument>
After signing, the signature is added into the document.
Comments were added manually afterwards for explanation.
<singableDocument xmlns="https://github.com/vkuzel/XAdES-Demo" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#">
<someElement>some-text</someElement>
<!-- Enveloped signature. -->
<ns2:Signature>
<!-- Signature information. -->
<ns2:SignedInfo>
<ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ns2:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
<!-- Reference to the signed document. Empty URI attribute points
to the root element of the current document. -->
<ns2:Reference URI="">
<!-- List of transformations performed before digest value is
calculated. E.g. remove line feeds, etc... -->
<ns2:Transforms>
<!-- Remove signature element (if there is any) -->
<ns2:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<!-- Normalize document. Remove line feeds, etc. Also
preserve comments. -->
<ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ns2:Transforms>
<ns2:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ns2:DigestValue>99XoijRJvFYlc/340OJDwK8kv9LnmD2xkCtcbyP96M8=</ns2:DigestValue>
</ns2:Reference>
</ns2:SignedInfo>
<!-- Signing certificate information. -->
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIDRTCCAi2gAwIBAgIEQjgraj...</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
<!-- Signature. -->
<ns2:SignatureValue>lo4vdHDqEx2nWrIBxViOWyUpCynGBYSV3VPh...</ns2:SignatureValue>
</ns2:Signature>
</singableDocument>
Creating XMLDSig signature
- Create
<SignedInfo>
describing process (document normalization, algorithms for calculating digest, etc.) for generating signature. - Create
<KeyInfo>
element holding signing certificate. - Generate signature itself into the
<SignatureValue>
element and envelop the<Signature>
element into the document.
SignedInfo
- Classes in this example are located in
javax.xml.crypto.dsig
orjava.security
packages or their subpackages. No 3rd party library is needed. - Instantiation of the
XMLSignatureFactory
will be shown later on.
private SignedInfo createSignedInfo(XMLSignatureFactory xmlSignatureFactory) {
CanonicalizationMethod c14nMethod = xmlSignatureFactory.newCanonicalizationMethod("http://www.w3.org/2001/10/xml-exc-c14n#", null);
DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod("http://www.w3.org/2001/04/xmlenc#sha256", null);
SignatureMethod signMethod = xmlSignatureFactory.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", null);
// Before calculating digest (hash) the document is transformed into its
// canonical (normalized) form so the digest is consistent even if document
// is reformatted, etc.
List<Transform> transforms = List.of(
// Because we are creating enveloped signature, this transformation
// removes any existing <Signature> element from the document,
// before digest calculation. This is useful not only for signing
// the document, but also for validating it when digest has to be
// calculated as well.
xmlSignatureFactory.newTransform("http://www.w3.org/2000/09/xmldsig#enveloped-signature", null),
// This transformation normalises the document. The c14n11 method
// may be used for XML 1.1 document. The #WithComments fragment
// may be used for preserving comments in the document.
// E.g. ...xml-exc-c14n#WithComments
xmlSignatureFactory.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", null)
);
// Empty URI argument points to the root element, i.e. root element is
// signed. Otherwise, the URI argument could hold an id value or full URL
// in case of detached signature.
Reference referenceDoc = xmlSignatureFactory.newReference("", digestMethod, transforms, null, null);
List<Reference> references = List.of(referenceDoc);
return xmlSignatureFactory.newSignedInfo(c14nMethod, signMethod, references);
}
KeyInfo
The java.security.cert.Certificate
object used for KeyInfo
can be loaded from a JKS keystore via standard Java tools (e.g. KeyStore.getInstance("JKS").load()
) or from PKCS#12 keystore by using 3rd party library Bouncy Castle (e.g. KeyStore.getInstance("PKCS12", BouncyCastleProvider.PROVIDER_NAME)
).
private KeyInfo createKeyInfo(XMLSignatureFactory xmlSignatureFactory) {
Certificate certificate = loadCertificateFromKeyStore();
KeyInfoFactory keyInfoFactory = xmlSignatureFactory.getKeyInfoFactory();
X509Data x509Data = keyInfoFactory.newX509Data(List.of(certificate));
return keyInfoFactory.newKeyInfo(List.of(x509Data));
}
Signature
The org.w3c.dom.Document
object can be created by marshalling a XJC generated structures or loading it from a file.
public Document signEnveloped(Document document) {
PrivateKey privateKey = loadPrivateKeyFromKeyStore();
XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance("DOM", "XMLDSig");
SignedInfo signedInfo = createSignedInfo(xmlSignatureFactory);
KeyInfo keyInfo = createKeyInfo(xmlSignatureFactory);
XMLSignature xmlSignature = xmlSignatureFactory.newXMLSignature(signedInfo, keyInfo, null, null, null);
Element rootNode = document.getDocumentElement();
DOMSignContext domSignContext = new DOMSignContext(privateKey, rootNode);
// To reuse XMLDSig namespace from the root element instead of placing
// xmlns="http://www.w3.org/2000/09/xmldsig#" as an argument of the
// <Signature> element, we have to specify namespace prefix in the context.
domSignContext.setDefaultNamespacePrefix("ns2");
xmlSignature.sign(domSignContext);
// Signed document
return document;
}
Validating XMLDSig signature
- Calculate digest of referenced document.
- Obtain public key.
- Verify digest and signature value against the public key.
Warning: In this example implementation, the public key is obtained via the KeyValueKeySelector
from the certificate embedded in the signed document itself. Even if the algorithm returns "valid signature" result, it doesn't mean that document was signed by trusted counterparty, signing certificate is valid, not-revoked or wasn't replaced with another certificate as it was discussed before.
// Returns true for valid signature.
public boolean validate(Document document) {
// Find <Signature> element. Expecting only one.
NodeList signatureNodes = document.getElementsByTagNameNS(XMLNS, "Signature");
if (signatureNodes.getLength() != 1) throw new RuntimeException("Cannot retrieve Signature");
Node signatureNode = signatureNodes.item(0);
// Create a DOMValidateContext and specify a KeyValue KeySelector
// and document context
DOMValidateContext validateContext = new DOMValidateContext(new KeyValueSelector(), signatureNode);
// Create a DOM XMLSignatureFactory that will be used to unmarshal the
// document containing the XMLSignature
XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance("DOM");
XMLSignature signature = xmlSignatureFactory.unmarshalXMLSignature(validateContext);
return signature.validate(validateContext);
}
private static class KeyValueSelector extends KeySelector {
public KeySelectorResult select(
KeyInfo keyInfo,
Purpose purpose,
AlgorithmMethod method,
XMLCryptoContext context
) {
for (XMLStructure keyInfoItem : keyInfo.getContent()) {
PublicKey publicKey = findPublicKey(keyInfoItem);
if (publicKey == null) continue;
return () -> publicKey;
}
throw new KeySelectorException("No KeyValue element found!");
}
private PublicKey findPublicKey(XMLStructure keyInfoItem) {
// The <KeyInfo> element can contain different structures holding a
// public key. In that case a different key-obtaining algorithm would
// have to be used.
if (keyInfoItem instanceof X509Data x509Data) {
List<?> x509DataContent = x509Data.getContent();
for (Object x509Item : x509DataContent) {
if (x509Item instanceof Certificate certificate) {
return certificate.getPublicKey();
}
}
}
return null;
}
}
XAdES
Structure of document with enveloped XAdES signature
XAdES exists in multiple versions. From basic (XAdES BES) holding a signing certificate information to XAdES with timestamp (XAdES-T) or XAdES with complete validation information (XAdES-C), etc.
This example shows basic XAdES implementation.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<singableDocument xmlns="https://github.com/vkuzel/XAdES-Demo" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#">
<someElement>some-text</someElement>
<ns2:Signature Id="signature-3c8cde91-c052-4abe-a7f9-6c3f71f3a1d4">
<ns2:SignedInfo>
<ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ns2:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
<ns2:Reference URI=""><!-- Same reference to the signed document as in XMLDSig --></ns2:Reference>
<!-- XAdES adds new reference to the element with description of
the signing certificate. -->
<ns2:Reference URI="#signed-properties-704f3271-b681-4f35-a8f5-e3c03590d5d7">
<ns2:Transforms>
<ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ns2:Transforms>
<ns2:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ns2:DigestValue>1YsOF7DZSzBJsARfGBdC9aJMOMJtqLUDdXkqzJJPczI=</ns2:DigestValue>
</ns2:Reference>
</ns2:SignedInfo>
<ns2:SignatureValue><!-- Same as in XMLDSig --></ns2:SignatureValue>
<ns2:KeyInfo><!-- Same as in XMLDSig --></ns2:KeyInfo>
<ns2:Object>
<QualifyingProperties xmlns="http://uri.etsi.org/01903/v1.3.2#" Target="#signature-3c8cde91-c052-4abe-a7f9-6c3f71f3a1d4">
<SignedProperties Id="signed-properties-704f3271-b681-4f35-a8f5-e3c03590d5d7">
<SignedSignatureProperties>
<SigningTime>2000-01-01T01:01:01.000+12:00</SigningTime>
<SigningCertificate>
<!-- Certificate information obtained from the
certificate itself. -->
<Cert>
<CertDigest>
<ns2:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ns2:DigestValue>2vrrQh8AIWiSe56oTEm5...</ns2:DigestValue>
</CertDigest>
<IssuerSerial>
<ns2:X509IssuerName>CN=XAdES Demo</ns2:X509IssuerName>
<ns2:X509SerialNumber>15248582071077365500</ns2:X509SerialNumber>
</IssuerSerial>
</Cert>
</SigningCertificate>
<SignaturePolicyIdentifier>
<!-- Presence of the <SignaturePolicyImplied>
element means the signature policy was agreed
between two parties separately and is not specified
in this document via link to the policy itself. -->
<SignaturePolicyImplied />
</SignaturePolicyIdentifier>
</SignedSignatureProperties>
</SignedProperties>
</QualifyingProperties>
</ns2:Object>
</ns2:Signature>
</singableDocument>
Creating XAdES signature
- Create XMLDSig
<SignedInfo>
element with new reference to the XAdES<SignedProperties>
. - Create
<KeyInfo>
same as in the XMLDSig. - Create XAdES
<QualifyingProperties>
holding<SignedProperties>
which contains signed information on the signing certificate. - Generate signature itself, similarly to XMLDSig signature.
SignedInfo
The list of signed info references is extended with a reference to the <SignedProperties>
element.
private Reference createSignedPropertiesReference(String signedPropertiesId) {
DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod("http://www.w3.org/2001/04/xmlenc#sha256", null);
Transform c14nTransform = xmlSignatureFactory.newTransform("http://www.w3.org/2001/10/xml-exc-c14n#", null);
String referenceType = "http://uri.etsi.org/01903#SignedProperties";
List<Transform> transforms = List.of(c14nTransform);
return xmlSignatureFactory.newReference("#" + signedPropertiesId, digestMethod, transforms, referenceType, null);
}
QualifyingProperties
This example assumes DTOs to be generated from the XAdES schema, marshalled into a document, and then adopted into the signed document.
Alternative approach could be to create qualifying properties element manually, by using Document.createElement()
methods. Drawback of such approach would be loss of type safety.
private XMLObject createQualifyingProperties(Document document, String signedPropertiesId, String signatureId) {
org.etsi.uri._01903.v1_3.ObjectFactory xadesFactory = new org.etsi.uri._01903.v1_3.ObjectFactory();
org.w3._2000._09.xmldsig_.ObjectFactory xmldSigFactory = new org.w3._2000._09.xmldsig_.ObjectFactory();
// To obtain all necessary information from certificate we have to work
// with the `X509Certificate` instead of simple `Certificate`.
X509Certificate certificate = loadCertificateFromKeyStore();
DigestAlgAndValueType certificateDigest = xadesFactory.createDigestAlgAndValueType();
// SHA-256 digest is calculated from the DER encoded version of the certificate
// returned from the `Certificate.getEncoded()` method.
certificateDigest.setDigestValue(calculateCertificateSha256Digest());
certificateDigest.setDigestMethod(xmldSigFactory.createDigestMethodType());
certificateDigest.getDigestMethod().setAlgorithm("http://www.w3.org/2001/04/xmlenc#sha256");
X509IssuerSerialType x509IssuerSerialType = xmldSigFactory.createX509IssuerSerialType();
x509IssuerSerialType.setX509IssuerName(certificate.getIssuerX500Principal().getName());
x509IssuerSerialType.setX509SerialNumber(certificate.getSerialNumber());
CertIDType signingCertificate = xadesFactory.createCertIDType();
signingCertificate.setCertDigest(certificateDigest);
signingCertificate.setIssuerSerial(x509IssuerSerialType);
CertIDListType signingCertificates = xadesFactory.createCertIDListType();
signingCertificates.getCert().add(signingCertificate);
// Usually the signature policy identifier points to a particular
// policy. Alternatively, the empty "implied element" can be used to
// state policy can be derived from semantics of the document.
SignaturePolicyIdentifierType signaturePolicyIdentifierType = xadesFactory.createSignaturePolicyIdentifierType();
signaturePolicyIdentifierType.setSignaturePolicyImplied("");
SignedSignaturePropertiesType signedSignaturePropertiesType = xadesFactory.createSignedSignaturePropertiesType();
signedSignaturePropertiesType.setSigningTime(currentTime());
signedSignaturePropertiesType.setSigningCertificate(signingCertificates);
signedSignaturePropertiesType.setSignaturePolicyIdentifier(signaturePolicyIdentifierType);
SignedPropertiesType signedPropertiesType = xadesFactory.createSignedPropertiesType();
signedPropertiesType.setId(signedPropertiesId);
signedPropertiesType.setSignedSignatureProperties(signedSignaturePropertiesType);
QualifyingPropertiesType qualifyingPropertiesType = xadesFactory.createQualifyingPropertiesType();
qualifyingPropertiesType.setTarget("#" + signatureId);
qualifyingPropertiesType.setSignedProperties(signedPropertiesType);
JAXBElement<QualifyingPropertiesType> qualifyingProperties = xadesFactory.createQualifyingProperties(qualifyingPropertiesType);
// Marshalling
JAXBContext jaxbContext = JAXBContext.newInstance(QualifyingPropertiesType.class);
Marshaller marshaller = jaxbContext.createMarshaller();
DOMResult domResult = new DOMResult();
marshaller.marshal(qualifyingProperties, domResult);
Element qualifyingPropertiesElement = (Element) domResult.getNode();
// If the qualifying-properties owner document is different to an owner
// document of signed-data, then the DOMXMLSignature.marshal() ->
// DOMXMLObject.marshal() method will try to adopt the qualifying-properties
// element. This adoption removes id flags from attributes leading to
// the "Cannot resolve element with ID" error.
//
// Explained: https://stackoverflow.com/questions/17331187/xml-dig-sig-error-after-upgrade-to-java7u25
//
// Also, sometimes the document object is just a wrapper delegating to
// a different document object. Different instances of owner-document
// objects lead to aforementioned error.
//
// To mitigate the issues, the qualifying-properties element is imported
// into the signed-data owner document.
Node importedQualifyingPropertiesElement = document.importNode(qualifyingPropertiesElement, true);
// Re-assign id flag lost during element import to all xs:id attributes
// by setting the `Element.setIdAttribute("Id", true)`.
markIdsRecursively(importedQualifyingPropertiesElement.getChildNodes());
// If the owner document of the DOMStructure is different from the target
// document of an XMLSignature, the XMLSignature.sign(XMLSignContext) method
// imports the node into the target document before generating the signature.
DOMStructure qualifyingPropertiesObject = new DOMStructure(importedQualifyingPropertiesElement);
return xmlSignatureFactory.newXMLObject(singletonList(qualifyingPropertiesObject), null, null, null);
}
Signature
public Document signEnveloped(Document document) {
// Id to reference the signed properties from the signature.
String signedPropertiesId = "signed-properties-" + UUID.randomUUID();
// Id to reference signature from the qualifying properties.
String signatureId = "signature-" + UUID.randomUUID();
SignedInfo signedInfo = createSignedInfo(signedPropertiesId);
KeyInfo keyInfo = createKeyInfo();
XMLObject qualifyingProperties = createQualifyingProperties(document, signedPropertiesId, signatureId);
XMLSignature xmlSignature = xmlSignatureFactory.newXMLSignature(signedInfo, keyInfo, List.of(qualifyingProperties), signatureId, null);
DOMSignContext domSignContext = createDomSignContext(document);
xmlSignature.sign(domSignContext);
return document;
}
Validating XAdES signature
- Calculate digest of referenced document and signed signature properties.
- Obtain public key.
- Verify digest and signature value against the public key.
Warning: Similarly to the XMLDSig example, this algorithm does not verify certificate validity, revocation, nor checks whether content of <KeyInfo>
element was replaced with another certificate holding the same public key. That would have to be done by calculating digest of the <KeyInfo>
certificate and comparing it against the signed <CertDigest>
value.
public boolean validate(Document document) {
// When document is deserialized from an XML file, the SignerProperties
// element ID attribute is not properly marked, which means reference
// URL to the signed properties does not work. Manual marking it, fixes
// the issue, similarly to what is done when generating qualifying
// properties.
markIdsRecursively(document);
// Rest of the implementation is same as for XMLDSig, including KeyValueSelector.
...
return signature.validate(validateContext);
}
Full example implementation
Consult the XAdES Demo project for implementation details.
Troubleshooting
Checking validation of each reference
Validate each reference separately to find out which one is invalid.
for (Reference reference : signature.getSignedInfo().getReferences()){
boolean isValid = reference.validate(validateContext);
}
Enabling logging
There are multiple DEBUG level messages in the XML signing code which may be helpful when debugging the signing mechanism. These are not enabled by default and must be enabled manually.
Debug level can be increased programmatically via a static class initializer.
static {
java.util.logging.LogManager.getLogManager().updateConfiguration(propertyName -> {
// Check for the log level property
if (propertyName.contains(".level")) {
// Level = ALL => logs all messages
return (oldValue, newValue) -> java.util.logging.Level.ALL.getName();
} else {
// Identity mapper for other properties
return (oldValue, newValue) -> newValue;
}
});
}
Fixing the relative namespace error
The signed document has to have absolute namespace, otherwise the "javax.xml.crypto.dsig.XMLSignatureException: javax.xml.crypto.dsig.TransformException: com.sun.org.apache.xml.internal.security.c14n.CanonicalizationException: Element has a relative namespace:" will occur.
Replace xmlns="namespace"
with xmlns="https://company.com/namespace"
in the XSD to solve the issue.
Fixing the URIReferenceException
If the "javax.xml.crypto.dsig.XMLSignatureException: javax.xml.crypto.URIReferenceException: com.sun.org.apache.xml.internal.security.utils.resolver.ResourceResolverException: Cannot resolve element with ID" occur and your id values seems to be correct, including hash symbol in a referencing attribute, it is likely the id attributes in the document are not properly marked.
Please make sure that your markIdsRecursively()
method implementation walks through all elements in the document and marks all attributes, including upper of lower case versions of such attributes in case your implementation is case-sensitive.