xml-crypto icon indicating copy to clipboard operation
xml-crypto copied to clipboard

Failed to generate a correct signature

Open hsan8 opened this issue 11 months ago • 5 comments

Can someone help me to generate same signature like this in nodejs using xml-crypto: I have two keys :

  • privatekey.pem
  • publickey.pem
<Req
    xmlns="http://www.abc.com/abc/schema/">
    <Header requestId="f359718a-b759-4617-aebb-1260d98fef3e" timestamp="2025-01-19 15:26:28.552" ver="1.0"/>
    <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="">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                <DigestValue>RX4gMUYcjhYtNPANxPJ2aCmxXzc=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>fptL80Jl6614.....</SignatureValue>
        <KeyInfo>
            <KeyValue>
                <RSAKeyValue>
                    <Modulus>hPN+z6tI/WSPV1sKDDfw2.....</Modulus>
                    <Exponent>AQAB</Exponent>
                </RSAKeyValue>
            </KeyValue>
        </KeyInfo>
    </Signature>
</Req>

This is my try in js

const crypto = require('crypto');
const SignedXml = require('xml-crypto').SignedXml;
const fs = require('fs').promises;

class XmlSigner {
  constructor() {
    this.XML_TEMPLATE = `<Req
    xmlns="http://www.abc.com/abc/schema/">
    <Header requestId="f359718a-b759-4617-aebb-1260d98fef3e" timestamp="2025-01-19 15:26:28.552" ver="1.0"/></Req>`;
  }

  async loadPrivateKey(pemContent) {
    return pemContent.replace('-----BEGIN PRIVATE KEY-----', '').replace('-----END PRIVATE KEY-----', '').replace(/\s/g, '');
  }

  async loadPublicKey(pemContent) {
    return pemContent.replace('-----BEGIN PUBLIC KEY-----', '').replace('-----END PUBLIC KEY-----', '').replace(/\s/g, '');
  }

  extractModulusAndExponent(publicKeyPem) {
    // Create a public key object
    const publicKey = crypto.createPublicKey({
      key: `-----BEGIN PUBLIC KEY-----\n${publicKeyPem}\n-----END PUBLIC KEY-----`,
      format: 'pem'
    });

    // Export as DER format
    const derKey = publicKey.export({ type: 'spki', format: 'der' });

    // Parse the DER structure to extract modulus and exponent
    // Skip the ASN.1 header to get to the key parts
    let offset = 24; // Typical offset for RSA public key in SPKI format

    // Read modulus length
    const modulusLength = derKey[offset];
    offset++;

    // Extract modulus
    const modulus = derKey.slice(offset, offset + modulusLength);
    offset += modulusLength;

    // Skip header byte
    offset++;

    // Read exponent length
    const exponentLength = derKey[offset];
    offset++;

    // Extract exponent
    const exponent = derKey.slice(offset, offset + exponentLength);

    return {
      modulus: modulus.toString('base64'),
      exponent: exponent.toString('base64')
    };
  }

  async signXml(privateKeyPem, publicKeyPem) {
    const { modulus, exponent } = this.extractModulusAndExponent(publicKeyPem);

    const sig = new SignedXml();

    sig.addReference({
      xpath: "/*",
      digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
      transforms: ['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
      isEmptyUri: false,
    });

    sig.canonicalizationAlgorithm = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
    sig.signatureAlgorithm = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';

    sig.privateKey = Buffer.from(privateKeyPem, 'base64');
    sig.keyInfo = `<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><KeyValue><RSAKeyValue><Modulus>${modulus}</Modulus><Exponent>${exponent}</Exponent></RSAKeyValue></KeyValue></KeyInfo>`;

    console.log(this.XML_TEMPLATE);

    sig.computeSignature(xml, signature);
    return sig.getSignedXml();
  }
}

async function main() {
  try {
    const signer = new XmlSigner();

    // Load keys from files
    const privateKeyPem = await fs.readFile('private_key.pem', 'utf8');
    const publicKeyPem = await fs.readFile('public_key.pem', 'utf8');

    // Load and process the keys
    const privateKey = await signer.loadPrivateKey(privateKeyPem);

    const publicKey = await signer.loadPublicKey(publicKeyPem);

    // Sign the XML
    const signedXml = await signer.signXml(privateKey, publicKey);
    console.log(signedXml);
  } catch (error) {
    console.error('Error:', error);
    console.error('Stack:', error.stack);
  }
}

main();

hsan8 avatar Jan 30 '25 21:01 hsan8

I did not test (run) your code but one thing caught my I immediately. Quote from your issue report

   sig.addReference({
     xpath: "/*",
     digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
     transforms: ['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
     isEmptyUri: false,
   });

xml-crypto does not apply implicitly http://www.w3.org/TR/2001/REC-xml-c14n-20010315 transformation even though such transformation is applied implicitly (if not excplicitly listed) during validation. I.e. if your problem is that some service does not calculate same digest as your code try:

    sig.addReference({
      xpath: "/*",
      digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
      transforms: [
        'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
        'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
      ],
      isEmptyUri: false,
    });

See more info from these two comments from another issue:

  1. https://github.com/node-saml/xml-crypto/issues/212#issuecomment-1949310736
  2. https://github.com/node-saml/xml-crypto/issues/212#issuecomment-1949361660

srd90 avatar Jan 30 '25 22:01 srd90

@srd90 : Thank you for replying, can you help me with a snippet that can generate a signed xml same as above

hsan8 avatar Jan 30 '25 22:01 hsan8

Did you add that transformation and test your stuff?

FWIW, if you are conserned about having that extra transformation explicitly listed at resulting xml (which means that your signature block doesn't look exactly same with what you want) you shouldn't (be conserned). You'd just happen to list it explicitly instead of relying to implicit transform.

FWIW2: you cannot make that (required) extra transform vanish unless you introduce a PR to xml-crypto repo which modify implementation (https://github.com/node-saml/xml-crypto/issues/212#issuecomment-1950166467) to use that transform implicitly (unless there aren't any explicitly listed C14N)...i.e. unless you make stuff work symmetrically with validation and how other libs seems to apply implicit transforms.

...can you help me with a snippet that can generate a signed xml same as above

I cannot. It is not possible with current (6.0.0) and past versions of xml-crypto (if your ultimate goal is to have 1:1 same transforms list at resulting XML as your sample XML).

srd90 avatar Jan 30 '25 23:01 srd90

I cloned the repo, I change snippets to fit my needs and it's working perfectly The changes:

  • I added to c14n-canonicalization.ts the following class:
export class C14nCanonicalizationEnveloped extends C14nCanonicalization {
  constructor() {
    super();
    this.includeComments = false;
  }

  getAlgorithmName() {
    return "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
  }
}
  • I added to CanonicalizationAlgorithms in signed-xml.ts
"http://www.w3.org/2000/09/xmldsig#enveloped-signature": c14n.C14nCanonicalizationEnveloped,

hsan8 avatar Feb 03 '25 06:02 hsan8

So you registered totally different implementation for algorithm http://www.w3.org/2000/09/xmldsig#enveloped-signature(?)

It (http://www.w3.org/2000/09/xmldsig#enveloped-signature) should be

https://github.com/node-saml/xml-crypto/blob/21201723d2ca9bc11288f62cf72552b7d659b000/src/enveloped-signature.ts#L10-L61

but your modification registered implementation which is

https://github.com/node-saml/xml-crypto/blob/21201723d2ca9bc11288f62cf72552b7d659b000/src/c14n-canonicalization.ts#L10-L281

Its usually bad thing / troubles ahead when you do such things. Obviously your problem was just the lack of C14N after enveloped signature transformation when signing. You could have added that explicitly to the tranformations list or you could have fixed xml-crypto to apply that implicitly also during signing as other libs seems to do if it is not explicitly listed instead of redefining implementation of http://www.w3.org/2000/09/xmldsig#enveloped-signature to C14N algorithm.

srd90 avatar Feb 03 '25 07:02 srd90