Reputation: 1
I need to sign a specific XML tag with java 1.6. My XML is like this:
<RecepcionarLoteRps>
<EnviarLoteRpsEnvio xmlns="http://isscuritiba.curitiba.pr.gov.br/iss/nfse.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://isscuritiba.curitiba.pr.gov.br/iss/nfse.xsd">
<LoteRps>
<ListaRps>
<Rps>
<InfRps id="1">
. . .
</InfRps>
</Rps>
</ListaRps>
</EnviarLoteRpsEnvio>
My java code to sign it (excluding the keystore part) is:
try {
String ArqAssinar = "file.xml";
String Charset = "UTF-8";
/* URI and ID */
String idRef = "1";
String uriRef = "#" + idRef;
XMLSignatureFactory XmlSignFac = XMLSignatureFactory.getInstance("DOM");
DigestMethod DigMet = XmlSignFac.newDigestMethod(DigestMethod.SHA1, null);
Transform TransfRef1 = XmlSignFac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null);
Transform TransfRef2 = XmlSignFac.newTransform(SignatureMethod.RSA_SHA1, (C14NMethodParameterSpec) null);
List<Transform> Transfs = new ArrayList<Transform>();
Transfs.add(TransfRef1);
Transfs.add(TransfRef2);
/* Reference - where I use URI and ID */
Reference Ref = XmlSignFac.newReference(uriRef, DigMet, Transfs, null, idRef);
CanonicalizationMethod CannMet = XmlSignFac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null);
SignatureMethod SignMet = XmlSignFac.newSignatureMethod(SignatureMethod.RSA_SHA1, null);
/* SignedInfo - where I use Reference */
SignedInfo SignInfo = XmlSignFac.newSignedInfo(CannMet, SignMet, Collections.singletonList(Ref));
KeyInfoFactory keyInfFac = XmlSignFac.getKeyInfoFactory();
List<X509Certificate> X509Content = new ArrayList<X509Certificate>();
X509Content.add(Certif);
X509Data X509dados = keyInfFac.newX509Data(X509Content);
KeyInfo KeyInf = keyInfFac.newKeyInfo(Collections.singletonList(X509dados));
/* Process file for input */
DocumentBuilderFactory DocBuilderFactory = DocumentBuilderFactory.newInstance();
DocBuilderFactory.setNamespaceAware(true);
DocumentBuilder DocBuilder = DocBuilderFactory.newDocumentBuilder();
InputStream Input = new FileInputStream(arq);
Reader Leitor = new InputStreamReader(Input, Charset);
InputSource Origem = new InputSource(Leitor);
Document Doc = DocBuilder.parse(Origem);
/* Search for tag in document using ID */
XPathFactory factory = XPathFactory.newInstance();
XPath xpathPesquisa = factory.newXPath();
XPathExpression expr = xpathPesquisa.compile(String.format("//*[@id='%s']", idRef));
NodeList nodes = (NodeList) expr.evaluate(docParaAssinar, XPathConstants.NODESET);
Node Tag = null;
DOMSignContext DocSignCont = null;
XMLSignature Signature = null;
if (nodes.getLength() != 0) {
tagComId = nodes.item(0);
Tag = tagComId.getParentNode();
DocSignCont = new DOMSignContext(PrivPass, Tag);
/* Do the signature */
Signature = this.XmlSignFac.newXMLSignature(SignInfo, KeyInf);
Signature.sign(DocSignCont);
/* Updates the file */
OutputStream Saida = new FileOutputStream(arqAtualizar);
Writer Escritor = new OutputStreamWriter(Saida, Charset);
StreamResult Resultado = new StreamResult(Escritor);
TransformerFactory TransformFac = TransformerFactory.newInstance();
Transformer Transf = TransformFac.newTransformer();
Transf.transform(new DOMSource(docAssinado), Resultado);
} else {
. . .
}
} catch (Exception E) {
. . .
}
When I run this code in Eclipse I get a StackOverflow error inside the DOMXMLSignature.sign() method. There is a call for the DOMXMLSignature.digestReference() method, and the function calls itself indefinitely.
This code works if the URI is "" and ID is null, that is, when I need to sign the entire XML.
What do I need to do differently to get sign a specific XML tag?
I have had some problems with this code:
public final class DOMXMLSignature extends DOMStructure
implements XMLSignature {
@Override
public void sign(XMLSignContext signContext)
throws MarshalException, XMLSignatureException
{
if (signContext == null) {
throw new NullPointerException("signContext cannot be null");
}
DOMSignContext context = (DOMSignContext)signContext;
marshal(context.getParent(), context.getNextSibling(),
DOMUtils.getSignaturePrefix(context), context);
// generate references and signature value
List<Reference> allReferences = new ArrayList<>();
// traverse the Signature and register all objects with IDs that
// may contain References
signatureIdMap = new HashMap<>();
signatureIdMap.put(id, this);
signatureIdMap.put(si.getId(), si);
@SuppressWarnings("unchecked")
List<Reference> refs = si.getReferences();
for (Reference ref : refs) {
signatureIdMap.put(ref.getId(), ref);
}
for (XMLObject obj : objects) {
signatureIdMap.put(obj.getId(), obj);
@SuppressWarnings("unchecked")
List<XMLStructure> content = obj.getContent();
for (XMLStructure xs : content) {
if (xs instanceof Manifest) {
Manifest man = (Manifest)xs;
signatureIdMap.put(man.getId(), man);
@SuppressWarnings("unchecked")
List<Reference> manRefs = man.getReferences();
for (Reference ref : manRefs) {
allReferences.add(ref);
signatureIdMap.put(ref.getId(), ref);
}
}
}
}
// always add SignedInfo references after Manifest references so
// that Manifest reference are digested first
allReferences.addAll(refs);
// generate/digest each reference
for (Reference ref : allReferences) {
digestReference((DOMReference)ref, signContext);
}
// do final sweep to digest any references that were skipped or missed
for (Reference ref : allReferences) {
if (((DOMReference)ref).isDigested()) {
continue;
}
((DOMReference)ref).digest(signContext);
}
Key signingKey = null;
try {
KeySelectorResult keySelectorResult = signContext.getKeySelector().select(ki,
KeySelector.Purpose.SIGN,
si.getSignatureMethod(),
signContext);
signingKey = keySelectorResult.getKey();
if (signingKey == null) {
throw new XMLSignatureException("the keySelector did not " +
"find a signing key");
}
ksr = keySelectorResult;
} catch (KeySelectorException kse) {
throw new XMLSignatureException("cannot find signing key", kse);
}
// calculate signature value
try {
byte[] val = ((AbstractDOMSignatureMethod)
si.getSignatureMethod()).sign(signingKey, si, signContext);
((DOMSignatureValue)sv).setValue(val);
} catch (InvalidKeyException ike) {
throw new XMLSignatureException(ike);
}
this.localSigElem = sigElem;
}
private void digestReference(DOMReference ref, XMLSignContext signContext)
throws XMLSignatureException
{
if (ref.isDigested()) {
return;
}
// check dependencies
String uri = ref.getURI();
if (Utils.sameDocumentURI(uri)) {
String parsedId = Utils.parseIdFromSameDocumentURI(uri);
if (parsedId != null && signatureIdMap.containsKey(parsedId)) {
XMLStructure xs = signatureIdMap.get(parsedId);
if (xs instanceof DOMReference) {
digestReference((DOMReference)xs, signContext);
} else if (xs instanceof Manifest) {
Manifest man = (Manifest)xs;
List<Reference> manRefs = DOMManifest.getManifestReferences(man);
for (int i = 0, size = manRefs.size(); i < size; i++) {
digestReference((DOMReference)manRefs.get(i),
signContext);
}
}
}
// if uri="" and there are XPath Transforms, there may be
// reference dependencies in the XPath Transform - so be on
// the safe side, and skip and do at end in the final sweep
if (uri.length() == 0) {
List<Transform> transforms = ref.getTransforms();
for (Transform transform : transforms) {
String transformAlg = transform.getAlgorithm();
if (transformAlg.equals(Transform.XPATH) ||
transformAlg.equals(Transform.XPATH2)) {
return;
}
}
}
}
ref.digest(signContext);
}
}
public final class Utils {
/**
* Returns the ID from a same-document URI (ex: "#id")
*/
public static String parseIdFromSameDocumentURI(String uri) {
if (uri.length() == 0) {
return null;
}
String id = uri.substring(1);
if (id != null && id.startsWith("xpointer(id(")) {
int i1 = id.indexOf('\'');
int i2 = id.indexOf('\'', i1+1);
id = id.substring(i1+1, i2);
}
return id;
}
/**
* Returns true if uri is a same-document URI, false otherwise.
*/
public static boolean sameDocumentURI(String uri) {
return uri != null && (uri.length() == 0 || uri.charAt(0) == '#');
}
}
This is just a piece of the DOMXMLSignature class and the Util class, just the methods that matter to the case.
My code above class the DOMXMLSignature.sign() method. This method do the marshall, get the reference, get the reference id, the call DOMXMLSignature.digestReference() method.
The DOMXMLSignature.digestReference() method verify if the reference is alredy digested - it is not. So he get the URI, verifies if this is a same-document URI - it is - , confiorms that the id in document is the id in URI. Then, the problem: the instance obtainded with signatureIdMap.get(parsedId) is ALWAYS of the type DOMReference, so the method calls yourself indefinitely.
This was happening in the java 1.6, in native class with no change. How can I solve this problem and sign a specific XML tag? Will I have to calclulate the digestvalue before call the DOMXMLSignature.sign() method?
Upvotes: 0
Views: 524
Reputation: 38771
Wow.
First, your code is not a reproducible example:
tagComId docParaAssinar docAssinado
are not declared, and the latter two never set. I added the type Node
for the first and replaced the latter two by Doc
. FYI: using initial caps for names of locals (also fields or methods) is against conventional style and thus feels weird, but does work, so I didn't bother changing these.
SignatureMethod.RSA_SHA1
is not a valid Transform
and trying to use it as one throws an exception. It also doesn't make sense to be in the Transforms list of a Reference, because signature is not applied separately to one Reference, but to the SignedInfo which includes potentially multiple References plus other data. I removed it.
in addition for more convenient testing I replaced the file I/O (which looked okay) with canned input and console output.
With these changes I do reproduce the stackoverflow. This is because you specified #1 as the URI and 1 as the id of the Reference, so to resolve the Reference it must goto the referenced element and resolve it, and the referenced element is itself the (same) Reference so it must go to the identified referenced element and resolve it, so it must etc etc. You must not make the id of the Reference the same as the element you are trying to reference, should not make it the same any other element, and you don't need to refer to the Reference itself so it's better to not set any id for it. I changed the id in newReference
to null.
With these changes, it does work for me to sign "" (the whole document) but not "#1". I found that is because #x does NOT refer to (an element with) an attribute named "id", it refers to (one with) an attribute defined in the schema or DTD as an id -- and your <InfRps id="1">
is not defined as an id. See
No working ID attribute when parsing XML in java
Java XML DOM: how are id Attributes special?
Xml id attribute to work with Java's getElementById?
The schema you reference defines this attribute only as a string not an id, and isn't being used anyway because DOM-parser by default doesn't use schemas. Rather than fiddle with editting and using a schema, I wrote a (very!) minimal DTD to make this attribute an id, and the following code works for me to sign the desired element (using a dummy key&cert I created):
static void SO69652045XMLSign (String[] args) throws Exception {
//++ debug data
String input = "<!DOCTYPE RecepcionarLoteRps [ <!ATTLIST InfRps id ID #REQUIRED> ] >"+
"<RecepcionarLoteRps><EnviarLoteRpsEnvio xmlns=\"http://isscuritiba.curitiba.pr.gov.br/iss/nfse.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://isscuritiba.curitiba.pr.gov.br/iss/nfse.xsd\">"+
"<LoteRps><ListaRps><Rps><InfRps id=\"1\">xxx</InfRps></Rps></ListaRps> </LoteRps> </EnviarLoteRpsEnvio></RecepcionarLoteRps>";
KeyStore keystore = KeyStore.getInstance("PKCS12");
try(InputStream is = new FileInputStream("69652045.p12")){ keystore.load(is,"sekrit".toCharArray()); }
PrivateKey PrivPass = (PrivateKey) keystore.getKey("mykey","sekrit".toCharArray());
X509Certificate Certif = (X509Certificate) keystore.getCertificateChain("mykey")[0];
//--String ArqAssinar = "file.xml";
//--String Charset = "UTF-8";
/* URI and ID */
String idRef = "1";
String uriRef = "#" + idRef;
XMLSignatureFactory XmlSignFac = XMLSignatureFactory.getInstance("DOM");
DigestMethod DigMet = XmlSignFac.newDigestMethod(DigestMethod.SHA1, null);
Transform TransfRef1 = XmlSignFac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null);
//--Transform TransfRef2 = XmlSignFac.newTransform(SignatureMethod.RSA_SHA1, (C14NMethodParameterSpec) null);
List<Transform> Transfs = new ArrayList<Transform>();
Transfs.add(TransfRef1);
//--Transfs.add(TransfRef2);
/* Reference - where I use URI and ID */
Reference Ref = XmlSignFac.newReference(uriRef, DigMet, Transfs, null, /*idRef*/null);
CanonicalizationMethod CannMet = XmlSignFac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null);
SignatureMethod SignMet = XmlSignFac.newSignatureMethod(SignatureMethod.RSA_SHA1, null);
/* SignedInfo - where I use Reference */
SignedInfo SignInfo = XmlSignFac.newSignedInfo(CannMet, SignMet, Collections.singletonList(Ref));
KeyInfoFactory keyInfFac = XmlSignFac.getKeyInfoFactory();
List<X509Certificate> X509Content = new ArrayList<X509Certificate>();
X509Content.add(Certif);
X509Data X509dados = keyInfFac.newX509Data(X509Content);
KeyInfo KeyInf = keyInfFac.newKeyInfo(Collections.singletonList(X509dados));
/* Process file for input */
DocumentBuilderFactory DocBuilderFactory = DocumentBuilderFactory.newInstance();
DocBuilderFactory.setNamespaceAware(true);
DocumentBuilder DocBuilder = DocBuilderFactory.newDocumentBuilder();
//--InputStream Input = new FileInputStream(arq);
//--Reader Leitor = new InputStreamReader(Input, Charset);
//--InputSource Origem = new InputSource(Leitor);
InputSource Origem = new InputSource (new StringReader (input)); //++
Document Doc = DocBuilder.parse(Origem);
/* Search for tag in document using ID */
XPathFactory factory = XPathFactory.newInstance();
XPath xpathPesquisa = factory.newXPath();
XPathExpression expr = xpathPesquisa.compile(String.format("//*[@id='%s']", idRef));
NodeList nodes = (NodeList) expr.evaluate(/*docParaAssinar*/Doc, XPathConstants.NODESET);
Node Tag = null;
DOMSignContext DocSignCont = null;
XMLSignature Signature = null;
if (nodes.getLength() != 0) {
Node tagComId = nodes.item(0); //**
Tag = tagComId.getParentNode();
DocSignCont = new DOMSignContext(PrivPass, Tag);
/* Do the signature */
Signature = /*this.*/XmlSignFac.newXMLSignature(SignInfo, KeyInf);
Signature.sign(DocSignCont);
/* Updates the file */
//--OutputStream Saida = new FileOutputStream(arqAtualizar);
//--Writer Escritor = new OutputStreamWriter(Saida, Charset);
Writer Escritor = new OutputStreamWriter(System.out,StandardCharsets.UTF_8); //++
StreamResult Resultado = new StreamResult(Escritor);
TransformerFactory TransformFac = TransformerFactory.newInstance();
Transformer Transf = TransformFac.newTransformer();
Transf.transform(new DOMSource(/*docAssinado*/Doc), Resultado);
} else {
throw new Exception ("id not found");
}
}
which produces, lightly edited for clarity:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<RecepcionarLoteRps><EnviarLoteRpsEnvio xmlns="http://isscuritiba.curitiba.pr.gov.br/iss/nfse.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://isscuritiba.curitiba.pr.gov.br/iss/nfse.xsd">
<LoteRps><ListaRps><Rps>
<InfRps id="1">xxx</InfRps>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#1">
<Transforms><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>fRXn427d6rObJ0udybG5aY5E6n4=</DigestValue>
</Reference></SignedInfo>
<SignatureValue>...</SignatureValue>
<KeyInfo><X509Data><X509Certificate>...</X509Certificate></X509Data></KeyInfo>
</Signature>
</Rps></ListaRps> </LoteRps> </EnviarLoteRpsEnvio></RecepcionarLoteRps>
Whether this form of defining the XML, or another, is acceptable in your environment or application(s) I cannot say.
Finally, in case you don't know, SHA1 was broken for collision in 2017 and most competent security authorities no longer allow signatures using it (directly or indirectly). (For SSL/TLS certificates including websites like this one SHA1 had already been forbidden since 2014-2015.) But that's offtopic for SO.
Upvotes: 0