node.jsexpresssslcryptographyxml-crypto

Sign XML with xml-crypto and getPeerCertificate


I am using NodeJS 20.11.1 and Express to create a HTTPS server that would have a route /cert. The idea is that this route will request the client certificate that will be used to sign a document. The idea is that our nation (Bulgaria) uses qualified digital signature. It is basically a smart card that contains a certificate used to identify you in government websites for electronic requests. I am developing a web application for dentists that want to leverage these certificates and submit some data to the national health institute. So my idea is the following - the client (a doctor) inserts their smart card in the PC, opens this route and submits their certificate.

To authenticate with this health system, I have to open a URL https://ptest-auth.his.bg/token (test environment) that will give me an XML document. I have to sign it with the client's certificate and send the signed data to the same URL and it will provide me with accessToken that I can use as an authorization header later in the requests.

So my code right now is this:

import { SignedXml } from "xml-crypto";
const keyPath = path.join(__dirname, "..", "..", "selfsigned.key");
const certPath = path.join(__dirname, "..", "..", "selfsigned.crt");
const key = fs.readFileSync(keyPath);
const cert = fs.readFileSync(certPath);
const options = {
  key: key,
  cert: cert,
  requestCert: true,
  rejectUnauthorized: false,
};

app.get("/cert", async (req: Request, res: Response) => {
  // Get the digital signature of the doctor
  const clientCert = (req.socket as TLSSocket).getPeerCertificate(true).raw.toString("base64");
  // build a PEM certificate
  const match = clientCert.match(/.{0,64}/g);
  const prefix = "-----BEGIN CERTIFICATE-----\n";
  const postfix = "-----END CERTIFICATE-----";
  if (!match) {
    return res.status(400).json({
      error: "No client certificate provided",
    });
  }
  const pemText = prefix + match.join("\n") + postfix;
  // Get the XML document
  axios.get("https://ptest-auth.his.bg/token")
    .then((response) => {
      // This is a normal response
    })
    .catch((error) => {
      // Doing the processing here because the API returns 401 if no certificate is provided
      const data = error.response.data;
      // Sign the document
      const signature = new SignedXml({ privateKey: pemText });
      signature.addReference({
        xpath: "/*",
        digestAlgorithm: "http://www.w3.org/2001/04/xmlenc#sha512",
        transforms: ["http://www.w3.org/2001/10/xml-exc-c14n#"],
      });
      signature.canonicalizationAlgorithm = "http://www.w3.org/2001/10/xml-exc-c14n#";
      signature.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512";
      signature.computeSignature(data);
      const signedData = signature.getSignedXml();
      console.log(signedData);
    });
  res.status(200).json({
    success: true,
    message: "Client certificate",
    clientCert,
  });
});

https.createServer(options, app).listen(8002, () => {
  console.log("listening on port 8002");
});

Unfortunately, this gives me an error:

Error: error:1E08010C:DECODER routines::unsupported
    at Sign.sign (node:internal/crypto/sig:128:29)
    at D:\GitHub\dentrino-gql\node_modules\xml-crypto\lib\signature-algorithms.js:51:32
    at RsaSha512.getSignature (D:\GitHub\dentrino-gql\node_modules\xml-crypto\lib\types.js:52:20)
    at SignedXml.calculateSignatureValue (D:\GitHub\dentrino-gql\node_modules\xml-crypto\lib\signed-xml.js:244:42)
    at SignedXml.computeSignature (D:\GitHub\dentrino-gql\node_modules\xml-crypto\lib\signed-xml.js:634:18)
    at D:\GitHub\dentrino-gql\dist\microservices\app.js:118:19
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  library: 'DECODER routines',
  reason: 'unsupported',
  code: 'ERR_OSSL_UNSUPPORTED'
}

I have read a lot on this, and I am a noob when it comes to crypto in NodeJS. The main question mark that still remains - is the certificate I am getting sufficient to sign the XML? If so, why is it not working?

Any ideas are appreciated!


Solution

  • ... is the certificate I am getting sufficient to sign the XML

    No. Signing something needs the private key, the certificate (which only contains the public key) is not enough. The private key has to be kept private by the owner of the certificate (hence the name). So in your specific case with the private key on the smartcard which is on the client side, the signature can only be done on the client side too.

    Just imagine what would happen if you could sign with just the (public) certificate and without the private key: you could sign arbitrary documents in the name of the client, who would have no control and knowledge about what you've signed in their name.