-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support parsing pem key only once for crypto api #15113
Comments
I don't like passing an opaque object to the |
@seishun This get a bit problematic to use when you are streaming data as what it seems the current API is build for. I'm guessing the hash is being incrementally calculated when doing the 'write' calls and the 'sign' call then encrypts that to provide the signature. The example below shows a sample where using a shared sign object would give an unexpected result: const crypto = require('crypto');
// Create shared sign object
const sign = crypto.createSign('RSA-SHA256', getPrivateKeySomehow());
// Start taking requests.
const server = http.createServer((req, res) => {
req.on('data', (chunk) => {
// Here we are mixing data between the requests
sign.write(chunk);
}).on('end', () => {
sign.end();
response.end(sign.sign(privateKey, 'hex'))
});
... Most other languages have a "parsed key" class as there are many different ways signing can be done, fx. both C# and Java can use different underlying signing implementations fx. when using HSM devices or running on other platforms where the key material is not a available to application. Fx. C# uses windows certificate store on windows, keychain on mac and openssl on Linux. Java use a hardware backed keystore on Android, etc. Having someone kind of abstraction would make the whole key handling a lot more flexible, fx. imagine using a hardware backed keystore for storage of the private key. For inspiration here is how the API looks in Java, it's similar to Node with the exception that you pass a key object so it's only constructed once. PrivateKey privatekey = customPemToPrivateKey(getPrivateKeySomehow())
for(let i = 0; i < 10000; i++) {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privatekey);
signature.update(String.getBytes("some data to sign" + i, StandardCharsets.UTF_8));
byte[] signatureBytes = signature.sign();
} And here in C# where the "key" in the focal point of the interface, so the "key" object has a sign function. SHA256 sha256 = SHA256.Create();
RSACryptoServiceProvider privatekey = customPemToCryptoProvider(getPrivateKeySomehow())
for(let i = 0; i < 10000; i++) {
byte[] signatureBytes = privatekey.SignData(Encoding.UTF8.GetBytes("some data to sign" + i), sha256);
} |
cc @nodejs/crypto I'd like to revive the idea. After having worked on the crypto module a lot recently, I think having key objects could actually be advantageous for us:
I am currently trying to assess some API drafts, I'll let you know when I have a proposal. |
@tniessen this would also enable us to work with keys stored outside the nodejs process, fx. in keychain on mac, certificate store on Windows, GNOME Keyring on linux, TPM (Trusted Platform Module) or HSM's (Hardware Security Module) like Yubikey, TouchID, smartcards, etc. Fx. if we stick with openssl it can be extended with an engine config: https://developers.yubico.com/YubiHSM2/Usage_Guides/OpenSSL_with_pkcs11_engine.html |
@tlbdk Will that require support for custom key objects (user-defined classes)? That would make a secure implementation more difficult. |
@tniessen No, not if the implementation detail is left to OpenSSL fx using its engine API to add the different HSM backends, it already has the needed abstraction. There is already API for setting the engine: https://nodejs.org/api/crypto.html#crypto_crypto_setengine_engine_flags But we need an API to load keys from engines:
fx.
The key object class could have the same the suggested conversion methods but would just return null when the engine does give access to the underlying key data. |
@tlbdk Awesome, thanks for the clarification. |
@tniessen Looks really good, thanks for working on this :) |
Key objects have landed. Please let us know whether there is a feature you would like to see that is still missing. (Loading keys from engines is not supported yet.) |
Would be nice when it's available for the key object and should be fairly simple with the EVP_PKEY_get* methods: I guess an api to work directly with public and private exponents, etc. by fx creating the RSA object directly with EVP_PKEY_set would also make some things simpler:
At the moment I have done the code to convert a JWK to PEM in zero dependency javascript, but parsing PEM without an ASN.1 library would be a lot more work: |
Btw did a quick test with the new key object API for an RSA-SHA256 verification and it about twice as fast as parsing the PEM key on every run, nice work @tniessen :)
|
@tniessen Brilliant work, thank you!
At the moment, after obtaining a Hence i'd like to see the ability to
This would enable native JOSE (JWK, JWS, JWA, etc) implementation with no dependencies and the possibility to create queryable key sets to be able to get a key from an imported key set (such as authorization server's or OAuth2.0 client's Libraries capable of this, e.g. node-jose rely heavily on pure JS implementation like forge to do key format conversion in order to pass the keys to asn.1 parsers (again pure JS) to get this information, making the whole process heavy on dependencies and slow as well. (sorry for re-posting my reply) |
I 100% agree with @panva. |
Thank you for your kind feedback! I have been discussing ways to access the individual properties with @sam-github. I believe our preferred way of doing this would be via a |
@panva @davidgtonge, I don't think exposing the undelaying ASN.1 tags would be the best option as this is very format dependent, fx if the key is loaded from an engine or if support for loading JWK keys directly is added in the future. Having a fields property fx with generic names inspired by JWK would be a nicer solution. @tniessen Would the OpenSSL key API not help with this, it seems to be fairly little code? And why would it be linked to TLS certificate handing(note I have not looked at the code so I might just be missing something obvious)? That node-jose is dependencies heavy and slow is not because of limitations in Node or a missing key object API, fx. we are doing all the needed JOSE stuff to integrate with Google and Microsofts OpenID Connect providers down to Node.js 6.x with zero dependencies, our IdP only has two extra dependencies to convert PEM keys to JWK, but if we were using JWK's as base it would be zero. For PEM to JWK: We have some sample code that shows how to do this with very little code: SP for Azure AD that loads the keys from an openid configuration endpoint: SP for Google that loads keys from googles JWK endpoint: POC IdP that's missing the key export parts, but that would not be very much code: |
@tlbdk if JWK format was native I'd agree, to some extent. But it isn't and is likely further out than being able to access the key's underlying asn.1 - which at least internally will be needed to support pem to JWK conversion anyway, won't it? Looking even at the samples i don't see any of them being zero dependency, the
"all needed" to do simple client integration is just a small portion of JOSE The moment you step out of a simple one way integration, e.g. clients that produce signed assertions, encrypt request content for the provider, decrypt content from the provider, or providers needing to nail down a key to use from a loaded keystore with specific RS bitsize or EC curve, having access to keyobject's properties is a godsend. The big target here is indeed native JWK support, so let's maybe shoot for that one
but at the same time being able to get a |
@panva Right now Node.js is using OpenSSL to do the PEM parsing and that internally is doing the ASN.1 decoding to a struct that has the key components, the struct can also manually be populated or read into for any other format/engines OpenSSL supports. The API's I linked gives access to the struct so it should be fairly simple to expose that in the key object. But I have not seen an API's exposing the ASN.1 parts without having to decode the same key twice, I did not look that hard so it might be there. My point was just that ASN.1 is an encoding detail better to not expose in an API. Generating PEM without dependencies is fairly easy if you have the key components. Adding support for converting JWK to PEM in crypto.createPrivateKey or crypto.createPublicKey would be super easy. Would be happy to do a pull request that does exactly that if it would be accepted, @tniessen ? The pretty option would be populating the OpenSSL key struct directly in C world. jwkutils.js should support all the key formats in JWK, am I missing any? That would sort:
For exporting to JWK I guess with a bit of cleanup we could get it down to 2-3 dependencies with an ASN.1 library or zero with only doing enough of the ASN.1 decoding to support PEM, but it would be quite a bit of work. Key components from the OpenSSL struct would make this so much easier. It's a very small portion of JOSE I have actually seen used in the wild so that was my definition of "needed" for now :). Deriving a PublicKeyObject from PrivateKeyObject would be useful and also calculating the public key fingerprint would also be nice. |
It certainly does, the implementation itself is quite simple. The API design is what we should consider carefully.
TLS already provides ways to access certain properties of the key, but I think @sam-github and I agreed that the property names being used there are not necessarily elegant choices, so I'd be okay with breaking compatibility there.
This is on my list of upcoming features, I am trying to split them into small incremental changes to make our internal processes easier. Eventually, |
I think if we attach the key properties to a Its worth bike-shedding the name since it will live a long time. I mildly lean towards |
So I agree with @panva that in an ideal world it would be great to get core JWK support. I think node.js and JWK go well together. For the majority of use-cases having the features mentioned above:
Would remove the need to even access a Having the key properties in a
I have to say I agree with @tlbdk . At the keyObject level I shouldn't care whether the key was encoded with |
Agreed, generic field names make more sense. |
@sam-github @panva @tlbdk Would you expect BIGNUM fields to be exposed as hexadecimal strings or as JS bigints? |
@tniessen I would say Bigint for output and Bigint and Buffer for input |
Using JWK names is insteresting, if possible. They are described in rfc7518 if anyone is looking (took me a while to find them). If we go that way, wouldn't it make sense to have the property values be JSON compatible as they are in JWK, so they would be hex strings on output, and bigint/buffer/hexstrings on input? |
@sam-github if the properties follow jwk names then the same format would indeed make more sense. |
@tniessen IIRC you thought there was some difficulty with JWK support, was it around the key usage fields? They all appear optional to output, and node would have to ignore them on input. |
Key usage fields (key_ops and use) are optional use by a given application. Node just needs to ignore all fields that aren’t the actual key components. |
That and the problem of Bigint support in JWK. |
@tniessen what issue with bigint support? AFAIK all key material is encoded as |
@panva I mean that Node.js should optimally output bigints, but JWK does not seem to support that (since JSON does not). |
Current API parses the PEM files on every crypto operation, fx:
node/src/node_crypto.cc
Line 4168 in 0d22858
I would be nice if this could be done once and a ref to the openssl key could be retained:
The text was updated successfully, but these errors were encountered: