Hello Stack Overflow community,
I’m facing an issue with XML signature verification and need some help debugging my code.
I have three key classes, my signature didn’t verified i am also new to java and tried everything but no success. kindly point out my mistake and correct my code also. so signature can be verified and generated against xml . kindly help me please ISO 20022 xml format shall be digitally signed and verified from below code
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03"><AppHdr><Fr><FIId><FinInstnId><BICFI>ABCDEFGHIJKL</BICFI></FinInstnId></FIId></Fr><To><FIId><FinInstnId><BICFI>MNOPQRSTUVWXYZ</BICFI></FinInstnId></FIId></To><BizMsgIdr>Message123</BizMsgIdr><MsgDefIdr>abcd.1234</MsgDefIdr><CreDt>2024-08-15T12:00:00Z</CreDt></AppHdr></Document>
- XadesSigner (CREATE SIGNATURE)
- XadesSignatureVerifier (verify signature)
- NoUriDereferencer.java (implemented custom URIDereferencer )
verfifier logs
Dereferencing URI: #_f838896a-be99-44a1-8b37-af12e079be08
Dereferencing URI: #_98ff15ed-7328-4241-abd0-67021d18488f-signedprops
URI is null or not found in map: com.ibm.xml.crypto.dsig.dom.ReferenceImpl@b0cf108f
Using data for null URI: javax.xml.crypto.OctetStreamData@982a8d94
Core Validation Status: false
Signature failed core validation
Signature validation status: true
Reference validity status: true, Reference URI: [#_f838896a-be99-44a1-8b37-af12e079be08]
Reference validity status: true, Reference URI: [#_98ff15ed-7328-4241-abd0-67021d18488f-signedprops]
Reference validity status: false, Reference URI: [null]
Signature is invalid.
package xades;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.xml.crypto.Data;
import javax.xml.crypto.NodeSetData;
import javax.xml.crypto.URIDereferencer;
import javax.xml.crypto.URIReference;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.keyinfo.X509IssuerSerial;
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.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class XadesSigner {
public Document sign(Document doc, X509Certificate signerCertificate, PrivateKey privateKey, boolean debugLog) throws Exception {
final String xadesNS = "http://uri.etsi.org/01903/v1.3.2#";
final String signedpropsIdSuffix = "-signedprops";
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
// 1. Prepare KeyInfo
KeyInfoFactory kif = fac.getKeyInfoFactory();
X509IssuerSerial x509is = kif.newX509IssuerSerial(
signerCertificate.getIssuerX500Principal().toString(),
signerCertificate.getSerialNumber());
X509Data x509data = kif.newX509Data(Collections.singletonList(x509is));
final String keyInfoId = "_" + UUID.randomUUID().toString();
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509data), keyInfoId);
// 2. Prepare references
List<Reference> refs = new ArrayList<>();
Reference ref1 = fac.newReference("#" + keyInfoId,
fac.newDigestMethod(DigestMethod.SHA256, null),
Collections.singletonList(fac.newCanonicalizationMethod(
CanonicalizationMethod.EXCLUSIVE, (XMLStructure) null)),
null, null);
refs.add(ref1);
final String signedpropsId = "_" + UUID.randomUUID().toString() + signedpropsIdSuffix;
Reference ref2 = fac.newReference("#" + signedpropsId,
fac.newDigestMethod(DigestMethod.SHA256, null),
Collections.singletonList(fac.newCanonicalizationMethod(
CanonicalizationMethod.EXCLUSIVE, (XMLStructure) null)),
"http://uri.etsi.org/01903/v1.3.2#SignedProperties", null);
refs.add(ref2);
Reference ref3 = fac.newReference(null,
fac.newDigestMethod(DigestMethod.SHA256, null),
Collections.singletonList(fac.newCanonicalizationMethod(
CanonicalizationMethod.EXCLUSIVE, (XMLStructure) null)),
null, null);
refs.add(ref3);
SignedInfo si = fac.newSignedInfo(
fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (XMLStructure) null),
fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null), refs);
// 3. Create element AppHdr/Sgntr that will contain the <ds:Signature>
Node appHdr = null;
NodeList sgntrList = doc.getElementsByTagName("AppHdr");
if (sgntrList.getLength() != 0)
appHdr = sgntrList.item(0);
if (appHdr == null)
throw new Exception("Mandatory element AppHdr is missing in the document to be signed");
Node sgntr = appHdr.appendChild(doc.createElementNS(appHdr.getNamespaceURI(), "Sgntr"));
DOMSignContext dsc = new DOMSignContext(privateKey, sgntr);
if (debugLog) {
dsc.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE);
}
dsc.putNamespacePrefix(XMLSignature.XMLNS, "ds");
// 4. Set up <ds:Object> with <QualifyingProperties> inside that includes SigningTime
Element QPElement = doc.createElementNS(xadesNS, "xades:QualifyingProperties");
QPElement.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xades", xadesNS);
Element SPElement = doc.createElementNS(xadesNS, "xades:SignedProperties");
SPElement.setAttributeNS(null, "Id", signedpropsId);
dsc.setIdAttributeNS(SPElement, null, "Id");
SPElement.setIdAttributeNS(null, "Id", true);
QPElement.appendChild(SPElement);
Element SSPElement = doc.createElementNS(xadesNS, "xades:SignedSignatureProperties");
SPElement.appendChild(SSPElement);
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
String signingTime = df.format(new Date());
Element STElement = doc.createElementNS(xadesNS, "xades:SigningTime");
STElement.appendChild(doc.createTextNode(signingTime));
SSPElement.appendChild(STElement);
DOMStructure qualifPropStruct = new DOMStructure(QPElement);
List<DOMStructure> xmlObj = new ArrayList<>();
xmlObj.add(qualifPropStruct);
XMLObject object = fac.newXMLObject(xmlObj, null, null, null);
List<XMLObject> objects = Collections.singletonList(object);
// 5. Set up custom URIDereferencer to process Reference without URI
final NodeList docNodes = doc.getElementsByTagName("Document");
final Node docNode = docNodes.item(0);
ByteArrayOutputStream refOutputStream = new ByteArrayOutputStream();
Transformer xform = TransformerFactory.newInstance().newTransformer();
xform.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
xform.transform(new DOMSource(docNode), new StreamResult(refOutputStream));
InputStream refInputStream = new ByteArrayInputStream(refOutputStream.toByteArray());
dsc.setURIDereferencer(new NoUriDereferencer(refInputStream));
// 6. Sign it!
XMLSignature signature = fac.newXMLSignature(si, ki, objects, null, null);
signature.sign(dsc);
// 7. For debug purposes, log each reference's digest and transformed data
if (debugLog) {
int i = 0;
for (Reference ref : refs) {
StringBuilder sb = new StringBuilder();
String digValStr = digestToString(ref.getDigestValue());
InputStream is = ref.getDigestInputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
sb.append(new String(buffer, 0, bytesRead));
}
is.close();
i++;
System.out.println("ref #" + i + " URI: [" + ref.getURI() + "], digest: " + digValStr + ", transformed data: [" + sb.toString() + "]");
}
}
return doc;
}
private static String digestToString(byte[] digest) {
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
package xades;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.security.Key;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
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.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.OutputKeys;
public class XadesSignatureVerifier {
private X509Certificate signerCertificate;
public XadesSignatureVerifier(X509Certificate signerCertificate) {
this.signerCertificate = signerCertificate;
}
public SignatureInfo verify(String dataPDU, boolean debugLog) throws Exception {
SignatureInfo sigInfo = new SignatureInfo();
XPath xpath = XPathFactory.newInstance().newXPath();
if (debugLog) {
System.out.println("Starting verification process...");
}
// Parse the input XML document
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(dataPDU)));
if (debugLog) {
System.out.println("Original Document XML:n" + convertDocumentToString(doc));
System.out.println("Original Document Root Element: " + doc.getDocumentElement().getTagName());
}
// Locate the Signature node
String xpathExpression = "//*[local-name()='Signature']";
NodeList nodes = (NodeList) xpath.evaluate(xpathExpression, doc.getDocumentElement(), XPathConstants.NODESET);
if (nodes == null || nodes.getLength() == 0) {
throw new Exception("Signature is missing in the document");
}
Node nodeSignature = nodes.item(0);
if (debugLog) {
System.out.println("Found Signature Node: " + nodeSignature.getNodeName());
}
// Create a KeySelector to retrieve the public key
final KeySelector keySelector = new KeySelector() {
@Override
public KeySelectorResult select(KeyInfo keyInfo, KeySelector.Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException {
return () -> signerCertificate.getPublicKey();
}
};
// Set up XMLSignatureFactory and validation context
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
DOMValidateContext valContext = new DOMValidateContext(keySelector, nodeSignature);
final NodeList docNodes = doc.getElementsByTagName("Document");
final Node docNode = docNodes.item(0);
if (debugLog) {
System.out.println("Document Node:n" + convertNodeToString(docNode));
}
// Set URI Dereferencer
ByteArrayOutputStream refOutputStream = new ByteArrayOutputStream();
Transformer xform = TransformerFactory.newInstance().newTransformer();
xform.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
xform.transform(new DOMSource(docNode), new StreamResult(refOutputStream));
InputStream refInputStream = new ByteArrayInputStream(refOutputStream.toByteArray());
valContext.setURIDereferencer(new NoUriDereferencer(refInputStream));
// Validate SignedProperties element
NodeList nl = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SignedProperties");
if (nl.getLength() == 0) {
throw new Exception("SignedProperties is missing in signature");
}
Element elemSignedProps = (Element) nl.item(0);
valContext.setIdAttributeNS(elemSignedProps, null, "Id");
// Validate the signature
XMLSignature signature = fac.unmarshalXMLSignature(valContext);
boolean coreValidity = signature.validate(valContext);
sigInfo.setValid(coreValidity);
if (debugLog) {
System.out.println("Core Validation Status: " + coreValidity);
}
if (!coreValidity) {
sigInfo.setValidationErrors("Signature failed core validation");
System.out.println("Signature failed core validation");
if (debugLog) {
boolean sv = signature.getSignatureValue().validate(valContext);
sigInfo.setValidationErrors("Signature validation status: " + sv);
System.out.println("Signature validation status: " + sv);
Iterator<?> i = signature.getSignedInfo().getReferences().iterator();
List<Boolean> refValidityList = new ArrayList<>();
while (i.hasNext()) {
Reference ref = (Reference) i.next();
boolean refValid = ref.validate(valContext);
refValidityList.add(refValid);
String refUri = ref.getURI();
if (refUri == null) {
refUri = "null";
}
sigInfo.setValidationErrors("Reference validity status: " + refValid + ", Reference URI: [" + refUri + "]");
System.out.println("Reference validity status: " + refValid + ", Reference URI: [" + refUri + "]");
}
sigInfo.setReferenceValidity(refValidityList);
}
}
return sigInfo;
}
private String convertDocumentToString(Document doc) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(new DOMSource(doc), new StreamResult(outputStream));
return new String(outputStream.toByteArray());
}
private String convertNodeToString(Node node) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
transformer.transform(new DOMSource(node), new StreamResult(outputStream));
return new String(outputStream.toByteArray());
}
}
package xades;
import java.io.InputStream;
import javax.xml.crypto.Data;
import javax.xml.crypto.OctetStreamData;
import javax.xml.crypto.URIReference;
import javax.xml.crypto.URIReferenceException;
import javax.xml.crypto.URIDereferencer;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dsig.XMLSignatureFactory;
public class NoUriDereferencer implements URIDereferencer {
private InputStream inputStream;
public NoUriDereferencer(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public Data dereference(URIReference uriRef, XMLCryptoContext ctx)
throws URIReferenceException {
String uri = uriRef.getURI();
if (uri != null) {
System.out.println("Dereferencing URI: " + uri);
URIDereferencer defaultDereferencer =
XMLSignatureFactory.getInstance("DOM").getURIDereferencer();
try {
// Utilize the default dereferencer if a URI is present
return defaultDereferencer.dereference(uriRef, ctx);
} catch (URIReferenceException e) {
System.err.println("Error dereferencing URI: " + uri);
e.printStackTrace();
throw e;
}
} else {
// Handle the case where the URI is null or not found
System.out.println("URI is null or not found in map: " + uriRef);
Data data = new OctetStreamData(inputStream);
System.out.println("Using data for null URI: " + data.toString());
return data;
}
}
}
SJS is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1