Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Failed to generate a correct signature #484

Open
hsan8 opened this issue Jan 30, 2025 · 5 comments
Open

Failed to generate a correct signature #484

hsan8 opened this issue Jan 30, 2025 · 5 comments

Comments

@hsan8
Copy link

hsan8 commented Jan 30, 2025

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();
@srd90
Copy link

srd90 commented Jan 30, 2025

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. Wrong Digest Value generated. Actual XML and Expected XML is not matching. Working from last three nights. #212 (comment)
  2. Wrong Digest Value generated. Actual XML and Expected XML is not matching. Working from last three nights. #212 (comment)

@hsan8
Copy link
Author

hsan8 commented Jan 30, 2025

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

@srd90
Copy link

srd90 commented Jan 30, 2025

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 (#212 (comment)) 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).

@hsan8
Copy link
Author

hsan8 commented Feb 3, 2025

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,

@srd90
Copy link

srd90 commented Feb 3, 2025

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

export class EnvelopedSignature implements CanonicalizationOrTransformationAlgorithm {
protected includeComments = false;
constructor() {
this.includeComments = false;
}
process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions): Node {
if (null == options.signatureNode) {
const signature = xpath.select1(
"./*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
node,
);
if (isDomNode.isNodeLike(signature) && signature.parentNode) {
signature.parentNode.removeChild(signature);
}
return node;
}
const signatureNode = options.signatureNode;
const expectedSignatureValue = xpath.select1(
".//*[local-name(.)='SignatureValue']/text()",
signatureNode,
);
if (isDomNode.isTextNode(expectedSignatureValue)) {
const expectedSignatureValueData = expectedSignatureValue.data;
const signatures = xpath.select(
".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']",
node,
);
for (const nodeSignature of Array.isArray(signatures) ? signatures : []) {
const signatureValue = xpath.select1(
".//*[local-name(.)='SignatureValue']/text()",
nodeSignature,
);
if (isDomNode.isTextNode(signatureValue)) {
const signatureValueData = signatureValue.data;
if (expectedSignatureValueData === signatureValueData) {
if (nodeSignature.parentNode) {
nodeSignature.parentNode.removeChild(nodeSignature);
}
}
}
}
}
return node;
}
getAlgorithmName(): CanonicalizationOrTransformAlgorithmType {
return "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
}
}

but your modification registered implementation which is

export class C14nCanonicalization implements CanonicalizationOrTransformationAlgorithm {
protected includeComments = false;
constructor() {
this.includeComments = false;
}
attrCompare(a, b) {
if (!a.namespaceURI && b.namespaceURI) {
return -1;
}
if (!b.namespaceURI && a.namespaceURI) {
return 1;
}
const left = a.namespaceURI + a.localName;
const right = b.namespaceURI + b.localName;
if (left === right) {
return 0;
} else if (left < right) {
return -1;
} else {
return 1;
}
}
nsCompare(a, b) {
const attr1 = a.prefix;
const attr2 = b.prefix;
if (attr1 === attr2) {
return 0;
}
return attr1.localeCompare(attr2);
}
renderAttrs(node) {
let i;
let attr;
const attrListToRender: Attr[] = [];
if (isDomNode.isCommentNode(node)) {
return this.renderComment(node);
}
if (node.attributes) {
for (i = 0; i < node.attributes.length; ++i) {
attr = node.attributes[i];
//ignore namespace definition attributes
if (attr.name.indexOf("xmlns") === 0) {
continue;
}
attrListToRender.push(attr);
}
}
attrListToRender.sort(this.attrCompare);
const res = attrListToRender.map((attr) => {
return ` ${attr.name}="${utils.encodeSpecialCharactersInAttribute(attr.value)}"`;
});
return res.join("");
}
/**
* Create the string of all namespace declarations that should appear on this element
*
* @param node The node we now render
* @param prefixesInScope The prefixes defined on this node parents which are a part of the output set
* @param defaultNs The current default namespace
* @param defaultNsForPrefix
* @param ancestorNamespaces Import ancestor namespaces if it is specified
* @api private
*/
renderNs(
node: Element,
prefixesInScope: string[],
defaultNs: string,
defaultNsForPrefix: string,
ancestorNamespaces: NamespacePrefix[],
): RenderedNamespace {
let i;
let attr;
const res: string[] = [];
let newDefaultNs = defaultNs;
const nsListToRender: { prefix: string; namespaceURI: string }[] = [];
const currNs = node.namespaceURI || "";
//handle the namespace of the node itself
if (node.prefix) {
if (prefixesInScope.indexOf(node.prefix) === -1) {
nsListToRender.push({
prefix: node.prefix,
namespaceURI: node.namespaceURI || defaultNsForPrefix[node.prefix],
});
prefixesInScope.push(node.prefix);
}
} else if (defaultNs !== currNs) {
//new default ns
newDefaultNs = node.namespaceURI || "";
res.push(' xmlns="', newDefaultNs, '"');
}
//handle the attributes namespace
if (node.attributes) {
for (i = 0; i < node.attributes.length; ++i) {
attr = node.attributes[i];
//handle all prefixed attributes that are included in the prefix list and where
//the prefix is not defined already. New prefixes can only be defined by `xmlns:`.
if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) {
nsListToRender.push({ prefix: attr.localName, namespaceURI: attr.value });
prefixesInScope.push(attr.localName);
}
//handle all prefixed attributes that are not xmlns definitions and where
//the prefix is not defined already
if (
attr.prefix &&
prefixesInScope.indexOf(attr.prefix) === -1 &&
attr.prefix !== "xmlns" &&
attr.prefix !== "xml"
) {
nsListToRender.push({ prefix: attr.prefix, namespaceURI: attr.namespaceURI });
prefixesInScope.push(attr.prefix);
}
}
}
if (utils.isArrayHasLength(ancestorNamespaces)) {
// Remove namespaces which are already present in nsListToRender
for (const ancestorNamespace of ancestorNamespaces) {
let alreadyListed = false;
for (const nsToRender of nsListToRender) {
if (
nsToRender.prefix === ancestorNamespace.prefix &&
nsToRender.namespaceURI === ancestorNamespace.namespaceURI
) {
alreadyListed = true;
}
}
if (!alreadyListed) {
nsListToRender.push(ancestorNamespace);
}
}
}
nsListToRender.sort(this.nsCompare);
//render namespaces
res.push(
...nsListToRender.map((attr) => {
if (attr.prefix) {
return ` xmlns:${attr.prefix}="${attr.namespaceURI}"`;
}
return ` xmlns="${attr.namespaceURI}"`;
}),
);
return { rendered: res.join(""), newDefaultNs };
}
/**
* @param node Node
*/
processInner(node, prefixesInScope, defaultNs, defaultNsForPrefix, ancestorNamespaces) {
if (isDomNode.isCommentNode(node)) {
return this.renderComment(node);
}
if (node.data) {
return utils.encodeSpecialCharactersInText(node.data);
}
if (isDomNode.isElementNode(node)) {
let i;
let pfxCopy;
const ns = this.renderNs(
node,
prefixesInScope,
defaultNs,
defaultNsForPrefix,
ancestorNamespaces,
);
const res = ["<", node.tagName, ns.rendered, this.renderAttrs(node), ">"];
for (i = 0; i < node.childNodes.length; ++i) {
pfxCopy = prefixesInScope.slice(0);
res.push(
this.processInner(node.childNodes[i], pfxCopy, ns.newDefaultNs, defaultNsForPrefix, []),
);
}
res.push("</", node.tagName, ">");
return res.join("");
}
throw new Error(`Unable to canonicalize node type: ${node.nodeType}`);
}
// Thanks to deoxxa/xml-c14n for comment renderer
renderComment(node: Comment) {
if (!this.includeComments) {
return "";
}
const isOutsideDocument = node.ownerDocument === node.parentNode;
let isBeforeDocument = false;
let isAfterDocument = false;
if (isOutsideDocument) {
let nextNode: ChildNode | null = node;
let previousNode: ChildNode | null = node;
while (nextNode !== null) {
if (nextNode === node.ownerDocument.documentElement) {
isBeforeDocument = true;
break;
}
nextNode = nextNode.nextSibling;
}
while (previousNode !== null) {
if (previousNode === node.ownerDocument.documentElement) {
isAfterDocument = true;
break;
}
previousNode = previousNode.previousSibling;
}
}
const afterDocument = isAfterDocument ? "\n" : "";
const beforeDocument = isBeforeDocument ? "\n" : "";
const encodedText = utils.encodeSpecialCharactersInText(node.data);
return `${afterDocument}<!--${encodedText}-->${beforeDocument}`;
}
/**
* Perform canonicalization of the given node
*
* @param node
* @api public
*/
process(node: Node, options: CanonicalizationOrTransformationAlgorithmProcessOptions): string {
options = options || {};
const defaultNs = options.defaultNs || "";
const defaultNsForPrefix = options.defaultNsForPrefix || {};
const ancestorNamespaces = options.ancestorNamespaces || [];
const prefixesInScope: string[] = [];
for (let i = 0; i < ancestorNamespaces.length; i++) {
prefixesInScope.push(ancestorNamespaces[i].prefix);
}
const res = this.processInner(
node,
prefixesInScope,
defaultNs,
defaultNsForPrefix,
ancestorNamespaces,
);
return res;
}
getAlgorithmName() {
return "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
}
}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants