Failed to generate a correct signature
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();
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:
- https://github.com/node-saml/xml-crypto/issues/212#issuecomment-1949310736
- https://github.com/node-saml/xml-crypto/issues/212#issuecomment-1949361660
@srd90 : Thank you for replying, can you help me with a snippet that can generate a signed xml same as above
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).
I cloned the repo, I change snippets to fit my needs and it's working perfectly The changes:
- I added to
c14n-canonicalization.tsthe 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
CanonicalizationAlgorithmsinsigned-xml.ts
"http://www.w3.org/2000/09/xmldsig#enveloped-signature": c14n.C14nCanonicalizationEnveloped,
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.