kjur / jsrsasign

The 'jsrsasign' (RSA-Sign JavaScript Library) is an opensource free cryptography library supporting RSA/RSAPSS/ECDSA/DSA signing/validation, ASN.1, PKCS#1/5/8 private/public key, X.509 certificate, CRL, OCSP, CMS SignedData, TimeStamp, CAdES and JSON Web Signature/Token in pure JavaScript.
https://kjur.github.io/jsrsasign
Other
3.27k stars 643 forks source link

Inconsistent EC private key pem files generated using KEYUTIL #549

Closed irririki closed 2 years ago

irririki commented 2 years ago

node: v16.13.2 jsrsasign: "^10.5.13"

I am trying to serialize an EC private key to a PEM file, using the code:

  const keypair = ecdsa.generateKeyPairHex();  
  const prvhex = keypair.ecprvhex;  
  const prvObj = new KJUR.crypto.ECDSA({'curve': 'secp256r1', 'prv': prvhex});  
  const prvPem = KEYUTIL.getPEM(prvObj, "PKCS8PRV");  

The generated PEM file is like

-----BEGIN PRIVATE KEY-----
MEgCAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcELjAsAgEBBCC3Hni7RYBybchchuaw
NCTxK1R3nQUt/bvJyLEw6/lEe6EFAwMAAAA=
-----END PRIVATE KEY-----

I tried to use Python Cryptography to cross verify the generated PEM file and got an error saying 'Could not deserialize key data. The data may be in an incorrect format'.

By inspecting the prvObj, I found that the object have the following values: {type: 'EC', isPrivate: true, isPublic: false, and public key is null: pubKeyHex: null

Then I tried a different approach to generate the PEM file:

const ecKeypair = KEYUTIL.generateKeypair("EC", "secp256r1");
  const prvKeyObj = ecKeypair.prvKeyObj;
  const pubKeyObj = ecKeypair.pubKeyObj;
  const prvKeyPem = KEYUTIL.getPEM(prvKeyObj, "PKCS8PRV");

This time the generated PEM file is like:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKshhe8RjIaRkXVQ4
o9nSdtA6jJwZY5m/Oa2frt9FjEKhRANCAAT2MPCApcxYznYsMizEWohNiPHk61Ci
xMwH0zT+P/lm3h8m+4e+MbwWD3I3WjMw7iBhuAi6hQrLotADMoq041Gf
-----END PRIVATE KEY-----

And it can be crossed verified using Python Cryptography library. The prvKeyObj has the following values: type: 'EC', isPrivate: true, isPublic: false and both prvKeyHex and pubKeyHex are set.

Out of curiosity, I set the public key hex to the private key object as in the first approach like:

  const keypair = ecdsa.generateKeyPairHex();
  const prvhex = keypair.ecprvhex;
  const pubhex = keypair.ecpubhex;
  const prvObj = new KJUR.crypto.ECDSA({'curve': 'secp256r1', 'prv': prvhex});
  prvObj.setPublicKeyHex(pubhex); // set public key hex
  const prvPem = KEYUTIL.getPEM(prvObj, "PKCS8PRV");

This time it generates a correct PEM file that can be verified. However, the object has the values as follows: type: 'EC', isPrivate: true, isPublic: true and both prvKeyHex and pubKeyHex are set. Note that both isPrivate and isPublic are true this time.

Further more, I used the incorrect PEM file in the CSR generation like:

const csr = new KJUR.asn1.csr.CertificationRequest({
        subject: {str:"/CN=test"},
        sbjpubkey: publicKeyPem,
        sbjprvkey: privateKeyPem,
        sigalg: "SHA256withECDSA"
      });
const pem = csr.getPEM();

It produced a valid CSR file just like using the correct private key PEM file which is weird but understandable.

If the last point is within the tolerance, the first PEM file definitely looks like a bug to me.

kjur commented 2 years ago

Your first EC key seems to have wrong optional public key field:

SEQUENCE {
  INTEGER 0
  SEQUENCE {
    OBJECT IDENTIFIER ecPublicKey (1 2 840 10045 2 1)
    OBJECT IDENTIFIER prime256v1 (1 2 840 10045 3 1 7)
    }
  OCTET STRING, encapsulates {
    SEQUENCE {
      INTEGER 1
      OCTET STRING
        B7 1E 78 BB 45 80 72 6D C8 5C 86 E6 B0 34 24 F1
        2B 54 77 9D 05 2D FD BB C9 C8 B1 30 EB F9 44 7B
      [1] {
        BIT STRING
          '0000000000000000'B // ****** WRONG PUBLIC KEY
        }
      }
    }
  }

Please use following for workaround:

const keypair = KEYUTIL.generateKeypair("EC", "secp256r1");
const prmPem = KEYUTIL.getPEM(keypair.prvKeyObj, "PKCS8PRV");

This will be much easier and working fine.

irririki commented 2 years ago

The first method is a valid route that leads to wrongly generated PEM using KEYUTIL.getPEM. I am aware of the second method which produces the correct result as mentioned in the description. But should method 1 be fixed since it's there?

kjur commented 2 years ago

It was fixed in the 10.5.14 release today.

When a public key is not provided as you do, optional public key field in a PKCS#8 will be omitted after this fix.

const keypair = ecdsa.generateKeyPairHex();  
const prvhex = keypair.ecprvhex;  
const prvObj = new KJUR.crypto.ECDSA({'curve': 'secp256r1', 'prv': prvhex});  
const prvPem = KEYUTIL.getPEM(prvObj, "PKCS8PRV");