PeculiarVentures / PKI.js

PKI.js is a pure JavaScript library implementing the formats that are used in PKI applications (signing, encryption, certificate requests, OCSP and TSP requests/responses). It is built on WebCrypto (Web Cryptography API) and requires no plug-ins.
http://pkijs.org
Other
1.3k stars 204 forks source link

Detached CMS produced by openssl cannot be verified by PKI.js #279

Closed ahuggins-nhs closed 4 years ago

ahuggins-nhs commented 4 years ago

Environment(s): Windows 10 x64, Linux 4.4.0-18362 x64, Node v12.15.0 x64 Versions: @peculiar/webcrypto 1.1.1, pkijs 2.1.89

Hi, I'm working on a project that uses this library for cryptography. I've run into an issue that started with node-forge, and after review of node-forge and PKI.js I decided to switch to PKI.js, I think it's a more competitive solution.

Long story short, I've ended up in the same place but was able to perform more debugging with PKI.js than I could with the other library. The test case provided by the reporter of my issue has been added to my mocha suite at this line. I manually added some logging, console.log('result:', result, "\n", 'messageDigestValue:', messageDigestValue);, at this line in PKI.js; I received the following results:

result: ArrayBuffer {
  [Uint8Contents]: <7c 90 88 b6 17 87 7e 11 61 79 ad 48 93 77 39 2a d9 61 b5 f8 07 09 d7 e1 35 ad a4 76 5d 8c 54 cf>,
  byteLength: 32
}
messageDigestValue: ArrayBuffer {
  [Uint8Contents]: <45 0d fa 74 ec 7a ca 96 fc 08 a1 26 81 e7 4e 3d fe a7 36 79 6f 80 36 32 3b 0c c7 6a e0 c4 3a f5>,
  byteLength: 32
}

I also ran openssl dgst -sha256 -hex 'test/temp-data/payload' to verify the message digest, which matches the result in PKI.js:

SHA256(test/temp-data/payload)= 7c9088b617877e116179ad489377392ad961b5f80709d7e135ada4765d8c54cf

I'm at a complete loss as to why OpenSSL would produce a wholly different message digest during signing than during verification with PKI.js and confirming the digest with openssl dgst. I've reviewed the RFC 2315 and RFC 5652 and have no idea how this can happen, as PKI.js appears to comply with signing and verification steps in both RFCs and I assume OpenSSL would also comply. Other tests are passing, as OpenSSL is able to verify SMIME payloads that my code produces, but that's out of the scope of this question.

Can someone help me understand what's different in the signing/verification routines, and how I might be able to go about fixing either my library or PKI.js to bring it into compliance with either OpenSSL or these RFCs?

YuryStrozhevsky commented 4 years ago

@ahuggins-nhs Seems your only problem is missing of understanding that both ASN1js and PKIjs work with ArrayBuffer data only. No Node.js Buffer type allowed. I briefly checked your code and at least at this line you made a mistake and leave data as Node.js Buffer type. Anyway, I made my own test just to check all the stuff and all works fine, of course, as expected:

    const fetchResultPayload = await fetch("./files/testing/pkijs-problem-payload");
    const fetchBufferPayload = await fetchResultPayload.arrayBuffer();

    const fetchResultCMS = await fetch("./files/testing/pkijs-problem-cms.bin");
    const fetchBufferCMS = await fetchResultCMS.arrayBuffer();

    const fetchResultCert = await fetch("./files/testing/pkijs-problem-cert.cer");
    const fetchBufferCert = await fetchResultCert.arrayBuffer();

    let asn1 = asn1js.fromBER(fetchBufferCert);
    const cert = new pkijs.Certificate({ schema: asn1.result });

    asn1 = asn1js.fromBER(fetchBufferCMS);

    const contentInfo = new pkijs.ContentInfo({ schema: asn1.result });
    const signedData = new pkijs.SignedData({ schema: contentInfo.content });

    const verificationResult = await signedData.verify({
        signer: 0,
        data: fetchBufferPayload,
        extendedMode: true
    });

    console.log(verificationResult);

This test I made right in browser, without any additional crypto engines, but it does not matter in your case. Also please be aware there are PKIjs examples plus PKIjs live examples. At final I do advice you to read README carefully.

ahuggins-nhs commented 4 years ago

Sorry, that was an oversight on my part on that line, thank you. I updated the code there and I also adjusted the test case. I took your example and adapted it for local development and everything passes on Windows just fine.

const pkijs = require("pkijs")
const asn1js = require("asn1js")
const { Crypto } = require("@peculiar/webcrypto")
const { readFileSync } = require("fs")
const { execSync } = require("child_process")

async function main () {
  const webcrypto = new Crypto()

  pkijs.setEngine(
    'newEngine',
    webcrypto,
    new pkijs.CryptoEngine({
      name: '@peculiar/webcrypto',
      crypto: webcrypto,
      subtle: webcrypto.subtle
    })
  )

  execSync("echo Something to Sign > payload")
  execSync("openssl req -new -x509 -nodes -keyout x509.key -out x509.pub -subj /CN=CoronaPub")
  execSync("openssl cms -sign -signer x509.pub -inkey x509.key -outform DER -out signature-cms.bin -in payload")
  execSync("openssl cms -verify -CAfile x509.pub -inkey x509.pub -inform DER -in signature-cms.bin -content payload")

  const fetchResultPayload = readFileSync('payload');
  const fetchBufferPayload = new Uint8Array(fetchResultPayload).buffer;

  const fetchResultCMS = readFileSync('signature-cms.bin');
  const fetchBufferCMS = new Uint8Array(fetchResultCMS).buffer;

  // Get and parse the PEM cert used to sign with OpenSSL from previous steps.
  const fetchResultCert = readFileSync('./x509.pub');
  const stringResultCert = fetchResultCert.toString('utf8')
  const base64ResultCert = stringResultCert
    .split('\n') // Split on new line
    .filter(line => !line.includes('-BEGIN') && !line.includes('-END')) // Remove header/trailer
    .map(line => line.trim()) // Trim extra white space
    .join('') // Back to string
  const fetchBufferCert = new Uint8Array(Buffer.from(base64ResultCert, 'base64')).buffer;

  let asn1 = asn1js.fromBER(fetchBufferCert);
  const cert = new pkijs.Certificate({ schema: asn1.result });

  asn1 = asn1js.fromBER(fetchBufferCMS);

  const contentInfo = new pkijs.ContentInfo({ schema: asn1.result });
  const signedData = new pkijs.SignedData({ schema: contentInfo.content });

  const verificationResult = await signedData.verify({
    signer: 0,
    data: fetchBufferPayload,
    extendedMode: true
  });

  console.log(verificationResult);
}

main()

However, I fired up Windows Subsystem for Linux and it backfired, PKIjs couldn't verify what OpenSSL produced. This tells me I'm looking at an environment issue where OpenSSL or the underlying system isn't right, and PKIjs in Node is working perfectly fine.

I'm firing up a Linux VM and will reply if it is also affecting Linux generally or just Windows Substsytem for Linux. I'll get you the affected versions/environments on this thread so it's searchable for others looking at the same problem.

ahuggins-nhs commented 4 years ago

EDIT: It's actually a line-ending issue. OpenSSL is canonicalizing the line-endings to CRLF per RFC 5751. I think it's supposed to be the way it works for OpenSSL, they're treating all CMS like it's supposed to be SMIME and canonicalizing, where pure CMS in either RFC 2315 or RFC 5652 makes no mention of canonicalization.

On Windows, it's not even really an issue, because Windows HAPPENS to use the terminator discussed by SMIME, CRLF. On Linux or Mac it will be either LF or LFCR. OpenSSL will ALWAYS canonicalize, so on those systems a command like echo Something to sign will always produce an apparently "bad" payload to verify UNLESS someone implementing PKIjs or other CMS lib also does what OpenSSL did.

So, it's a feature, not a bug, although I would argue that OpenSSL cms command should only canonicalize when someone is trying to output SMIME, and not when someone wants to get a plain DER or PEM file detached signature.

YuryStrozhevsky commented 4 years ago

@ahuggins-nhs PKIJS dealing with a raw data and has no idea abou line ending - the data is array of bytes only. Same should be on any systems. I will check the test case on Linux but I don’t believe the issue is because of line ending. If you do have the issue it could be because of wrong WebCrypto polifill, for example.

ahuggins-nhs commented 4 years ago

@YuryStrozhevsky We're in agreement. It's no issue with PKIjs. OpenSSL is canonicalizing as a convenience for the user.

PKIjs is fine, no bug. The developer just has to keep in mind that they will need to canonicalize their detached payload before verifying with any CMS library if they used OpenSSL cms command to produce a detached signature. This issue can be closed.

ahuggins-nhs commented 4 years ago

I've enshrined it as a warning in my test suite. https://github.com/ahuggins-nhs/node-libas2/blob/ac2a2bff2c162262e8268226234086b81f160358/test/AS2CryptoSuite.ts#L36