RustCrypto / formats

Cryptography-related format encoders/decoders: DER, PEM, PKCS, PKIX
228 stars 122 forks source link

How do I create a valid openssl DER or PEM file using pkcs8? #1349

Closed gshuflin closed 4 months ago

gshuflin commented 4 months ago

This is a continuation of some of the issues previously discussed in this thread: https://github.com/RustCrypto/formats/issues/1221

I have an example repo here: https://github.com/gshuflin/generate-pkcs8-format-key that uses the ed25519 and pkcs8 crates to generate ED25519 keypairs, and write them to files. Regardless of whether I output PEM or DER format files, or whether I use ed25519_zebra or ed25519_dalek as the underlying library, the files generated are invalid as far as openssl is concerned, yielding output such as:

$ openssl pkcs8 -in output-dalek.der -topk8
Enter pass phrase for output-dalek.der:
Could not read key from output-dalek.der
8005FA0401000000:error:1608010C:STORE routines:ossl_store_handle_load_result:unsupported:crypto/store/store_result.c:151:

How do I correctly use these rust crates to generate valid PEM or DER keyfiles?

tarcieri commented 4 months ago

You aren't using either library's support for encoding PKCS#8 private keys, instead you're handrolling your own here and it's broken:

https://github.com/gshuflin/generate-pkcs8-format-key/blob/master/src/main.rs#L50-L60

I already stepped you through the required encoding here:

https://github.com/RustCrypto/formats/issues/1221#issuecomment-1912721338

Note that the private key is encoded as a nested OCTET STRING.

Instead of handrolling the encoding, you can just use ed25519-dalek's built-in support, which is documented here (which could arguably be better):

https://docs.rs/ed25519-dalek/latest/ed25519_dalek/#pkcs8-key-encoding

SigningKey impls the EncodePrivateKey trait: https://docs.rs/ed25519-dalek/latest/ed25519_dalek/struct.SigningKey.html#impl-EncodePrivateKey-for-SigningKey

You can just call SigningKey::to_pkcs8_der here to encode the key as DER: https://github.com/gshuflin/generate-pkcs8-format-key/blob/master/src/main.rs#L32

gshuflin commented 4 months ago

I've updated my repo with a new method that calls write_pkcs8_[der|pem]_file from the ed25519_dalek::pkcs8::EncodePrivateKey trait (https://github.com/gshuflin/generate-pkcs8-format-key/commit/7846087f4a09558070eaf51f9abae349bc52c27f) I'm still seeing the following error:

$ openssl pkcs8 -topk8 -in new-dalek.der
Could not read key from new-dalek.der
80C5730201000000:error:1608010C:STORE routines:ossl_store_handle_load_result:unsupported:crypto/store/store_result.c:151:

I also note that these methods do not offer an argument to input an encryption password, so I'm not sure how I would generate an encrypted version of these generated ED25519 keys.

tarcieri commented 4 months ago

It's possible that OpenSSL lacks support for PKCS#8 v2 keys which are specified in RFC8410 Section 7, which is what ed25519-dalek implements.

Here is what your example generates:

$ openssl asn1parse -in new-dalek.pem
    0:d=0  hl=2 l=  81 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :01
    5:d=1  hl=2 l=   5 cons: SEQUENCE
    7:d=2  hl=2 l=   3 prim: OBJECT            :ED25519
   12:d=1  hl=2 l=  34 prim: OCTET STRING      [HEX DUMP]:042015C1EA86B9108DB710ED44ED9A32469742E62BA5DCF308775DAE955FF60E1C70
   48:d=1  hl=2 l=  33 prim: cont [ 1 ]

Here is a key generated by OpenSSL:

$ openssl genpkey -algorithm ed25519 -out privkey.pem
$ openssl asn1parse -in privkey.pem
    0:d=0  hl=2 l=  46 cons: SEQUENCE
    2:d=1  hl=2 l=   1 prim: INTEGER           :00
    5:d=1  hl=2 l=   5 cons: SEQUENCE
    7:d=2  hl=2 l=   3 prim: OBJECT            :ED25519
   12:d=1  hl=2 l=  34 prim: OCTET STRING      [HEX DUMP]:0420A47446132E82EADC1CCEABE2E015A41B1135980146D246352A3B3EE8C13157A4

The main differences are the lower version and the absence of a public key.

If you remove the public key from the serialization, OpenSSL may be able to parse it. Note that this does not conform to RFC8410.

tarcieri commented 4 months ago

The ed25519::pkcs8::KeypairBytes type makes the public_key field an Option, so you can just set it to None.

tarcieri commented 4 months ago

I wasn't able to get OpenSSL to parse a PKCS#8 v2 example from RFC8410, but it is able to parse the PKCS#8 v1 example.

The RFC includes the following note:

NOTE: There exist some private key import functions that have not picked up the new ASN.1 structure OneAsymmetricKey that is defined in RFC7748]. This means that they will not accept a private key structure that contains the public key field. This means a balancing act needs to be done between being able to do a consistency check on the key pair and widest ability to import the key.

So arguably ed25519-dalek should produce PKCS#8 v1 by default (a.k.a. OpenSSL is why we can't have nice things)

gshuflin commented 4 months ago

I've updated my example repo to manually convert the ed25519_dalek::SigningKey bytes into a KeypairBytes, and invoke the file-writing methods on that: https://github.com/gshuflin/generate-pkcs8-format-key/commit/02701d608f61a25f711e2ddb36e005423579018f

With these new keys, I can run:

$ openssl pkey -in new-dalek.der
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEII8hYgQsBf887pTJqi1E641YxhuSDvHwTezaH0iklGMr
-----END PRIVATE KEY-----
$ openssl pkey -in new-dalek.der -pubout
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA8d+4e60CoP/Lj5EaSC2uEwdeu89H97FfbZksMX4mMUw=
-----END PUBLIC KEY-----

Interestingly, if I run:

$ openssl pkcs8 -in new-dalek.der -topk8
Enter Encryption Password:
Verifying - Enter Encryption Password:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAi5N5rFy/E3xgICCAAw
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEHvicoJK+HA2OHJT9XwKLg4EQH7T
SqMxqHhbUUvGw3l6fCmzEaOf6Y2fHA5pMmrbYzzLOL8OS5ITmqJ27y3AW4XIOr9k
2q0BcGFY/dsIoybe/G4=
-----END ENCRYPTED PRIVATE KEY-----

It prompts me for an encryption password, and then accepts any password at all, before printing out the following output. I'm still not sure how I would specify a PKCS5 password in code when I generate the key. I'm also not sure I understand the difference between PKCS8 v1 and v2, or the difference between running openssl pkey and openssl pkcs8 -topk8

tarcieri commented 4 months ago
$ openssl pkcs8 -in new-dalek.der -topk8

You're giving it an unencrypted key, it's prompting for a password, and then producing an encrypted PKCS#8 document.

Adding -nocrypt will make it operate on unencrypted PKCS#8 keys.

Otherwise it looks like it's working.

I'm also not sure I understand the difference between PKCS8 v1 and v2, or the difference between running openssl pkey and openssl pkcs8 -topk8

PKCS#8 v2 includes a public key in addition to a private key (i.e. a keypair).

openssl pkey produces the public key, whereas openssl pkcs8 -topk8 produces a private key, and unless -nocrypt is specified, an encrypted one.

Aside from making a tracking issue on dalek-cryptography to suggest using PKCS#8 v1 for OpenSSL interop, I think this issue can be closed.