TarqTeles
TarqTeles

Reputation: 61

What exactly to digest and sign on XMLDSIG and how? (or, OSX-native client mismatches XMLDSIG calculated on server)

I'm trying to sign a xml document following XMLDSIG specification with an enveloped signature, sha1 digest and rss-sha1 signature, and the server keeps returning a "297 - Rejection: Signature does not match calculated [result]" ("297 - Rejeicao: Assinatura difere do calculado" in Brazilian Portuguese original)

My client application needs to be Mac OS X native (so Objective-C and Swift). I'm adhering to Apple's CryptoCompatibility guidelines and using Security.framework's SecSignTransform and CommonCrypto's CC_SHA1.

Here is the XML I'm trying to XMLDSIG (did not PrettyPrint and omitted terms to save space):

<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe Id="NFe351503...1455341071" versao="2.00"><ide><cUF>35</cUF><cNF>45534107</cNF><natOp>VENDA</natOp><indPag>1</indPag><mod>55</mod>
...
</infNFe><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI="#NFe351503...1455341071"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>H4l0eMA6H4ndKzY3ftwlsKpeX58=</DigestValue></Reference></SignedInfo><SignatureValue>QYMVPWvZOeF4XgorObl33Tm9DiZEW4N7zuuAbt9Jjop79V41SNAIO5qIXe06cLiJACghi1X+p3pROE3P/E/lhPhwGmA3G26Jm5hZqsGhURS1osHDNKDWARBpi+musgi5naHm4tKqlKKIKqARljyXyYRRVaoxOSrC3vmxPx2ClwwTrlgnqtDTODQU0yNN4OUXTxWAMYPm8rc2rO6OUohTK+eXE3mN5vgCB6GLMWj0Cp2k6N21264WNv/P+L45kHUllFnV+ByMshXFYzySvthujlq/4ClSG+1xOFYMATn1F6qvklMDXy7bS+Dqcp635ZFxfD97gTDriFUYH0+nEe95zw==</SignatureValue><KeyInfo><X509Data><X509Certificate>...</X509Certificate></X509Data></KeyInfo></Signature></NFe>

Unfortunately, since both .Net and Java offer very high level support for XMLDSIG, detailed information about which parts of the XML to get, what to retain and what to remove, are scant on the internet. Apart from W3.org's own specs, which is pretty dry, the only in-depth explanation I found was: http://www.di-mgt.com.au/xmldsig2.html

I'm not sure if the mismatch is in the sha1 digest or the rsa-sha1 signature, the return code is unclear. Also, I don't know if I'm using the wrong input, or if Mac OS X libraries I'm using are not compatible with the server (which is .Net based).

Here is the code for the digest. Please notice it uses a same-document URI reference:

// Imports XML data from XML file
var xmlStr: String? = File.open(documentPath)

// gets Id (formated "NFe" + 44x[0-9]) to create SignedInfo reference URI
let myId = getNFeId(xmlStr!)

// creates a XML Document using String xmlStr and canonicalize "c14n"
var xmlDocument = XMLSupportClass.createXMLDocument(xmlStr!)

let canonicalXmlStr = xmlDocument.canonicalXMLStringPreservingComments(false)
var stringToDigest = ""

// retrieves element referenced by URI (#myId) to create digest
if (xmlDocument.rootElement() != nil) {
    let xmlRoot: NSXMLElement = xmlDocument.rootElement()!
//    let myURI = "#" + myId
//    let nodesToTest: [NSXMLElement] = xmlRoot.elementsForLocalName("NFe", URI: myURI) as [NSXMLElement]
//    let nodesToTest2: [NSXMLElement] = xmlRoot.elementsForName("infNFe") as [NSXMLElement]
    let myXPath: String = "//*[@Id=\'" + myId + "\']"
    let nodesToDigest = xmlRoot.nodesForXPath(myXPath, error: &xpathError) as [NSXMLElement]
    if nodesToDigest.count > 0 {
        stringToDigest = nodesToDigest[0].canonicalXMLStringPreservingComments(false)
    } else { println(xpathError) }
} else {
    println("I'm root-less!!")
}

// creates the digest using CryptoCompatibility
digestData = stringToDigest.sha1()
let digestDataAsString: String =  digestData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.EncodingEndLineWithLineFeed)

Extra methods used in code:

func getNFeId(xml: String) -> String {
    // mas como extrair o atributo Id do elemento <infNFe>
    var myError: NSError?
    let root = XMLSupportClass.createXMLDocument(xml).rootElement()! as NSXMLElement
    let infNodes = root.elementsForName("infNFe") as [NSXMLElement]
    if infNodes.count > 0 {
        let idNode = infNodes[0].attributeForName("Id")! as NSXMLNode
        let myId = idNode.objectValue as String
        println(myId)
        return myId
    } else {
        println("error extracting NFeId")
        return "error extracting NFeId"
    }    
}

// SHA-1 Digest from CryptoCompatibility returning a Hex String
extension String {
    func sha1() -> String {
        let data = self.dataUsingEncoding(NSUTF8StringEncoding)!
        var digest = [UInt8](count:Int(CC_SHA1_DIGEST_LENGTH), repeatedValue: 0)
        CC_SHA1(data.bytes, CC_LONG(data.length), &digest)
        let output = NSMutableString(capacity: Int(CC_SHA1_DIGEST_LENGTH))
        for byte in digest {
            output.appendFormat("%02x", byte)
        }
        return output
    }
}

After calculating the digest, it is inserted in a pre-formatted Signature XML String, to create a XML Document, and then the SignedInfo node is extracted and used to generate the SignatureValue:

// pre-formatted XML String for Signature node, leaving SignatureValue empty for filling in later
let xmlAssinatura = "<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=\"#\(myId)\"><Transforms><Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/><Transform Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/></Transforms><DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>\(digestDataAsString)</DigestValue></Reference></SignedInfo><SignatureValue></SignatureValue><KeyInfo><X509Data><X509Certificate>\(certDataAsString)</X509Certificate></X509Data></KeyInfo></Signature>"

// tranforms xmlAssinatura String in NSXMLDocument
var xmlAssinaturaDocument = XMLSupportClass.createXMLDocument(xmlAssinatura)
let signatureNode = xmlAssinaturaDocument.rootElement()!

// and retrieves SignedInfo node, converts to NSData for signing
let xmlSignedInfoElement = (signatureNode.elementsForName("SignedInfo") as [NSXMLElement])[0]


    **// ====> the line below was the problem!!!**
/*let signedInfoData = XMLSupportClass.createXMLDocument(xmlSignedInfoElement.canonicalXMLStringPreservingComments(false)).XMLData */

    **// ====> and this is the fix:**
let signedInfoData = (XMLSupportClass.createXMLDocument(xmlSignedInfoElement.canonicalXMLStringPreservingComments(false), withTidyXML:true).rootElement()!.XMLString).dataUsingEncoding(NSUTF8StringEncoding)!



// creates SecTransform object
    signer = SecSignTransformCreate(priKey, &error).takeRetainedValue()
    if error != nil { print("signer transform creation error: ") ; println(error) }


// signer to use SHA1 digest method and use signedInfoData as input
SecTransformSetAttribute(signer, kSecDigestTypeAttribute, kSecDigestSHA1, &error)
if error != nil { print("verifier digest attribute setting error: ") ; println(error) }
SecTransformSetAttribute(signer, kSecTransformInputAttributeName, signedInfoData, &error)
if error != nil { print("signer attribute setting error: ") ; println(error) }

// executes the transform
signedData = (SecTransformExecute(signer, &error) as NSData)
let signedDataAsString = signedData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.EncodingEndLineWithLineFeed)
if error != nil { print("signer execute error: ") ; println(error) }

// inserts generated signedDataAsString in <SignatureValue> node
let signatureValueElements = signatureNode.elementsForName("SignatureValue") as [NSXMLElement]
if signatureValueElements.count > 0 { signatureValueElements[0].setStringValue(signedDataAsString, resolvingEntities: false) } else { println(signatureValueElements) }
signatureNode.detach()
xmlAssinaturaDocument = nil

// then replaces <Signature> placeholder node in xmlDocument
if (xmlDocument.rootElement() != nil) {
    let xmlRoot: NSXMLElement = xmlDocument.rootElement()!
    let signatureNodePlaceholder: NSXMLElement = (xmlRoot.elementsForName("Signature") as [NSXMLElement])[0]
    let signatureNodeIndex = signatureNodePlaceholder.index
    xmlRoot.replaceChildAtIndex(signatureNodeIndex, withNode: signatureNode)

    // and creates xmlDocument canonicalized String
    xmlStr = xmlRoot.canonicalXMLStringPreservingComments(false)
}

As far as I can see, everything is correct and compliant with W3.org's specifications for XMLDSIG. Still, the server always rejects the generated XML.

I'm at wits end. Any help and wisdom will be highly appreciated!!

Upvotes: 3

Views: 1403

Answers (1)

TarqTeles
TarqTeles

Reputation: 61

Found it!!

Here is what I was doing wrong:

My code converted the NSXMLDocument straight into NSData, but this meant the XML type <?xml version="1.0" encoding="UTF-8"?> was being include in the beginning.

When after some fiddling to find the right expression, I replaced this code:

let signedInfoData = XMLSupportClass.createXMLDocument(xmlSignedInfoElement.canonicalXMLStringPreservingComments(false)).XMLData

with this code:

let signedInfoData = (XMLSupportClass.createXMLDocument(xmlSignedInfoElement.canonicalXMLStringPreservingComments(false), withTidyXML:true).rootElement()!.XMLString).dataUsingEncoding(NSUTF8StringEncoding)!

it worked!!

Probably because the missing namespace <SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> was included, but not the XML type declaration...

As warned, XMLDSIG is extremely detail-oriented. Too bad there is no high-level support in Objective-C or Swift, but I guess Apple considers it too dated.

PS, circa 2023: XMLSupport was a simple bridge encapsulation NSXMLDocument init, which was at the time unavailable in Swift. You can now simply use the following code to create from your XML in string format:

var xmlDocument: XMLDocument?
do {
    xmlDocument = try XMLDocument(xmlString: xmlStr!, options: .nodePreserveWhitespace)
} catch {
    // XMLDocument creation without tidyXML failed... created doc may differ from original
    xmlDocument = try XMLDocument(xmlString: xmlStr!, options: .documentTidyXML)
}

Which will produce an XML document with the right format. Then you can extract the root element and transform it in data like this:

var stringToDigest = ""
// extract from xmlDocument the URI (#myId) element to generate digest
if (xmlDocument!.rootElement() != nil && myId != "") {
    let xmlRoot: XMLElement = xmlDocument!.rootElement()!
    let myXPath: String = "//*[@Id=\'" + myId + "\']"
    let nodesToDigest = (try! xmlRoot.nodes(forXPath: myXPath)) as! [XMLElement]
    if nodesToDigest.count > 0 {
        stringToDigest = nodesToDigest[0].canonicalXMLStringPreservingComments(false)
        // doing great! you now have the URI element needed for XMLDSIG (XML digital signature)
    } else {
        // something wrong, no nodes found for myId
    }
} else {
    // no root or myId URI does not exist
}

Finally, you can proceed to create the digest that will be part of the signed element to create the digital signature itself (in this case, using outdated SHA1 as that was the spec):

var error: Unmanaged<CFError>?
digest = SecDigestTransformCreate(kSecDigestSHA1, 0, &error)
if error != nil { print("digest transform create error == nil") }
SecTransformSetAttribute(digest, kSecTransformInputAttributeName, stringToDigest.data(using: String.Encoding.utf8)! as CFTypeRef, &error)
if error != nil { print("digest attribute setting error == nil") }
digestData = (SecTransformExecute(digest, &error) as! Data)
let digestDataAsString: String =  digestData.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithLineFeed)

To finish up, in my case (do not know if this is general for .Net), a new xML document was created like above for the following string (note insertions of the digest above, myId, and digital certificate):

let xmlAssinatura = "<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=\"#\(myId)\"><Transforms><Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/><Transform Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"/></Transforms><DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/><DigestValue>\(digestDataAsString)</DigestValue></Reference></SignedInfo><SignatureValue></SignatureValue><KeyInfo><X509Data><X509Certificate>\(certDataAsString)</X509Certificate></X509Data></KeyInfo></Signature>"

Then again, extract the root element and transform it in data, then sign the data (finally!) using the certificate private key, convert the result back into string using .base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithLineFeed) and insert the resulting use 64 string in the node above.

There are further steps needed for the Brazilian digital fiscal invoice, yet the above steps should be all needed for most digitally signed XMLs compatible with Microsoft .Net

Upvotes: 1

Related Questions