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.25k stars 204 forks source link

Fix support for RSAES-PKCS1-V1_5 certificate signatures #342

Closed benpetermorris closed 2 years ago

benpetermorris commented 2 years ago

There is a regression between 2.1.91 and 2.1.92 and all later versions when trying to sign data using a RSAES-PKCS1-V1_5 ~public~private key (which must be performed with the RSASSA-PKCS1-V1_5 algorithm).

After tracing the issue in 2.2.2, it seems that the problem is currently that CryptoEngine.getAlgorithmParameters:2299 should switch on parameters.algorithm.name.toUpperCase() rather than privateKey.algorithm.name.toUpperCase().

microshine commented 2 years ago

There are only 2 commits in CryptoEngine file since v2.1.90 (where RSAES-PKCS1-v1_5 was supported)

Looks like regression is in another file.

Could you share the simple example of code you are using for that error getting?

benpetermorris commented 2 years ago

Looks like regression is in another file.

Possibly. In any case, the source of the rejection message is the default case in this switch statement; it's clear the intent is to use the algorithm returned by getAlgorithmParameters, which for these certs is RSASSA-PKCS1-V1_5.

You can test this by attempting to sign data with any RSAES-PKCS1-V1_5 certificate.

microshine commented 2 years ago

RSAES-PKCS1-v1_5 is an old algorithm that is not supported by WebCrypto. And this algorithm is for encrypt/decrypt operations (not for signing). For signing, you should use RSASSA-PKCS1-v1_5 (or RSA-PSS) mechanism.

I can't reproduce your problem.

benpetermorris commented 2 years ago

RSAES-PKCS1-v1_5 is an old algorithm that is not supported by WebCrypto. And this algorithm is for encrypt/decrypt operations (not for signing). For signing, you should use RSASSA-PKCS1-v1_5 (or RSA-PSS) mechanism.

I'm trying to sign using a PKCS#1 private key with the SignedData.sign method. PKI.js should determine from this key that the correct algorithm is RSASSA-PKCS1-V1_5, as you mention. I've included a repro of the issue below.

  // using the most recent versions of all packages
  import * as asn1js from 'asn1js';
  import * as pkijs from 'pkijs';
  import { Crypto } from '@peculiar/webcrypto';

  const certPem = `MIIDSzCCAjOgAwIBAgIUMAIZL8n9v/fW96J094KsZgBVsPwwDQYJKoZIhvcNAQEL
    BQAwNTEZMBcGA1UECgwQTGliQXMyIENvbW11bml0eTEYMBYGA1UEAwwPbGliYXMy
    Y29tbXVuaXR5MB4XDTIwMDUwMjIzMTUyNFoXDTMwMDUwMzIzMTUyNFowNTEZMBcG
    A1UECgwQTGliQXMyIENvbW11bml0eTEYMBYGA1UEAwwPbGliYXMyY29tbXVuaXR5
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv23qjQXKTECPAqZQfZ3c
    GAoB4G5BrVPtyTP9VwzosC7NVuyxfLN1hHfMhhdJWP+NFVwfBxMi2CI3qV46p3sk
    P687cUE+9Ls7IgwzSETt9M35zs7mPCrMOlt+F6LE/uYAIP0QjjHomaz0BnoI8gjs
    OgmO21clJPqxPIvA6quIl6wOPvNE+07++z3IyBXho1fHLlEXCyyMOYEBNO76e4QQ
    vDeJ42liD3+98pFiuR37J409CmIOc6VEsRjK4/sj30HbIo7rtfftejO5H5ToYstx
    Ibu5J0DJiS/+IohqQoiKIltTnZvuxkdz9IAKEMgkUOJaPeZMlgmVRjzIn6UzzRMu
    YwIDAQABo1MwUTAdBgNVHQ4EFgQU0bOhjydhvDlKaVL207SGNEbWSK0wHwYDVR0j
    BBgwFoAU0bOhjydhvDlKaVL207SGNEbWSK0wDwYDVR0TAQH/BAUwAwEB/zANBgkq
    hkiG9w0BAQsFAAOCAQEAuQG1OuQ2PcKtWgHWPrDkjrYK2kIeoyqMOettAHxYeaGx
    Zp/MPqzDeryVJKPrci38QKGL9X/mwi+Piz7bOWDC9xUIDumHMpASKQRlVv1pt9IF
    qBvqpHtTZz/Li+Q3EtLQTQ6Mo0+jmAL26Od/Om8UNFbDpNXJRr3MojkiH+7h+GSY
    k6pdpIqAv4aDA9zNlNwKE8UaHhYnJGCSAXn3JSoa2Lhhiy8po5AJYRaiSFSjTu5k
    WCyKXk2KS6SCqDTOq6P0a8ImeAQoZSGifTz/W6qKSoKdqa827Z3sLc7wQKfoVsxx
    KH+3xQYCRs3xnJNHna85ObsCqcD+uGgu4vyqZpiXig==`;

  const keyPem = `MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/beqNBcpMQI8C
    plB9ndwYCgHgbkGtU+3JM/1XDOiwLs1W7LF8s3WEd8yGF0lY/40VXB8HEyLYIjep
    XjqneyQ/rztxQT70uzsiDDNIRO30zfnOzuY8Ksw6W34XosT+5gAg/RCOMeiZrPQG
    egjyCOw6CY7bVyUk+rE8i8Dqq4iXrA4+80T7Tv77PcjIFeGjV8cuURcLLIw5gQE0
    7vp7hBC8N4njaWIPf73ykWK5HfsnjT0KYg5zpUSxGMrj+yPfQdsijuu19+16M7kf
    lOhiy3Ehu7knQMmJL/4iiGpCiIoiW1Odm+7GR3P0gAoQyCRQ4lo95kyWCZVGPMif
    pTPNEy5jAgMBAAECggEAUUNq95TGrRoW26wYrUrPPRE6fLixftALOIeuez7KpMgp
    eUYfjm1sbOCiXSYTiAlsLe4eadVwzEmyUV7kDWcUG0jbNhfZjvDQiIKfXoWMcoji
    DC3+xPnyGq/uVkBN2ltvIJHtbj+3m001hm5Vz9GD3ptiHrDe3tThWm+FZNmOsbun
    Oenop+uwcBwNa8xtz6cu8X2JvJLUoZtH6r1w4arToEtPcdYwnBjYPH55CGtrtDkO
    zv5oYgXi+8DCdDHeyax8JxCY7VHV7ukttR7HYzOu3lpyGb6C8wF3zfR3Oi1jyWKL
    kXF2TDbnhNNu6G5CY1znz3fw8wr5cEfIesRsG3krsQKBgQDruCvZdFzn3m6CmfwR
    crEV+LpzoJtrGZOX9gTNGscThIpSDDca9LEc4yg0ZMBI6Q48Cnzrt1lXAkIwibcP
    hAJGR+/Wfxesu6VWPWO8pw8l8bIyKnWdnKd0YcmJcsLszKgHhqbKZBQvFiQICm81
    0xqGEa644EG4wwuaMvzQwYUqGQKBgQDP5jxqB2ErcN8pGqi4DaOH0rr26Uvk4JV0
    5RgGBXN3CZYDjigkgPXFIK9ijNLG/0hh5utxz5JW9BoKSEOz0Qu47cEHJ6r0Vf8X
    i/Cy/TIVcxGOYkX65IPNDNTzilI3dLBHjai0xSmZup7Z8+V/iB7drLPVKWtAT8MK
    F1Ta7PTj2wKBgCFH1oEUSc2+/PFZllpMTC5i+Mg8g9UCPnF1HcZronHiA7mD2f/n
    Tl5awCFtnCxvI0Bc5rhNIcMEIZ5Cw2Lga4XKwFUTip8ruzNK5ZsMJzpfPp6QmhyP
    sqDe8ZqDZnwShSLS4xeuO59OS/YKqxr5XERTmMyndQAGIcw6qLE8sXV5AoGAHmPd
    ePJTNfJt7KhE+YKk4Png8q7vtSlPL6M9e8PYORJhp2tkWtGvG33HpXRIa2ZtwkHr
    MNvS3HsaQ16E5wgr2oCGsvENgGIBxANk0AVLCO+lJVcgO6ijy5mQl3kkw45/JjaC
    7snTZvFsvxdz+MyBFo5kS0iesOv36sW2VbpFofkCgYBql6ColSctfxvOeFo4E8CW
    p4jJirsQeDO87PE8XReE5mKlfyU1Q8t968MG5TOB7qpBAo/d8lSe+wUenL3PeIpI
    P4GDsj6VnJ8z12zgvxw2WhmAesyY9c/6FaILW4PuQPMK0MP0cGgH05JWtOWvMLsL
    arosjBabGx60rqU7PusXSQ==`;

  const pemToBytes = pem => new Uint8Array(Buffer.from(pem.replace(/\s/g, ''), 'base64')).buffer;

  const webcrypto = new Crypto();
  const crypto: SubtleCrypto = new pkijs.CryptoEngine({
    name: '@peculiar/webcrypto',
    crypto: webcrypto,
    subtle: webcrypto.subtle,
  });
  pkijs.setEngine('asdf', webcrypto, crypto);

  const data = Buffer.alloc(1024);
  webcrypto.getRandomValues(data);
  const certificate = new pkijs.Certificate({ schema: asn1js.fromBER(pemToBytes(certPem)).result });
  const keyAlgorithm = pkijs.getAlgorithmByOID(certificate.subjectPublicKeyInfo.algorithm.algorithmId);
  // keyAlgorithm is 1.2.840.113549.1.1.1 = RSAES-PKCS1-V1_5
  const privateKey = await crypto.importKey('pkcs8', pemToBytes(keyPem), keyAlgorithm, true, ['sign']);
  const hashAlgorithm = 'sha-256';
  const messageDigest = await crypto.digest({ name: hashAlgorithm }, data);

  const signed = new pkijs.SignedData({
    version: 1,
    encapContentInfo: new pkijs.EncapsulatedContentInfo({
      eContentType: '1.2.840.113549.1.7.1', // data
    }),
    signerInfos: [],
    certificates: [],
  });
  signed.certificates.push(certificate);
  signed.signerInfos.push(
    new pkijs.SignerInfo({
      sid: new pkijs.IssuerAndSerialNumber({
        issuer: certificate.issuer,
        serialNumber: certificate.serialNumber,
      }),
      signedAttrs: new pkijs.SignedAndUnsignedAttributes({
        type: 0,
        attributes: [
          new pkijs.Attribute({
            type: '1.2.840.113549.1.9.3', // contentType
            values: [
              new asn1js.ObjectIdentifier({
                value: '1.2.840.113549.1.7.1', // data
              }),
            ],
          }),
          new pkijs.Attribute({
            type: '1.2.840.113549.1.9.5', // signingTime
            values: [new asn1js.UTCTime({ valueDate: new Date() })],
          }),
          new pkijs.Attribute({
            type: '1.2.840.113549.1.9.4', // messageDigest
            values: [
              new asn1js.OctetString({
                valueHex: messageDigest,
              }),
            ],
          }),
        ],
      }),
    }),
  );

  // When attempting to sign data with this private key, PKI.js should attempt to do so with the RSASSA-PKCS1-V1_5 algorithm.
  // This algorithm is correctly identified in the return value of the call to getAlgorithmParameters('RSAES-PKCS1-V1_5', 'sign'),
  // but that portion of the return value is ignored in the switch statement in getSignatureParameters(). This is a bug.
  await signed.sign(privateKey, 0, hashAlgorithm, data);
benpetermorris commented 2 years ago

@microshine Do you need any more information? Thanks.

microshine commented 2 years ago

@benpetermorris Thank you for your example.

I updated your example. Looks like the right way of getting the signing algorithm is to use the getAlgorithmParameters function. It supports the operation argument which allows converting the RSAES-PKCS1-v1_5 name to RSASSA-PKCS1-v1_5

const keyAlgorithm = pkijs.getAlgorithmByOID(certificate.subjectPublicKeyInfo.algorithm.algorithmId);
const keySignAlgorithm = pkijs.getAlgorithmParameters(keyAlgorithm.name, "sign");
// keyAlgorithm is 1.2.840.113549.1.1.1 = RSAES-PKCS1-V1_5
const privateKey = await crypto.importKey("pkcs8", pemToBytes(keyPem), keySignAlgorithm.algorithm, true, ["sign"]);

P.S. But it's strange that the getAlgorithmParameters function returns RSASSA-PKCS1-v1_5 for the RSAES-PKCS1-v1_5 + encrypt

image
benpetermorris commented 2 years ago
const keySignAlgorithm = pkijs.getAlgorithmParameters(keyAlgorithm.name, "sign");

@microshine Thanks for this. I wasn't aware that importKey could be used successfully without specifying the OID of the algorithm identified by the key itself, i.e. 1.2.840.113549.1.1.1.

I'll close this PR.