Repository navigation
Exposing KeyObject fields vs. native JWK support #26854
Description
Activity
I think that
create***KeyandkeyObject.exportsupport for JWK as input and output is the endgame goal. One I would <3Constructing a PEM from a passed JWK is not hard, it's just annoying and it's something that is either error prone when done by each implementer or requires additional dependencies in the form of asn1.js (that in turn require ~7 more in the form of browser-ready version of assert, BN (doesn't use the built-in when available), and even that doesn't come with the actual asn.1 type definition, so again, prone to errors.
In addition to JWK support I could use .fields because i'd otherwise have to run
.exporton a passed in PEM/KeyObject to get to those values. This i think would save a roundtrip.For the record here are the bare bone JWK key types and their attributes. Props with
?are required when the key's a private one. All strings are base64url encoded with no padding.interface JWKOctKey { // createSecretKey kty: 'oct', k: string } interface JWKECKey { kty: 'EC' crv: 'P-256' | 'P-384' | 'P-521' x: string y: string d?: string } interface JWKOKPKey { kty: 'OKP' crv: 'Ed25519' | 'Ed448' | 'X25519' | 'X448' x: string d?: string } interface JWKRSAKey { // i think covers RSA-PSS keys too kty: 'RSA' e: string n: string d?: string p?: string q?: string dp?: string dq?: string qi?: string }Just realized the use case i have for implementing both.
In addition to JWK<>PEM two-way support we have the need to use x509 certificates in OpenID Connect/OAuth2.0 for binding tokens to mutual-TLS client certificates. We have a client established property on the server to check for in the certificate, either DN or SAN values. We also want to do x5c (chain) validation.
In those cases i'd pass the certificate(s) to
createPublicKeyand then use.fieldsto get to the subject, issuer, subject alternative names etc. That's something JWK support alone won't provide.So I'd like both.
/cc @omsmith
While JWK support could be cool, I don't think it precludes implementing fields.
JWK's kty field is defined by this registry: https://www.iana.org/assignments/jose/jose.xhtml#web-key-types
So node would be stuck making a decision about key types that aren't currently specified by JOSE, while the fields may still be valuable (including for an individual looking to make a non-standard JWK)
cc @nodejs/security-wg
In those cases i'd pass the certificate(s) to createPublicKey and then use .fields to get to the subject, issuer, subject alternative names etc. That's something JWK support alone won't provide.
The
KeyObjectAPI represents cryptographic keys, not certificates. If you pass a certificate, OpenSSL extracts thesubjectPublicKeyInfofield and uses it as the key. No other certificate information is retained as part of theKeyObject. Dealing with certificates is still an issue in node.So node would be stuck making a decision about key types that aren't currently specified by JOSE, while the fields may still be valuable (including for an individual looking to make a non-standard JWK)
I completely agree with you. I don't think we should support exporting those keys.
Reacted by Owen SmithThe
KeyObjectAPI represents cryptographic keys, not certificates. If you pass a certificate, OpenSSL extracts thesubjectPublicKeyInfofield and uses it as the key. No other certificate information is retained as part of theKeyObject. Dealing with certificates is still an issue in node.Understood, thank you for the clarification.
Another thing to consider are the names of the fields we are talking about. There are not really standardized names for all of the fields. JWK uses short keys that are close to the mathematical description of the properties. OpenSSL follows this scheme roughly, but some fields still have different names. RFCs for encoding these keys usually use different names that are easier to read:
d mod (p - 1)is calleddpin JWK,dmp1in OpenSSL andexponent1in PKCS#1.We had a similar discussion around the scrypt API which originally used parameters
N,randp, and we later added aliases (cost,blockSize,parallelization) to make the API more accessible. Similarly, I think I prefer such "verbose" names over abbreviations such asdpordmp1here, but we should discuss the naming scheme.Similarly, I think I prefer such "verbose" names over abbreviations such as dp or dmp1 here, but we should discuss the naming scheme.
I agree with a verbose naming scheme.
Reacted by Tobias NießenOne of the reasons users have requested this feature is to be able to implement JWK on top of the native crypto module without having to tap into OpenSSL. However, this would still only make that work in one direction (KeyObject → JWK), to create a KeyObject from JWK, a different API would be necessary to construct keys from their fields.
This makes sense, existing create APIs only create from ASN.1, but once
.fieldsexists, I'd expect that keys become round-trippable (eventually):priv0 = crypto.generateKeyPairSync(type, {...}) priv1 = crypto.createPrivateKey(priv0.fields)Another solution would be to natively support JWK. I am not an expert when it comes to JWK, but it shouldn't be difficult to implement as long as we don't need to include algorithm information in the key as WebCrypto does. This approach would extend create***Key and KeyObject.export with support for JWK.
Its not clear to me that "native jwk support" is necessarily an alternative to JWK (EDIT: I meant to ".fields"), did you intend to imply that? If the fields structure was identical to JWK, and
.fieldswas acceptable as an input tocreate[Private|Public|Secret]Key(), then these two options, "native JWK support" and a.fieldswould be unified, wouldn't they be?Just to clarify whether I understand this correctly, JOSE is https://jose.readthedocs.io/en/latest/, and has spun off sub-projects, JWK for key representation, JWS for signing, JWE for encryption.
And the relation of JOSE to the WebCrypto API is... what? It looks like WebCrypto implements lower-level cryptographic primitives than JWS and JWE, but that it supports JWK key import/export. Do I understand correctly?
Many node APIs predate the emerging the "standard" APIs, but there is a general drift in node towards exposing the standard APIs as appropriate (an individuals have the energy and time to implement). URL and Performance Timeline for example. I'd bet heavily that in 5 years we have a fetch API, some support for async streams, etc. The evolution is a bit jarring, but leaving aside questions of small-core, and whther we should do this, I have a question:
Would it be possible to expose the WebCrypto API as the primary crypto API? Does it define sufficient primitives, of a wide enough scope, or is its scope of application too limited? For example, I think it has only one-shot symetric encrypt/decrypt APIs, which makes it insufficiently general for non-browser use.
Regardless of future node API directions, I think we all agree on the basic principle: it's our intention that the crypto API evolve to provide sufficient functionality that web-standard (and other) crypto APIs can be implemented using
crypto, without native bindings, and if reasonably possible, without excessive use of BER/DER libraries. "excessive" and "reasonable" are obviously a bit "in the eye of the beholder". :-).WRT. naming: ideally, we would have long form "readable" names, where the names come from a recognizable standard (so we don't have to invent them). JWK is a recognizable standard, and its tempting to use its naming, but it looks problematic:
- Its not human readable: https://tools.ietf.org/html/rfc7517#appendix-A.3
- Its incomplete: it defines things only if applicable to its use (JSON sign/encrypt). For example, EC curve names are pretty limited, and it doesn't appear to have any DH/DSA support at all: https://www.iana.org/assignments/jose/jose.xhtml#web-key-types
The other options are:
- Use names from the PKCS1/SECG/etc ASN.1. They tend to be human readable, well-known, and close to camelCase.
- Use names from the OpenSSL API: They appear to be more of a mixture of mathematical and well-known, but have the advantage that if they are used, people who need to ask questions about their meaning or look up more information can go directly the OpenSSL API docs and/or mailing list
- Invent our own names. Pro: we can make them great (maybe?), con: people might have to read node_crypto.cc to figure out the maping from the name in our API to the name OpenSSL uses if they have more questions
- ...any others?
We'll live with this decision for a long time, so what I'd like to see if its possible (and I know this is a lot of work), is a table of equivalent names for the .field of all key types: JWK, ASN.1, WebCrypto (if this is a thing, it might not be), and OpenSSL API name. The goal would be to 1) pick the best name, and 2) make sure that .fields contains sufficient raw information that JWK, WebCrypto, and ASN.1 (though we already do this internally with OpenSSL) can be implemented on top of it. The second goal is critical, I assume if that's not achieved then
.fieldsjust won't be supporting its use-case. The table could eventually be provided in the docs, so its not just a use-once affair.Oh, and I've yet to be convinced that any of the raw crypto information should be exposed as numbers. The ASN.1 does not tend to do that. While we all know that some crypto operations are mathematical in nature (but not all, some is just bit operations like shift and XOR, and those aren't best done to numbers), that doesn't mean that users of the broken-out data are going to do math with them. They are mostly, effectively, opaque data that is shoved in and out of various structures, or passed to low-level crypto libs for use (and they may do math with them, but they didn't need or want BigInts in order to do that).
For this reason, and that most languages don't have arbitrary precision integers, key parts are usually treated as opaque data. I think JWK did the right thing here.
And the relation of JOSE to the WebCrypto API is... what? It looks like WebCrypto implements lower-level cryptographic primitives than JWS and JWE, but that it supports JWK key import/export. Do I understand correctly?
Yes. JOSE (JWA) restricts / profiles the primitives to form
algs - "algorithms", it e.g. strictly defines the saltLength to be used forRSASSA-PSSinto an JWA algorithm, e.g.PS256. But there is no relation between JOSE and WebCrypto. The only thing it shares is JWK key representation. And WebCrypto does not even support all of them (OKP).With the exception of node/openssl unsupported RSA-OAEP-256 you can see all the algorithm implementations using node's crypto here.
For clarity here are the final JOSE-family specs
- JSON Web Algorithms (JWA) - RFC7518
- JSON Web Signature (JWS) - RFC7515
- JSON Web Encryption (JWE) - RFC7516
- JSON Web Key (JWK) - RFC7517
- JSON Web Key (JWK) Thumbprint - RFC7638
- CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures JOSE - RFC8037
I'm not including JWT because it's just JWS profile using compact serialization and specific claim definition (type and processing), i'm also omitting JWS format extensions
Options 1 and 3 sound good to me,
- Use names from the PKCS1/SECG/etc ASN.1. They tend to be human readable, well-known, and close to camelCase.
- Invent our own names. Pro: we can make them great (maybe?), con: people might have to read node_crypto.cc to figure out the maping from the name in our API to the name OpenSSL uses if they have more questions
But frankly, since @tniessen clarified
.fieldsjust returns what's insubjectPublicKeyInfowhen cert is passed in I think it's not that useful anymore (the main additional use-case i wanted to cross off was dealing with certificates) considering we'd could have an API like so.const jwk = { kty: 'EC', crv: 'P-256', x: 'gskvlmd8hChm_hxtH4oyDC2rizV1jwK4exTn3icBxu8', y: '0El8NqTyybjkEJ2grVUDK1BdPo49OF1pmCJCD92_h-Y' }; const keyObject = createPublicKey(jwk); keyObject.export({ format: 'jwk' }); // jwk members equal to exported members
And we could forget about naming because JWK cut out the work for us and we're just supporting another key representation, not extending the API with new methods and properties per se.
Reacted by Daemon and Alex YangWould it be possible to expose the WebCrypto API as the primary crypto API? Does it define sufficient primitives, of a wide enough scope, or is its scope of application too limited? For example, I think it has only one-shot symetric encrypt/decrypt APIs, which makes it insufficiently general for non-browser use.
It is certainly possible to implement WebCrypto as part of node core, but I still believe that it is not a sufficient standard for non-browser applications. It has no streaming interface and there is a lot of overhead in each operation (e.g. by using
Promises for everything, even when synchronous operation would be much faster). There is also only a handful of operations and algorithms.The goal would be to 1) pick the best name, and 2) make sure that .fields contains sufficient raw information that JWK, WebCrypto, and ASN.1 (though we already do this internally with OpenSSL) can be implemented on top of it.
Locally, I have been using RFC names when possible. The fields are sufficient to be used to implement JWK on top of them, but if that is their primary use, we might be better off supporting JWK directly.
Oh, and I've yet to be convinced that any of the raw crypto information should be exposed as numbers. The ASN.1 does not tend to do that. While we all know that some crypto operations are mathematical in nature (but not all, some is just bit operations like shift and XOR, and those aren't best done to numbers), that doesn't mean that users of the broken-out data are going to do math with them. They are mostly, effectively, opaque data that is shoved in and out of various structures, or passed to low-level crypto libs for use (and they may do math with them, but they didn't need or want BigInts in order to do that).
For this reason, and that most languages don't have arbitrary precision integers, key parts are usually treated as opaque data. I think JWK did the right thing here.
So far, I sticked to the data types OpenSSL is using in its public APIs, and those are mostly BIGNUMs. If we decide to use strings instead, then
.fieldsis much closer to JWK than to actually providing useful data to users in my opinion.@panva I think that maybe eventually
const keyObject = createPublicKey(jwk); keyObject.export({ format: 'jwk' });could end up in core, but that the first most important step is to support the underlying key info that will allow you to implement that yourself.since @tniessen clarified .fields just returns what's in subjectPublicKeyInfo when cert is passed in I think it's not that useful anymore
I'm puzzled by this. For two reasons. 1, one of your primary use-cases of JWK import/export is met by
.fieldseven without an X.509 parse output not being in .fields (or so I thought). 2, X.509 certs are not keys, they are wrappers around keys. I don't think jamming full X.509 cert decodes into apublicKey.fieldsis helpful. Among other things, it means that when you import a key, what.fieldsit has depends on its original format, which is odd, if its encrypted/unencrypted, PKCS1/, SPKI, PFX, X.509-DER, or X.509-PEM should not affect the contents ofpublicKey.fields.I do think a X.509 decode (and, ultimately, support for the range of OpenSSL APIs for import/export/create of cert requests, certs, PKCS7 and CMS, PFX, etc.) would be desierable. X.509 decode is relatively easy, the code is there, I might do it soon, but TLS1.3 is hight priority ATM.
If we decide to use strings instead, then .fields is much closer to JWK than to actually providing useful data to users in my opinion.
Can you describe what use you think users would make of BigInts coming out of
.fields?The only uses I can think of is if they want to experiment with implementing, for example, cryptographic primitives (for example, ECMQV) that are not supported by Node.js. Or perhaps they are cryptography/math students, and want to directly implement some of the existing algorithms to explore how they are done. I've done this before, but with ruby arbitrary precision numbers, when I was trying to get a deeper understanding of RSA.
Both those use-cases are supported by outputting strings, though with a loss of efficiency since conversion to a JS numeric type is required. That conversion cost would be dwarfed by the actual overhead of most crypto algorithms, I think.
The most common use of the
.fieldswill be transport/import/export/display-to-humans of keys, and that is best done with strings. So, making the most common representation the default seems right to me.We could have our cake and eat it too by having
.fieldsand.fields.bigintor.fieldsand.numbers(those names suck, but just an example), or even.fields('numeric')and.fields('octet').Reacted by Daemon70 remaining items
- added a commit that references this issue
on Nov 20, 2020 - added a commit that references this issue
on Jan 8, 2021 With JWK export/import now being part of the
KeyObject.export/create*KeyAPIs andasymmetricKeyDetails, is there still a need for this kind of representation?There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.
For more information on how the project manages feature requests, please consult the feature request management document.
There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.
For more information on how the project manages feature requests, please consult the feature request management document.
- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Mar 24, 2022 There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.
For more information on how the project manages feature requests, please consult the feature request management document.
Now that node has a decent API to deal with cryptographic keys, users have requested features to access parts of keys, e.g. the modulus of an RSA key or the curve name of an EC key. @sam-github and I came up with the idea to add a
.fieldsproperty toKeyObjectwhich exposes those parts. Other users have encouraged this approach.I am currently working on a PR for that and the basics are working nicely:
One of the reasons users have requested this feature is to be able to implement JWK on top of the native crypto module without having to tap into OpenSSL. However, this would still only make that work in one direction (
KeyObject→ JWK), to create aKeyObjectfrom JWK, a different API would be necessary to construct keys from their fields.Another solution would be to natively support JWK. I am not an expert when it comes to JWK, but it shouldn't be difficult to implement as long as we don't need to include algorithm information in the key as WebCrypto does. This approach would extend
create***KeyandKeyObject.exportwith support for JWK.If JWK support is not the only reason to access parts of the key, it might still make sense to implement
.fieldssince JWK was designed for storing keys, not for interacting with its components. For example,.fieldscould make use of ES BigInts whereas JWK encodes everything as strings.So as I see it, there are four options: Implement both, implement one and not the other, or implement none of it. What do you think?
cc @nodejs/crypto @panva @mscdex