Sample code for 30+ languages & platforms
PureBasic

Sign SOAP XML for New Zealand Customs Service

See more XAdES Examples

Demonstrates how to create an XAdES signed SOAP XML pertaining to the New Zealand Customs Service.

Note: This example requires Chilkat v9.5.0.96 or later.

Chilkat PureBasic Downloads

PureBasic
IncludeFile "CkXmlDSigGen.pb"
IncludeFile "CkXml.pb"
IncludeFile "CkStringBuilder.pb"
IncludeFile "CkDateTime.pb"
IncludeFile "CkCert.pb"

Procedure ChilkatExample()

    success.i = 0

    ; This example assumes the Chilkat API to have been previously unlocked.
    ; See Global Unlock Sample for sample code.

    success = 1

    ; Create the following XML to be signed:

    ; <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    ;     xmlns:v1="http://customs.govt.nz/jbms/msggate/reqresp/v1">
    ;     <soapenv:Header>
    ;         <wsse:Security soapenv:mustUnderstand="1"
    ;             xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
    ;             xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    ;             <wsu:Timestamp wsu:Id="TS-037E78514E9B9132CB16817563559151">
    ;                 <wsu:Created>2023-04-17T18:32:35.913Z</wsu:Created>
    ;                 <wsu:Expires>2023-04-17T19:32:35.913Z</wsu:Expires>
    ;             </wsu:Timestamp>
    ;         </wsse:Security>
    ;     </soapenv:Header>
    ;     <soapenv:Body wsu:Id="id-8"
    ;         xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    ;         <v1:RequestResponse>
    ;             <v1:Submitter>TEST1234</v1:Submitter>
    ;             <v1:MailboxMsgId>999999</v1:MailboxMsgId>
    ;         </v1:RequestResponse>
    ;     </soapenv:Body>
    ; </soapenv:Envelope>

    ; Create a random ID like this: TS-037E78514E9B9132CB16817563559151
    tsId.i = CkStringBuilder::ckCreate()
    If tsId.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkStringBuilder::ckAppend(tsId,"TS-")
    CkStringBuilder::ckAppendRandom(tsId,16,"hex")

    ; STR-037E78514E9B9132CB16817563559614
    strId.i = CkStringBuilder::ckCreate()
    If strId.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkStringBuilder::ckAppend(strId,"STR-")
    CkStringBuilder::ckAppendRandom(strId,16,"hex")

    ; KI-037E78514E9B9132CB16817563559583
    keyInfoId.i = CkStringBuilder::ckCreate()
    If keyInfoId.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkStringBuilder::ckAppend(keyInfoId,"KI-")
    CkStringBuilder::ckAppendRandom(keyInfoId,16,"hex")

    ; Create a date/time for the current time with this format:  2023-04-17T18:32:35.913Z
    dt.i = CkDateTime::ckCreate()
    If dt.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkDateTime::ckSetFromCurrentSystemTime(dt)

    sbNow.i = CkStringBuilder::ckCreate()
    If sbNow.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkStringBuilder::ckAppend(sbNow,CkDateTime::ckGetAsTimestamp(dt,0))
    ; If we really need the milliseconds, we can replace the "Z" with ".000Z"
    ; The server will also likely accept a timestamp without milliseconds, such as 2023-04-17T18:32:35Z
    n.i = CkStringBuilder::ckReplace(sbNow,"Z",".000Z")

    sbNowPlusOneHour.i = CkStringBuilder::ckCreate()
    If sbNowPlusOneHour.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkDateTime::ckAddSeconds(dt,3600)
    CkStringBuilder::ckAppend(sbNowPlusOneHour,CkDateTime::ckGetAsTimestamp(dt,0))
    n = CkStringBuilder::ckReplace(sbNowPlusOneHour,"Z",".000Z")

    xmlToSign.i = CkXml::ckCreate()
    If xmlToSign.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkXml::setCkTag(xmlToSign, "soapenv:Envelope")
    CkXml::ckAddAttribute(xmlToSign,"xmlns:soapenv","http://schemas.xmlsoap.org/soap/envelope/")
    CkXml::ckAddAttribute(xmlToSign,"xmlns:v1","http://customs.govt.nz/jbms/msggate/reqresp/v1")
    CkXml::ckUpdateAttrAt(xmlToSign,"soapenv:Header|wsse:Security",1,"soapenv:mustUnderstand","1")
    CkXml::ckUpdateAttrAt(xmlToSign,"soapenv:Header|wsse:Security",1,"xmlns:wsse","http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd")
    CkXml::ckUpdateAttrAt(xmlToSign,"soapenv:Header|wsse:Security",1,"xmlns:wsu","http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd")
    CkXml::ckUpdateAttrAt(xmlToSign,"soapenv:Header|wsse:Security|wsu:Timestamp",1,"wsu:Id",CkStringBuilder::ckGetAsString(tsId))
    CkXml::ckUpdateChildContent(xmlToSign,"soapenv:Header|wsse:Security|wsu:Timestamp|wsu:Created",CkStringBuilder::ckGetAsString(sbNow))
    CkXml::ckUpdateChildContent(xmlToSign,"soapenv:Header|wsse:Security|wsu:Timestamp|wsu:Expires",CkStringBuilder::ckGetAsString(sbNowPlusOneHour))
    CkXml::ckUpdateAttrAt(xmlToSign,"soapenv:Body",1,"wsu:Id","id-8")
    CkXml::ckUpdateAttrAt(xmlToSign,"soapenv:Body",1,"xmlns:wsu","http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd")
    CkXml::ckUpdateChildContent(xmlToSign,"soapenv:Body|v1:RequestResponse|v1:Submitter","TEST1234")
    CkXml::ckUpdateChildContent(xmlToSign,"soapenv:Body|v1:RequestResponse|v1:MailboxMsgId","999999")

    gen.i = CkXmlDSigGen::ckCreate()
    If gen.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkXmlDSigGen::setCkSigLocation(gen, "soapenv:Envelope|soapenv:Header|wsse:Security")
    CkXmlDSigGen::setCkSigLocationMod(gen, 0)
    CkXmlDSigGen::setCkSigId(gen, "SIG-037E78514E9B9132CB16817563559695")
    CkXmlDSigGen::setCkSigNamespacePrefix(gen, "ds")
    CkXmlDSigGen::setCkSigNamespaceUri(gen, "http://www.w3.org/2000/09/xmldsig#")
    CkXmlDSigGen::setCkSignedInfoPrefixList(gen, "soapenv v1")
    CkXmlDSigGen::setCkIncNamespacePrefix(gen, "ec")
    CkXmlDSigGen::setCkIncNamespaceUri(gen, "http://www.w3.org/2001/10/xml-exc-c14n#")
    CkXmlDSigGen::setCkSignedInfoCanonAlg(gen, "EXCL_C14N")
    CkXmlDSigGen::setCkSignedInfoDigestMethod(gen, "sha256")

    ; Set the KeyInfoId before adding references..
    CkXmlDSigGen::setCkKeyInfoId(gen, CkStringBuilder::ckGetAsString(keyInfoId))

    ; -------- Reference 1 --------
    xml1.i = CkXml::ckCreate()
    If xml1.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkXml::setCkTag(xml1, "ds:Transforms")
    CkXml::ckUpdateAttrAt(xml1,"ds:Transform",1,"Algorithm","http://www.w3.org/2001/10/xml-exc-c14n#")
    CkXml::ckUpdateAttrAt(xml1,"ds:Transform|ec:InclusiveNamespaces",1,"PrefixList","wsse soapenv v1")
    CkXml::ckUpdateAttrAt(xml1,"ds:Transform|ec:InclusiveNamespaces",1,"xmlns:ec","http://www.w3.org/2001/10/xml-exc-c14n#")

    CkXmlDSigGen::ckAddSameDocRef2(gen,CkStringBuilder::ckGetAsString(tsId),"sha256",xml1,"")

    ; -------- Reference 2 --------
    xml2.i = CkXml::ckCreate()
    If xml2.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkXml::setCkTag(xml2, "ds:Transforms")
    CkXml::ckUpdateAttrAt(xml2,"ds:Transform",1,"Algorithm","http://www.w3.org/2001/10/xml-exc-c14n#")
    CkXml::ckUpdateAttrAt(xml2,"ds:Transform|ec:InclusiveNamespaces",1,"PrefixList","v1")
    CkXml::ckUpdateAttrAt(xml2,"ds:Transform|ec:InclusiveNamespaces",1,"xmlns:ec","http://www.w3.org/2001/10/xml-exc-c14n#")

    CkXmlDSigGen::ckAddSameDocRef2(gen,"id-8","sha256",xml2,"")

    ; Provide a certificate + private key. (PFX password is test123)
    cert.i = CkCert::ckCreate()
    If cert.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    success = CkCert::ckLoadPfxFile(cert,"qa_data/pfx/cert_test123.pfx","test123")
    If success <> 1
        Debug CkCert::ckLastErrorText(cert)
        CkStringBuilder::ckDispose(tsId)
        CkStringBuilder::ckDispose(strId)
        CkStringBuilder::ckDispose(keyInfoId)
        CkDateTime::ckDispose(dt)
        CkStringBuilder::ckDispose(sbNow)
        CkStringBuilder::ckDispose(sbNowPlusOneHour)
        CkXml::ckDispose(xmlToSign)
        CkXmlDSigGen::ckDispose(gen)
        CkXml::ckDispose(xml1)
        CkXml::ckDispose(xml2)
        CkCert::ckDispose(cert)
        ProcedureReturn
    EndIf

    CkXmlDSigGen::ckSetX509Cert(gen,cert,1)

    CkXmlDSigGen::setCkKeyInfoType(gen, "Custom")

    ; Create the custom KeyInfo XML..
    xmlCustomKeyInfo.i = CkXml::ckCreate()
    If xmlCustomKeyInfo.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkXml::setCkTag(xmlCustomKeyInfo, "wsse:SecurityTokenReference")
    CkXml::ckAddAttribute(xmlCustomKeyInfo,"wsu:Id",CkStringBuilder::ckGetAsString(strId))
    CkXml::ckUpdateAttrAt(xmlCustomKeyInfo,"wsse:KeyIdentifier",1,"EncodingType","http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary")
    CkXml::ckUpdateAttrAt(xmlCustomKeyInfo,"wsse:KeyIdentifier",1,"ValueType","http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3")
    ; Insert the single-line base64 of the signing certificate's DER
    CkCert::setCkUncommonOptions(cert, "Base64CertNoCRLF")
    CkXml::ckUpdateChildContent(xmlCustomKeyInfo,"wsse:KeyIdentifier",CkCert::ckGetEncoded(cert))

    CkXml::setCkEmitXmlDecl(xmlCustomKeyInfo, 0)
    CkXmlDSigGen::setCkCustomKeyInfoXml(gen, CkXml::ckGetXml(xmlCustomKeyInfo))

    ; Load XML to be signed...
    sbXml.i = CkStringBuilder::ckCreate()
    If sbXml.i = 0
        Debug "Failed to create object."
        ProcedureReturn
    EndIf

    CkXml::ckGetXmlSb(xmlToSign,sbXml)

    CkXmlDSigGen::setCkBehaviors(gen, "IndentedSignature")

    ; Sign the XML...
    CkXmlDSigGen::setCkVerboseLogging(gen, 1)
    success = CkXmlDSigGen::ckCreateXmlDSigSb(gen,sbXml)
    If success <> 1
        Debug CkXmlDSigGen::ckLastErrorText(gen)
        CkStringBuilder::ckDispose(tsId)
        CkStringBuilder::ckDispose(strId)
        CkStringBuilder::ckDispose(keyInfoId)
        CkDateTime::ckDispose(dt)
        CkStringBuilder::ckDispose(sbNow)
        CkStringBuilder::ckDispose(sbNowPlusOneHour)
        CkXml::ckDispose(xmlToSign)
        CkXmlDSigGen::ckDispose(gen)
        CkXml::ckDispose(xml1)
        CkXml::ckDispose(xml2)
        CkCert::ckDispose(cert)
        CkXml::ckDispose(xmlCustomKeyInfo)
        CkStringBuilder::ckDispose(sbXml)
        ProcedureReturn
    EndIf

    ; Save the signed XML to a file.
    success = CkStringBuilder::ckWriteFile(sbXml,"c:/temp/qa_output/signedXml.xml","utf-8",0)

    Debug CkStringBuilder::ckGetAsString(sbXml)


    CkStringBuilder::ckDispose(tsId)
    CkStringBuilder::ckDispose(strId)
    CkStringBuilder::ckDispose(keyInfoId)
    CkDateTime::ckDispose(dt)
    CkStringBuilder::ckDispose(sbNow)
    CkStringBuilder::ckDispose(sbNowPlusOneHour)
    CkXml::ckDispose(xmlToSign)
    CkXmlDSigGen::ckDispose(gen)
    CkXml::ckDispose(xml1)
    CkXml::ckDispose(xml2)
    CkCert::ckDispose(cert)
    CkXml::ckDispose(xmlCustomKeyInfo)
    CkStringBuilder::ckDispose(sbXml)


    ProcedureReturn
EndProcedure