PeculiarVentures/xadesjs

Help with XADES-EPES signature from .p12 cert

jsousa-Resulto opened this issue · 24 comments

Hi! At first thanks for your great work! This repo is amazing.
I'm trying to use it for signing a file with a .p12 cert, i've read related issues like #54 but without success... When I get the signature from xadesjs it differs about official software of signatures, and i don't know how i can make some parts, for example:

  • xadesjs:

<ds:X509Data> <ds:X509Certificate>MIII1TCCB72...</ds:X509Certificate> </ds:X509Data>

  • official software:
    <ds:X509Data> <ds:X509Certificate>MIII1TCCB72gA...</ds:X509Certificate> <ds:X509Certificate>MIIG3DCCBMSg...</ds:X509Certificate> <ds:X509Certificate>MIIFgzCCA2ugAw...</ds:X509Certificate> </ds:X509Data>

I can provide the both files if you need.

I'm obtaining the private key as you explained in #78 and #107 but i don't understand some things, like what information should be setted into "signingCertificate"? PublicCert? Because when i set this, I get errors running the app.. I'm sure i'm doing something wrong.. I would be very appreciated if you can support me with an example consuming a p12 and using their public cert to sign the file with XADES-EPES

Thanks in advance!

official software:
ds:X509Data ds:X509CertificateMIII1TCCB72gA...</ds:X509Certificate> ds:X509CertificateMIIG3DCCBMSg...</ds:X509Certificate> ds:X509CertificateMIIFgzCCA2ugAw...</ds:X509Certificate> </ds:X509Data>

Looks like your X509Data contains the certificate chain (leaf + issuer CA + ... + issuer CA)

I'm obtaining the private key as you explained

Do you have the private key in PKCS#8 format?

XAdESjs supports WebCrypto API which supports PKCS#8 key importing. And you can use crypto.subtle.importKey to create a private CryptoKey

If you don't, you can use PKCS#12 pkijs example to get Certificate and CryptoKey

what information should be setted into "signingCertificate"? PublicCert? Because when i set this, I get errors running the app

What is your error?

Looks like your X509Data contains the certificate chain (leaf + issuer CA + ... + issuer CA)

Yes but how can I add this info to xadesjs? I've tried to get the public chain and add them to X509 array but xadesjs generate the file with format:
ds:X509Data ds:X509CertificateMIII1TCCB72...</ds:X509Certificate> </ds:X509Data>
ds:X509Data ds:X509CertificateMIIG3DCCBMSg...</ds:X509Certificate> </ds:X509Data>
ds:X509Data ds:X509Certificate MIIFgzCCA2ugAw...</ds:X509Certificate> </ds:X509Data>

Separating X509Data tags by each member of array...

Do you have the private key in PKCS#8 format?

I think so, I'm getting and converting in PKCS8 like: https://codesandbox.io/s/xadesjs-issue-78-o00ez?file=/index.js

What is your error?

Sorry, I was wrong, The error was related with trying to set my public pem string to keyValue property (inside options in sign function). Could you please support me on how I should convert my public cert into CryptoKey for using in keyValue?

Thanks a lot for your help!

Yes but how can I add this info to xadesjs?

Do it via XML structure of signedXml.XmlSignature object.

import * as XmlDSig from "xmldsigjs";

const x509Data = new XmlDSig.KeyInfoX509Data();
x509Data.AddCertificate(new XmlDSig.X509Certificate(Buffer.from(cert, "base64")));
x509Data.AddCertificate(new XmlDSig.X509Certificate(Buffer.from(cert2, "base64")));
signedXml.XmlSignature.KeyInfo.Add(x509Data);

const signature = await signedXml.Sign(
  // ...

Console log

<ds:KeyInfo>
  <ds:X509Data>
    <ds:X509Certificate>MIIGgTCC...fPasd9NbpBNp7A==</ds:X509Certificate>
    <ds:X509Certificate>MIIGgTCC...fPasd9NbpBNp7A==</ds:X509Certificate>
  </ds:X509Data>
</ds:KeyInfo>

Thanks @microshine for your help, now the format of my x509Data is well formed.
Now I'm just trying to validate the result but if I paste my signed xml in xadesjs.com error appears:
XMLJS0013: Cryptographic error: Invalid digest for uri 'undefined'. Calculated digest is zaj2/SKHHWrsxj8ONry2K4auOakwC7FxMA6DmTBJyUY= but the xml to validate supplies digest lWj7TIL2nSX41JjdGeGDeX34HoYXCW8VqpTvnWLoCNQ=

This error is related with SignedInfo -> First Reference block:
<ds:SignedInfo> <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> <ds:Reference> <ds:Transforms> <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> </ds:Transforms> <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/> <ds:DigestValue>lWj7TIL2nSX41JjdGeGDeX34HoYXCW8VqpTvnWLoCNQ=</ds:DigestValue> </ds:Reference> <ds:Reference URI="#xades-id-b87fc0df3c99" Type="http://uri.etsi.org/01903#SignedProperties"> <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> <ds:DigestValue>/Tjd4XjWBOkxsbLGYiXVIQjFQmU=</ds:DigestValue> </ds:Reference> </ds:SignedInfo>

Could you please help me to understand what is causing the error?

-- edited --
This is my code sample for helping to analysis: https://codesandbox.io/s/p12-xades-rslt-3j7l5e
-- end edition --

Thanks in advance!

Hi guys, any idea what can cause this digest calculation error? Maybe could be related with canonicalization?
I've tried to add c14n to transforms, adding Id and Type to references, adding '' to URI for avoiding "undefined"... but the error should be related with other section..

My code is published in: https://codesandbox.io/s/p12-xades-rslt-3j7l5e

Your help would be very very appreciate.

Thanks a lot!

Could you share your signed XML file (microshine@mail.ru)?

Sent!

Thanks a lot!

I found the problem in your example. You are using incorrect uri attribute for KeyInfo reference. If you need to point to Id it must be the next format #<id>

try to update your example and try again

- { uri: signedXml.XmlSignature.KeyInfo.Id, hash }
+ { uri: `#${signedXml.XmlSignature.KeyInfo.Id}`, hash }

Here is my sign example without PFX but with X509 chain generator.

TypeScript

import * as fs from "fs";
import * as crypto from "crypto";
import * as xades from "xadesjs";
import * as xmldsig from "xmldsigjs";
import * as x509 from "@peculiar/x509";
import { Convert } from "pvtsutils";

const nodeCrypto = crypto.webcrypto as unknown as Crypto;
xades.Application.setEngine("NodeJS", nodeCrypto);
x509.cryptoProvider.set(nodeCrypto);

const hash = "SHA-256";
const alg: EcKeyGenParams & EcdsaParams = {
  name: "ECDSA",
  namedCurve: "P-256",
  hash,
};

async function createCert() {
  const currentDate = new Date();;
  const rootKeys = await nodeCrypto.subtle.generateKey(alg, false, ["sign", "verify"]);
  const rootCert = await x509.X509CertificateGenerator.createSelfSigned({
    serialNumber: Convert.ToHex(nodeCrypto.getRandomValues(new Uint8Array(10))),
    name: "CN=Root CA",
    keys: rootKeys,
    notBefore: currentDate,
    notAfter: new Date(currentDate.getTime() + 172800000),
    signingAlgorithm: alg,
  });
  rootCert.privateKey = rootKeys.privateKey;

  const leafKeys = await nodeCrypto.subtle.generateKey(alg, false, ["sign", "verify"]);
  const leafCert = await x509.X509CertificateGenerator.create({
    serialNumber: Convert.ToHex(nodeCrypto.getRandomValues(new Uint8Array(10))),
    subject: "CN=Leaf cert",
    issuer: rootCert.subjectName,
    publicKey: leafKeys.publicKey,
    notBefore: currentDate,
    notAfter: new Date(currentDate.getTime() + 86400000),
    signingKey: rootKeys.privateKey,
    signingAlgorithm: alg,
  });
  leafCert.privateKey = leafKeys.privateKey;

  return new x509.X509Certificates([leafCert, rootCert]);
}

async function main() {
  const xmlString = fs.readFileSync("invoice.xml", "utf-8");
  const xmlDoc = xades.Parse(xmlString);
  const chain = await createCert();

  // new instance of signed document
  const signedXml = new xades.SignedXml();

  // adding of public chain into x509Data block of KeyInfo
  const x509Data = new xmldsig.KeyInfoX509Data();
  for (const cert of chain) {
    x509Data.AddCertificate(new xmldsig.X509Certificate(cert.rawData));
  }
  signedXml.XmlSignature.KeyInfo.Id =
    signedXml.XmlSignature.Id + "-KeyInfo";
  signedXml.XmlSignature.KeyInfo.Add(x509Data);

  // Sign
  const signature = await signedXml.Sign(
    // Signing document
    alg, // algorithm
    chain[0].privateKey!, // key
    xmlDoc, // document
    {
      // options
      references: [
        {
          uri: "",
          hash,
          transforms: ["enveloped"],
          type: "http://www.w3.org/2000/09/xmldsig#Object",
          id: signedXml.XmlSignature.Id + "-Reference"
        },
        { uri: `#${signedXml.XmlSignature.KeyInfo.Id}`, hash }
      ],
      policy: {
        hash,
        identifier: {
          value:
            "http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf"
        },
        // description: "Política de firma electrónica para facturación electrónica con formato Facturae"
      },
      productionPlace: {
        country: "Spain",
        state: "Badajoz",
        city: "Merida",
        code: "06800"
      },
      keyValue: await chain[0].publicKey.export(),
      signingTime: {
        value: new Date(),
      },
      signerRole: { claimed: ["emisor"] },
      signingCertificate: chain[0].toString("base64"),
    }
  );

  xmlDoc.documentElement.appendChild(signature.GetXml()!);
  fs.writeFileSync(
    "signed.xml",
    signedXml.toString()
  );
}

main().catch(e => {
  console.error();
  process.exit(1);
});

NOTE: If you are using VSCode please enable TS for type validation (use // @ts-check). Your code has some type errors, and I fixed them in my example.

Here is a good article about JS in VSCode https://code.visualstudio.com/docs/nodejs/working-with-javascript.

Thanks a lot @microshine for your appreciation about id and your notes. Unfortunately my error persist:

XMLJS0013: Cryptographic error: Invalid digest for uri ''.
Calculated digest is Vk0v+zDRD5ksRIIvGM2kWprsuuziCrBDV+Do1edDh3Q= but the xml to validate supplies digest lWj7TIL2nSX41JjdGeGDeX34HoYXCW8VqpTvnWLoCNQ=

Based on your code I think the problem is related with use (or getting method) of privateKey and publicKey when signedXml.Sign is called.

I'm going to continue testing and looking for a solution.

Thanks

@jsousa-Resulto Please code I've shared it works without XMLJS0013 error.

The problem can be in XML serialization. Maybe something changes your output XML. Please try to serialize XML like I do in my example

  xmlDoc.documentElement.appendChild(signature.GetXml()!);
  fs.writeFileSync(
    "signed.xml",
    signedXml.toString()
  );

Please code I've shared it works without XMLJS0013 error.

Of course I know it!

But I've adapted my code (published) to run like yours (with some differences but same serialization) and result has been the same: XMLJS0013 error.

By that I've thought my error is related with other reason :(

I've tried to use crypto like you but I got the error: Unsupported key usage for an RSASSA-PKCS1-v1_5 key
(same result with some algorithms)

I'm reading if the algorithms can be part of the problem buy I've no idea..

Thanks a lot for your support

Please send me the last signed file. I'd like to understand which reference is broken

Hi @microshine, comparing with other signed files I've found a missing block:

            <xades:SignedDataObjectProperties>
                <xades:DataObjectFormat ObjectReference="#Reference-cd1bd792-4bac-49a6-b0a3-6082ee520e8b">
                    <xades:Description/>
                    <xades:ObjectIdentifier>
                        <xades:Identifier Qualifier="OIDAsURN">urn:oid:1.2.840.10003.5.109.10</xades:Identifier>
                        <xades:Description/>
                    </xades:ObjectIdentifier>
                    <xades:MimeType>text/xml</xades:MimeType>
                    <xades:Encoding/>
                </xades:DataObjectFormat>
            </xades:SignedDataObjectProperties>

The ObjectReference is referenced to the "corrupt" reference block.

How can I add this SignedDataObjectProperties to signedXML with xmldsigjs o xadesjs? I'm reading documentation to find how do it but if you can guide me it would be faster.

Thanks a lot and I hope be closer than solution...

Please call this code before the Sign method

const dataObjectFormat = new xades.xml.DataObjectFormat();
dataObjectFormat.ObjectReference = "#Reference-cd1bd792-4bac-49a6-b0a3-6082ee520e8b";
dataObjectFormat.Description = "Política de firma electrónica para facturación electrónica con formato Facturae";
dataObjectFormat.ObjectIdentifier.Identifier.Qualifier = "OIDAsURN";
dataObjectFormat.ObjectIdentifier.Identifier.Value = "urn:oid:1.2.840.10003.5.109.10";
dataObjectFormat.MimeType = "text/xml";
signedXml.SignedProperties.SignedDataObjectProperties.DataObjectFormats.Add(dataObjectFormat);

I was hoping that was the problem but same result... The signed file now is very very similar to a valid one..

Do you think the error could be related with namespaces of QualifyingProperties?

In a valid xml I see:

    <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="Signature-fd1bd268-66e7-49fc-82b2-e1f5cf3de028-QualifyingProperties" Target="#Signature-fd1bd268-66e7-49fc-82b2-e1f5cf3de028-Signature"
        xmlns:ds="http://www.w3.org/2000/09/xmldsig#">

But in my file I got:

    <xades:QualifyingProperties Target="#id-f9ceb0a9daf8"
                xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">

Another one idea is that I'm misdefining the object reference:

      // options
      references: [
        {
          uri: "",
          hash,
          transforms: ["enveloped"],
          type: "http://www.w3.org/2000/09/xmldsig#Object",
          id: signedXml.XmlSignature.Id + "-Reference"
        },
        { uri: `#${signedXml.XmlSignature.KeyInfo.Id}`, hash }
      ],

https://codesandbox.io/s/issue-122-xfjb6v?file=/src/index.ts

Please see this app

Run

npx ts-node src

Output

Application started
true

It's not clear why you've got reference error. I bet it's not the problem in key usage, it could be the problem in canon method or xml serialization. But you can see that my scripts works, and I can't reproduce your exception

There is one more way how to catch the difference. You can edit the DigestReference method directly in node_modules/xmldsigjs/build folder https://github.com/PeculiarVentures/xmldsigjs/blob/master/src/signed_xml.ts#L436. And add console.log(canonOutput) to see it on signing and verification

Hi @microshine you was right (I had no doubts about that!), I've minified my input xml and error in Reference dissapears. Now the same error is in next reference (KeyInfo)... Commenting reference of KeyInfo, I got a valid signature ( :,) ), but with the block of KeyInfo added:
XMLJS0013: Cryptographic error: Invalid digest for uri '#id-ec6970bc8ed9-KeyInfo'. Calculated digest is tgergis9k2YmPCE9ptZf2IG6eZ1y+zy9fEAPspatifQ= but the xml to validate supplies digest 3daHnrGmHOEX3Jsos+atKFI836DQB7qO1BGTm+pzmbw=

The canon output looks great (I think so...):
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:f="http://www.facturae.es/Facturae/2009/v3.2/Facturae" Id="id-ec6970bc8ed9-KeyInfo"><ds:X509Data><ds:X509Certificate>MIII1TCCB72g...Hd1CX</ds:X509Certificate><ds:X509Certificate>MIIG3DCC...jj2So=</ds:X509Certificate><ds:X509Certificate>MIIFgzC...PaLtrM=</ds:X509Certificate></ds:X509Data><ds:KeyValue><ds:RSAKeyValue><ds:Modulus>mkUhqW...bBmLyw==</ds:Modulus><ds:Exponent>AQAB</ds:Exponent></ds:RSAKeyValue></ds:KeyValue></ds:KeyInfo>

This block is added programmatically before signing adding the PEM of the public chain certificates.. I've reviewed each chain and I don't see nothing wrong..

Any idea about any trick to solving this last (I hope!) error?

Many many thanks for your help!

Interesting. In my example, I update KeyInfo before signing, and it doesn't break the signature

Please double-check that you don't change XmlSignature object after signing

image

Is very strange... I'm downloading the result once sign is performed:

  xmlDoc.documentElement.appendChild(signature.GetXml()!);
  fs.writeFileSync(
    "signed.xml",
    signedXml.toString()
  );

After that, I reuse signedXml.toString() for verifying signature:

  const xml = xades.Parse(signedXml.toString());
  const xmlSignature = xml.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
  const sigXml = new xades.SignedXml(xml);
  sigXml.LoadXml(xmlSignature.item(0)!);
  const ok = await sigXml.Verify();
  console.log(ok);

The result is: true

But if I copy the content of my exported file and I paste it in xadesjs.com I get the error:
XMLJS0013: Cryptographic error: Invalid digest for uri '#id-b1f97edb2c91-KeyInfo'. Calculated digest is UpcVWziOgOkDOis8ql0bQAA6+FkxpTaG+k0hnxp6Kkc= but the xml to validate supplies digest fk8XIhcRnfHYy7UVhXR8GznqrduhP76+gpMTaMtGK64=

I'm trying to verify the signed file with our official validator and not passes validations.

It seems that exporting file generate something different... any idea?

It's possible that verify changes incoming objects on processing. Please serialize your signed XML to string and parse it again for verify (like I do in my example)

About xadesjs.com I'm not sure if it uses the latest version of the lib. I need to check it out.

Try https://tools.chilkat.io/xmlDsigVerify for XML verification

image

Thanks a lot @microshine! Now I'm going to continue trying to send xml and asking why validator returns "The signing policy is not correct".

Hi!

it seems that the signature fails policy validations in most validators (in my code and in your example, is a general validation fail). For example at https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation

I've seen that is not accepted like XAdes-EPES, is recognized as XAdes-BES.. This clue is the one i'm following

I'm doing some research to try to identify where the problem is. You may have commented on it on occasion but I haven't seen anything in the repository. I will try to provide the information that I obtain.

Thanks!

We can close this issue. Finally, reviewing policy digest I get a valid xml. Thanks a lot for your help! Your sample code in https://codesandbox.io/s/issue-122-xfjb6v?file=/src/index.ts will be very great for a lot of colleagues. I'll recommend your solution!