xml-crypto

repository·master·Indexed 42 days ago

https://github.com/node-saml/xml-crypto

An XML digital signature and encryption library for Node.js (v6.0.0) that allows developers to sign, verify, and transform XML documents according to W3C standards. It supports various canonicalization, transformation, and hashing algorithms, including RSA-SHA256 and SHA512. The library provides the SignedXml class for managing the signing and verification workflow, including support for HMAC-SHA1 and implicit transforms.

Tokens
13.6K
Snippets
37
Records
52
Agent score
53%

What's inside xml-crypto

  1. Handle X.509 and PEM key formats

    master

    The library uses Node.js's crypto module, so it supports PEM-encoded certificates and private keys.

    For signing, use a private key PEM:

    -----BEGIN PRIVATE KEY-----
    ...
    -----END PRIVATE KEY-----

    For verification, use a public certificate PEM:

    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----

    PEM Parsing Utilities

    • toPem(value, label?): Returns canonical PEM (newline endings, 64-character line width). If value is a Buffer starting with an encapsulation boundary, it is read as a PEM file; otherwise, it is treated as raw DER. If value is base64 text, it is treated as a string.
    • pemToDer(pem): Returns the decoded bytes of the single message contained in the value.
    • pemCertificates(pem): Extracts and returns the base64 of each CERTIFICATE message found in the value, ignoring other labels (like private keys).
  2. Implement custom algorithms in SignedXml

    master

    You can extend SignedXml by implementing custom classes for digests, signatures, transformations, or canonicalization. Once implemented, you must register them on the SignedXml instance using the internal registry objects: CanonicalizationAlgorithms, HashAlgorithms, or SignatureAlgorithms.

    // 1. Define custom classes
    class MyDigest {
      getHash(xml) { return "base64-hash"; }
      getAlgorithmName() { return "http://myDigestAlgorithm"; }
    }
    
    class MySignatureAlgorithm {
      getSignature(signedInfo, privateKey) { return "base64-signature"; }
      getAlgorithmName() { return "http://mySigningAlgorithm"; }
    }
    
    // 2. Register and use
    const sig = new SignedXml({
      signatureAlgorithm: "http://mySigningAlgorithm",
      canonicalizationAlgorithm: "http://myCanonicalization",
    });
    
    sig.SignatureAlgorithms["http://mySigningAlgorithm"] = MySignatureAlgorithm;
    sig.HashAlgorithms["http://myDigestAlgorithm"] = MyDigest;
    
    sig.addReference({
      xpath: "//path",
      digestAlgorithm: "http://myDigestAlgorithm",
      transforms: ["http://myTransformation"]
    });
    
    sig.computeSignature(xml);
  3. How transforms and canonicalization interact

    master

    The order and type of transforms significantly affect the resulting XML processing:

    Transforms ending in a DOM node

    If the final transform in a Reference returns a DOM Node, it is converted to octets using inclusive canonicalization. This applies to SignedInfo canonicalization algorithms that return a Node as well.

    Transforms following a canonicalization

    When a transform returns a string and is followed by another transform, the string is parsed into a new document before the next transform is applied. This leads to several behaviors:

    • Enveloped Signature: If enveloped-signature follows a canonicalization, it removes the Signature element (including any nested within the referenced element).
    • Comment Handling: A #WithComments canonicalization followed by enveloped-signature results in canonicalized XML without comments.
    • Namespace Handling: Exclusive canonicalization following inclusive canonicalization will omit inherited namespace declarations that the referenced element does not explicitly use.
    • Custom Transforms: A custom transform placed after a canonicalization will receive the parsed document as input.
    • Error Handling: A transform will throw an error if the string returned by the preceding transform is not well-formed XML.
  4. How comments are handled in referenced content

    master

    When a Reference uses an empty URI or a fragment identifier (e.g., #item), the library follows the XMLDSig same-document reference specification:

    1. Comment Removal: Comments are removed from the referenced content before any transforms are applied.
    2. Signature Impact: Because comments are removed, they are not part of the signed content. Adding, removing, or changing a comment in that section will not invalidate the signature.
    3. Data Access: To read the actual signed content (which excludes these comments), use getSignedReferences() instead of getReferences().
    4. Transforms: Custom transforms in these references will not receive comment nodes. However, a #WithComments CanonicalizationMethod will still sign the comments located inside the SignedInfo element itself.
  5. Sign XML documents with SignedXml

    master

    To sign an XML document, instantiate the SignedXml class with a configuration object. You must provide a privateKey and specify the signatureAlgorithm and canonicalizationAlgorithm. You can optionally provide a publicCert to include in the <KeyInfo> element of the resulting signature.

    Constructor Options:

    • privateKey (required): A Buffer or PEM-encoded String containing your private key.
    • publicCert (optional): A Buffer or PEM-encoded String containing your public key/certificate.
    • signatureAlgorithm (required): A supported signature algorithm URI (e.g., http://www.w3.org/2001/04/xmldsig-more#rsa-sha256).
    • canonicalizationAlgorithm (required): A supported canonicalization algorithm URI (e.g., http://www.w3.org/2001/10/xml-exc-c14n#).

    After instantiation, use addReference to define what part of the XML is being signed, then call computeSignature(xml) to generate the signature.

    const { SignedXml } = require("xml-crypto");
    const fs = require("fs");
    
    const xml = "<library><book><name>Harry Potter</name></book></library>";
    
    const sig = new SignedXml({
      privateKey: fs.readFileSync("client.pem"),
      canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
      signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    });
    
    sig.addReference({
      xpath: "//*[local-name(.)='book']",
      digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
      transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
    });
    
    sig.computeSignature(xml);
    fs.writeFileSync("signed.xml", sig.getSignedXml());
  6. Convert .pfx certificates to .pem

    master

    If you have a .pfx certificate, you can convert it to .pem using OpenSSL:

    openssl pkcs12 -in c:\certs\yourcert.pfx -out c:\certs\cag.pem
    • For signing: Use the resulting .pem file directly.
    • For validation: Open the .pem file in a text editor, copy everything from -----BEGIN CERTIFICATE----- to -----END CERTIFICATE----- (inclusive), and save it as a new .pem file.
  7. Verify XML documents with SignedXml

    master

    Verifying a signature involves two steps: loading the signature element and then checking it against the full XML document.

    Important Security Note: To prevent attacks, do not use the data parsed from the original document to perform your business logic. Instead, verify the signature, then use sig.getSignedReferences() to retrieve the actual bytes that were signed. Only use the data extracted from these authenticated bytes.

    Constructor Options for Verification:

    • publicCert (optional): A string (or multiple PEM strings) or a Buffer containing the certificate. Used if getCertFromKeyInfo() returns null.
    • privateKey (optional): A string or Buffer used for verifying symmetrical signatures (HMAC).
    • getCertFromKeyInfo (optional): A function to customize how the certificate is determined from the XML. If it returns null, the constructor falls back to publicCert, then privateKey.

    Workflow:

    1. Parse the XML using a DOM parser (e.g., @xmldom/xmldom).
    2. Locate the <Signature> element using XPath.
    3. Initialize SignedXml and call loadSignature(signatureElement).
    4. Call checkSignature(xml).
    5. If successful, retrieve authenticated data via sig.getSignedReferences().
    const { DOMParser } = require("@xmldom/xmldom");
    const xpath = require("xpath");
    const { SignedXml } = require("xml-crypto");
    const fs = require("fs");
    
    // 1. Setup dependencies: npm install @xmldom/xmldom xpath
    const xml = fs.readFileSync("signed.xml", "utf8");
    const doc = new DOMParser().parseFromString(xml, "text/xml");
    
    // 2. Find the signature element
    const signature = xpath.select1(
      "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
      doc,
    );
    
    // 3. Verify
    const sig = new SignedXml({ publicCert: fs.readFileSync("client_public.pem") });
    sig.loadSignature(signature);
    const res = sig.checkSignature(xml);
    
    if (!res) {
      throw new Error("Invalid signature");
    }
    
    // 4. SECURELY extract authenticated data
    const signedBytes = sig.getSignedReferences();
    const authenticatedDoc = new DOMParser().parseFromString(signedBytes[0], "text/xml");
    // Now it is safe to use authenticatedDoc for parsing business logic
  8. Sign XML documents with SignedXml

    master

    To sign an XML document, follow these steps:

    1. Instantiate SignedXml with your keys and algorithms.
    2. Call addReference() to specify which elements to sign using an XPath expression.
    3. Call computeSignature(xml, [options]) to perform the signing.
    4. Call getSignedXml() to retrieve the final XML document containing the signature.

    Note: getSignedXml() and getSignatureXml() must only be called after computeSignature() has completed.

    const { SignedXml } = require("xml-crypto");
    
    const sig = new SignedXml({
      privateKey: '...', 
      signatureAlgorithm: '...'
    });
    
    sig.addReference({
      xpath: "//*[local-name(.)='book']",
      transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
      digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256"
    });
    
    sig.computeSignature(xml);
    const signedXml = sig.getSignedXml();
  9. Configure SignedXmlOptions

    master

    When constructing new SignedXml(options), you can provide the following configuration keys:

    • idMode: (default null) If set to wssecurity, it creates/validates IDs with the WS-Security namespace.
    • idAttribute: (string, default undefined) An additional attribute name to check for an element's ID (checked before Id, ID, and id).
    • privateKey: (string or Buffer, default null) The private key used for signing.
    • publicCert: (string or Buffer, default null) The public certificate used for verification.
    • signatureAlgorithm: (string) The signature algorithm to use.
    • canonicalizationAlgorithm: (string, default undefined) The canonicalization algorithm to use.
    • inclusiveNamespacesPrefixList: (string, default null) A list of namespace prefixes to include during canonicalization.
    • implicitTransforms: (string[], default []) A list of implicit transforms to use during verification.
    • keyInfoAttributes: (object, default {}) A hash of attrName: value to add to the KeyInfo node.
    • getKeyInfoContent: (function, default SignedXml.getKeyInfoContent) A function returning the content of the KeyInfo node.
    • getCertFromKeyInfo: (function, default noop) A function that returns the certificate from the <KeyInfo /> node.
  10. Handle implicit transforms during verification

    master

    If signature verification fails unexpectedly, it may be due to "implicit transforms"—hidden normalization steps (like removing extra whitespace or sorting attributes) required by the XML signature specification.

    To resolve this, you can attempt to specify these transforms manually in the SignedXml constructor using the implicitTransforms option. These transforms run after the transforms declared in the <Reference> element.

    Common transforms to try:

    • http://www.w3.org/TR/2001/REC-xml-c14n-20010315
    • http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments
    • http://www.w3.org/2001/10/xml-exc-c14n#
    • http://www.w3.org/2001/10/xml-exc-c14n#WithComments
    const sig = new SignedXml({
      implicitTransforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
      publicCert: fs.readFileSync("client_public.pem"),
    });
    sig.loadSignature(signature);
    const res = sig.checkSignature(xml);
  11. Perform asynchronous signing

    master

    If your signing process requires an external service (like an HSM or a signing server), you can implement an asynchronous signing algorithm. Your getSignature method should accept a callback as the third argument and invoke it when the operation is complete. When using asynchronous signing, you must pass a callback to computeSignature(xml, callback) to handle the result.

    class AsyncRsaSha256 {
      getSignature(signedInfo, privateKey, callback) {
        // Simulate async operation
        const signer = crypto.createSign("RSA-SHA256");
        signer.update(signedInfo);
        callback(null, signer.sign(privateKey, "base64"));
      }
    
      getAlgorithmName() {
        return "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
      }
    }
    
    const sig = new SignedXml({
      privateKey: fs.readFileSync("client.pem"),
      signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    });
    
    sig.SignatureAlgorithms["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"] = AsyncRsaSha256;
    
    sig.computeSignature(xml, (err) => {
      if (err) return console.error(err);
      fs.writeFileSync("signed.xml", sig.getSignedXml());
    });
  12. Add a prefix to the signature using computeSignature

    master

    When calling computeSignature, you can pass a prefix option to add a namespace prefix to the signature elements.

    const { SignedXml } = require("xml-crypto");
    const fs = require("fs");
    
    const xml = "<library><book><name>Harry Potter</name></book></library>";
    
    const sig = new SignedXml({
      privateKey: fs.readFileSync("client.pem"),
      canonicalizationAlgorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
      signatureAlgorithm: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    });
    sig.addReference({
      xpath: "//*[local-name(.)='book']",
      digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
      transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
    });
    sig.computeSignature(xml, {
      prefix: "ds",
    });