csoapxmlsec

Sign part of XML using xmlsec


I’m trying to sign a simple soap message. The message looks like this:

<?xml version="1.0" encoding="utf-8"?>
<e:Envelope xmlns:e="http://schemas.xmlsoap.org/soap/envelope/">
    <e:Header />
    <e:Body>
         <Data>someData</Data>
    </e:Body>
</e:Envelope>

By no means I’m an expert in network and security stuff. As far as I understand, the result should look something like:

<?xml version="1.0" encoding="utf-8"?>
<e:Envelope xmlns:e="http://schemas.xmlsoap.org/soap/envelope/">
<e:Header>
         <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
         .... 
                 <Reference URI="#signedContent">
         ....
        </Signature>
</e:Header>
<e:Body id="signedContent">
             <Data>someData</Data>
</e:Body>
</e:Envelope>

So to my understanding the signature goes into the header, and the signature reference points to the body. I’ve put the signature node into the header by calling

xmlAddChild(xmlDocGetRootElement(doc)->children, signNode);

However I don’t know how to tell the library to sign the body. Do I have to place the id attribute in the body by myself, or should I let the library do it? The library isn’t very well documented (or maybe the functions make sense, but not for someone like me). Which functions do I need to call next (I’m using the x509 example as a basis: https://www.aleksey.com/xmlsec/api/xmlsec-examples-sign-x509.html#xmlsec-example-sign3). Probably I should create the reference node like this:

xmlNodePtr refNode = xmlSecTmplSignatureAddReference(signNode, xmlSecTransformSha256Id,
       NULL, (const xmlChar*)"signedContent”, NULL);

…but I’m not very sure at all. I see that there are other functions like xmlSecTmplReferenceAddTransform. The examples on the xmlsec webpage call this with xmlSecTransformEnvelopedId, but I don’t need enveloped, perhaps I should call it with something else?.


Solution

  • This is exactly what happens, when you don't read the FAQ of the library. First of all - no need to dynamically create a signature template. If you build your SOAP manually like I do, it's more than enough to make it a literal like this:

    <?xml version="1.0" encoding="utf-8"?>
    <e:Envelope xmlns:e="http://schemas.xmlsoap.org/soap/envelope/">
    <e:Header><Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
    <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
    <Reference URI="#signedContent">
    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
    <DigestValue></DigestValue>
    </Reference>
    </SignedInfo>
    <SignatureValue></SignatureValue>
    <KeyInfo>
    <X509Data>
    <X509Certificate></X509Certificate>
    </X509Data>
    </KeyInfo>
    </Signature>
    </e:Header>
    <e:Body id="signedContent"><Data>SomeData<Data>
    </e:Body>
    </e:Envelope>
    

    The next step is to pass it to xmlsec. As you can see, digestValue, signedValue and x509 are left empty. I use C++, so I pass it as string, with my x509 certificate and private key from the HSM (I access it using libp11):

    std::string XmlSigner::signSoapTemplate(const std::string& document, evp_pkey_st* prvKey, const std::string& x509)
    {
    
        if (!init && !initialize()) {
            return "xmlsec could not be initialized";
        }
        init = true;
       
        if (prvKey == nullptr) return{};
        if (x509.empty()) return{};
    
        std::string result;
    
        xmlChar* signedBuffer{ nullptr };
        int signedLength{ 0 };
    
        /* load doc file */
        xmlDocPtr doc = xmlParseMemory(document.data(), document.size());
        if ((doc == NULL) || (xmlDocGetRootElement(doc) == NULL)) {
            return "Error: unable to parse file";
        }
        
        xmlAttrPtr attr = xmlDocGetRootElement(doc)->children->next->properties;
    //THIS IS THE MOST IMPORTANT LINE:
        xmlAddID(NULL, doc, (const xmlChar*)"signedContent", attr);
    
        xmlNodePtr signNode = xmlSecFindNode(xmlDocGetRootElement(doc), xmlSecNodeSignature, xmlSecDSigNs);
        if (signNode == NULL) {
            return "Error: start node not found";
        }
    
        /* create signature context */
        xmlSecDSigCtxPtr dsigCtx = xmlSecDSigCtxCreate(NULL);
        if (dsigCtx == NULL) {
            return "Error: failed to create signature context\n";
        }
    
        dsigCtx->enabledReferenceUris = xmlSecTransformUriTypeSameDocument;
    
        dsigCtx->signKey = xmlSecKeyCreate();
    
        dsigCtx->signKey->value = xmlSecOpenSSLEvpKeyAdopt(prvKey);
    
        if (
            xmlSecOpenSSLAppKeyCertLoadMemory(
                dsigCtx->signKey,
                reinterpret_cast<const unsigned char*>(x509.data()),
                x509.size(),
                xmlSecKeyDataFormatPem) < 0
            )
    
        { return "Failed to load certificate"; }
        
    
         /* sign the template */
        if (xmlSecDSigCtxSign(dsigCtx, signNode) < 0) {
            return "Error: signature failed\n";
    
        }
    
        /* dumps the memory to the buffer */
        xmlDocDumpMemory(doc, &signedBuffer, &signedLength);
    
        result = std::string(reinterpret_cast<char*>(signedBuffer), signedLength);
    
        /* success */
    
    
        /* cleanup */
        if (dsigCtx != NULL) {
            xmlSecDSigCtxDestroy(dsigCtx);
        }
    
        if (doc != NULL) {
            xmlFreeDoc(doc);
        }
    
        return result;
    }
    

    The error handling shouldn't be done with return strings, but again - that's just an example.