MatrixAI / Polykey

Polykey Core Library
https://polykey.com
GNU General Public License v3.0
30 stars 4 forks source link

Encrypted private key PEM format #508

Closed tegefaulkes closed 1 year ago

tegefaulkes commented 1 year ago

Specification

In #506 we found that the uWebsockets library needed to read SSL keys and certs from the file system. Given this we need to write the cert chain PEM and private key PEM files to the file system so we can provide them to the web socket server.

To this end we need to generate a encrypted private key PEM format that the uWebsocket library can use. Ideally we can use the peculiar webcrypto, asn1-schema and asn1 libraries to generate this format.

Given the crypto ecosystem in JS and node and the level of required knowledge, this issue is pretty tricky to tackle.

Additional context

Tasks

  1. Generate a encrypted private key file in a format that the uWebsockets can load with a provided passphrase.
  2. Update the ClientServer to use this when starting the uWebsockets server.
CMCDragonkai commented 1 year ago

Reposting here:

Regarding the encryption of the private key for web socket server.

In order to encrypt the private into an encrypted PEM file, we need to follow the PKCS#5 standard (version 2) to produce an encrypted PKCS#8 pem file.

For example, in openssl, it has a pkcs8 subcommand. https://www.openssl.org/docs/man1.0.2/man1/pkcs8.html

In this subcommand there is a -v2 alg option. And in the comments it says:

The alg argument is the encryption algorithm to use, valid values include des, des3 and rc2. It is recommended that des3 is used.

So this will basically end using an algorithm specified in pkcs#5 v2 RFC. However it's not clear which algorithm this is, and that it's OID is going to be.

One way to solve this is to actually use the openssl pkcs8 command, and run it with a plaintext PKCS8 key. Using the -v2 des3 option, and you get back your PKCS8 encrypted PEM.

Subsequently you then just need to interrogate this file. Probably using another openssl command.

Actually in the updated version https://www.openssl.org/docs/man1.1.1/man1/openssl-pkcs8.html, it turns out the -v2 alg can be omitted, and the default cipher becomes AES256GCM.

So that means, we can just do the encryption using openssl pkcs8 command, then open up the file, and parse it, to get the algorithm identifier.

This identifier is going to be an OID. The OID is just a string that looks like 1.3.101.110. But we just need to know which one is for this one.

Use the ASN1 parser to see if you can open up the encrypted file and just console.log out the version identifier string.

Once you get the confirmed OID, we can actually proceed with building this encrypted pem file.

To do this, use https://github.com/PeculiarVentures/asn1-schema/blob/master/packages/pkcs8/src/encrypted_private_key_info.ts.

Then construct the EncryptedPrivateKeyInfo object, with the algorithm identifier. Because this identifier may not exist in any of asn1 libraries. The string can be defined in the utils if we need to do this. It should go into the keys/utils.

The next thing to do is to actually encrypt it. It is explained here.

https://datatracker.ietf.org/doc/html/rfc5208#section-6

We would reuse our own existing code:

  const pkcs8 = new asn1Pkcs8.PrivateKeyInfo({
    privateKeyAlgorithm: new asn1X509.AlgorithmIdentifier({
      algorithm: x509.idEd25519,
    }),
    privateKey: new asn1Pkcs8.PrivateKey(
      new asn1.OctetString(privateKey).toASN().toBER(),
    ),
  });

That provides us the private key info object.

The encryption process involves the following two steps:

 1. The private-key information is BER encoded, yielding an octet
    string.

 2. The result of step 1 is encrypted with the secret key to give
    an octet string, the result of the encryption process.

But what we actually need is something like:

new asn1.OctetString(pkcs8).toASN().toBER()

That then produces an arraybuffer.

Then that array buffer has to go through the encryption process of AES-256-GCM, whatever is specified as part of the OID we discover through openssl pkcs8.

The resulting function can be called privateKeyToPEMEncrypted. Then to be symmetric privateKeyFromPEMEncrypted.

Good thing is that our libsodium supports aes256gcm. So we should be able re-use symmetric algos. We currently have not exposed this from the sodium-native JS wrapper, so you'll need to have a look at that.

CMCDragonkai commented 1 year ago

I just looked at https://github.com/sodium-friends/sodium-native/blob/master/binding.c, it doesn't export the aes256gcm from libsodium. So the only alternative right now is the peculiar webcrypto which would have it.

However we need to create a new feature issue to replace webcrypto with whatever is possible in libsodium.

CMCDragonkai commented 1 year ago

Hopefully openssl here actually understands ed25519 keys. But I think your test would prove that it works. Also https://blog.pinterjann.is/ed25519-certificates.html

CMCDragonkai commented 1 year ago

Producing an encrypted PEM would be useful extra feature to the keys domain that other things could use as well.

So in terms of following openssl aes256gcm, this is therefore a secure default.

We are going to need to fork the sodium-native wrapper later in the future.

CMCDragonkai commented 1 year ago

Important: https://github.com/PeculiarVentures/asn1-schema/issues/82

CMCDragonkai commented 1 year ago

@tegefaulkes you can do this after you've fixed up the PR, this can be a second PR as it adds extra functionality to the keys domain that is then used by the websockets domain.

CMCDragonkai commented 1 year ago

See this comment https://github.com/MatrixAI/Polykey/issues/503#issuecomment-1445638011, this explains how we can do this.

The motivation for this whole thing is cause uWS doesn't seem to support in-memory key and certificate.

The uWS API is lacking alot of bindings that are actually available in the underlying code. So we might want to create our own uWS bindings in the future, and specifically bind to the uWS C code, then we can even just use Node's openssl instead.

Of course the whole thing is really about not relying on Node's version of websockets... so maybe boringssl can be used again. Not sure atm.

If we end up with needing an HTTP server, I think uWS does supply HTTP too.

That would mean something like js-ws just like js-quic. We're going lower and lower into the stack...

tegefaulkes commented 1 year ago

After switching to ws.js we now load the certs and keys from memory. There is no need for an encrypted PEM format now.

I'm closing this as a wontfix.

CMCDragonkai commented 1 year ago

I think there can be an issue created to enable export of our root key as encrypted PEM format. The above notes I wrote can still be useful if for whatever reason such a utility is needed in the future.

CMCDragonkai commented 1 year ago

Recreated as a feature request for #550.