-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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
crypto: add crypto.sign() and crypto.verify() #26611
Conversation
CI: https://ci.nodejs.org/job/node-test-pull-request/21456/ /cc @nodejs/crypto |
e1ee2d6
to
43be5b7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should consider other names? I think having such "one shot" APIs might be useful, also for hashing, but the names sound a bit awkward to me. Do we need to reserve the names sign
, verify
, hash
etc. for something else?
doc/api/crypto.md
Outdated
@@ -2651,6 +2651,18 @@ added: v10.0.0 | |||
Enables the FIPS compliant crypto provider in a FIPS-enabled Node.js build. | |||
Throws an error if FIPS mode is not available. | |||
|
|||
### crypto.signOneShot(data, privateKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC all other functions that consume keys take them as the first argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ordered them according to the ordinary streaming flow (.update(data)
followed by .sign(privateKey)
), which made sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that and it makes sense, but it deviates from all other functions.
doc/api/crypto.md
Outdated
added: REPLACEME | ||
--> | ||
* `data` {Buffer | TypedArray | DataView} | ||
* `privateKey` {Buffer | KeyObject} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deviates from other functions that consume keys, where they can also be string
s and certain object
s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Supporting string encoding names complicates things and makes separating required vs. optional function arguments nearly impossible as far as I can tell.
Besides, the only reason we have the string encoding parameters is for backwards compatibility and internally node immediately converts it to a Buffer
using Buffer.from()
, something userland could easily do themselves these days...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not talking about the data
, only about privateKey
. See e.g. privateDecrypt
:
If privateKey is not a KeyObject, this function behaves as if privateKey had been passed to crypto.createPrivateKey().
I designed that and I still think it is a good idea while maintaining compatibility with older API designs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it should be all or nothing, it doesn't make sense to have an encoding parameter for just one parameter when we're discussing backwards compatibility (.update()
also supports an encoding parameter).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I am not talking about the data
parameter and none of the existing APIs accept an encoding
option or argument for parsing the key, since PEM is ASCII-only.
As far as I can tell, your code already accepts strings and objects as the privateKey
, the documentation does not match that behavior, that's all I am trying to explain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc updated.
doc/api/crypto.md
Outdated
added: REPLACEME | ||
--> | ||
* `data` {Buffer | TypedArray | DataView} | ||
* `key` {Buffer | KeyObject} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
doc/api/crypto.md
Outdated
|
||
Calculates and returns the signature for `data` using the given private key. | ||
|
||
This method only supports Ed25519 and Ed448 keys. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason for this restriction? I think having these "one shot" implementations could solve problems such as #25857 where JS object creation time is critical.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the reason is to keep things simple and the fact that EdDSA does not utilize a hash algorithm argument with OpenSSL's API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lack of a digest arg and signing algorithm makes this very EdDSA specific, if its going to be that specific, might as well just call it what it is: crypto.eddsaSign()
. I'm not opposed to that, its a bit odd, but then, EdDSA made some odd algorithm design choices so it doesn't fit so well into generice asym sign/verify APIs. Using the alg-specific name would allow us to do a full one-shot API later.
My skim of the RFCs gave me the impression that there was PureEdDSA and EdDSA, but this API doesn't allow a choice between them. Not a blocker, but I'm curious, does OpenSSL not support them, or did I misunderstand?
I think that even if the other key types are left unsupported for now and only EdDSA is implemented, if there is a one-shot API, it should be equivalent to the multi-shot API:
createSign(digest)
.update(input, inputEncoding)
.sign(privateKey+padding+saltLength, outputEncoding)
Its unfortunate IMO that we are squeezing the signature algorithm (technically, the padding method, but most APIs consider the padding to be part of the signature algorithm) into the privateKey, but that's how it is.
crypto.signOneShot(digest, data, dataEncoding, privateKey, outputEncoding)
would be one way (with private Key being all the possibilities of Sign.sign()
), but using an options object might be nicer, particularly since all 5 args can be string
, so its not easy to just not provide them and have the API provide default values.
The one-shot version would be expected to have a null
for the digest
with EdDSA.
Same general comment on the verify function below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lack of a digest arg and signing algorithm makes this very EdDSA specific, if its going to be that specific, might as well just call it what it is:
crypto.eddsaSign()
There was interest shown earlier about possibly allowing an algorithm in the future somehow, so I made the name more generic. I honestly don't care so much about the name as long as it's fairly accurate and concise and doesn't look like it came from Java.
My skim of the RFCs gave me the impression that there was PureEdDSA and EdDSA, but this API doesn't allow a choice between them. Not a blocker, but I'm curious, does OpenSSL not support them, or did I misunderstand?
As noted in the referenced issue, OpenSSL implements PureEdDSA which does not support streaming data.
but using an options object might be nicer, particularly since all 5 args can be
string
, so its not easy to just not provide them and have the API provide default values.
Possibly, but the other reason for using an object is that having the encoding parameters be optional like they are in the streaming API would be very hard to detect all of the various possibly-string arguments.
As noted in other comments, I did not bother implementing the encoding parameters because they are part of a legacy API and using Buffer.from()
and buffer.toString()
are trivial these days and it greatly simplifies the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so that addresses most of my comments, but the plan for this is unclear:
There was interest shown earlier about possibly allowing an algorithm in the future somehow, so I made the name more generic. I honestly don't care so much about the name as long as it's fairly accurate and concise and doesn't look like it came from Java.
I don't know what Java's API conventions are, so this doesn't illuminate the kinds of names you like, for me at least.
I still don't love xxxOneShot()
as a naming convention, but I can live with it.
I don't think the API should have a generic name if it doesn't have enough args to one-shot a generic sign algorithm. digest
should be added as the first arg, IMO, if the name stays generic.
assert.strictEqual(sig.length, pair.sigLen); | ||
|
||
assert.strictEqual(crypto.verifyOneShot(data, pair.private, sig), true); | ||
assert.strictEqual(crypto.verifyOneShot(data, pair.public, sig), true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should also be tests for verification failures (e.g. using the wrong key, the wrong message, the wrong signature).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
I had to choose something.
I think having a static method that's named the same as an instance method could be confusing. |
43be5b7
to
000ccf0
Compare
000ccf0
to
e3af7fb
Compare
doc/api/crypto.md
Outdated
@@ -2651,6 +2651,21 @@ added: v10.0.0 | |||
Enables the FIPS compliant crypto provider in a FIPS-enabled Node.js build. | |||
Throws an error if FIPS mode is not available. | |||
|
|||
### crypto.signOneShot(data, key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just crypto.sign()
and crypto.verify()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previous comments: #26611 (review) and #26611 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mscdex:
I think having a static method that's named the same as an instance method could be confusing.
I think its the opposite of confusing, it makes it clear it does the same thing, but with no saved state (because there is no object to hold state), its clearly one-shot.
src/node_crypto.cc
Outdated
case EVP_PKEY_ED448: | ||
break; | ||
default: | ||
return env->ThrowError("Unsupported key type"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to do this check at the JavaScript layer? Either way, it would be great to have an ERR_*
code for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not reliably and not without crossing the JS<->C++ boundary an extra time. Is there a way to throw errors with codes set from C++?
EDIT: I see that there is. I could add a new error code I suppose, although nothing else in this source file utilizes error codes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added, but I noticed that the name
for error objects produced from C++ are not formatted the same as those generated from JS. JS includes the code in brackets after the error type. C++ does not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like an error code, if its not too much trouble. I have a PR almost done that adds a lot more error codes, but it didn't work everywhere, and I had to put it aside while doing other work. Every new throw of a string is something I'll have to convert to setting .code sometime in the future.
e3af7fb
to
61ca0f5
Compare
Marking this for not landing on v10.x/v11.x unless it's decided at some point that we don't support OpenSSL v1.1.0 or older for those branches. |
doc/api/crypto.md
Outdated
@@ -2651,6 +2651,21 @@ added: v10.0.0 | |||
Enables the FIPS compliant crypto provider in a FIPS-enabled Node.js build. | |||
Throws an error if FIPS mode is not available. | |||
|
|||
### crypto.signOneShot(data, key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mscdex:
I think having a static method that's named the same as an instance method could be confusing.
I think its the opposite of confusing, it makes it clear it does the same thing, but with no saved state (because there is no object to hold state), its clearly one-shot.
doc/api/crypto.md
Outdated
|
||
Calculates and returns the signature for `data` using the given private key. | ||
|
||
This method only supports Ed25519 and Ed448 keys. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lack of a digest arg and signing algorithm makes this very EdDSA specific, if its going to be that specific, might as well just call it what it is: crypto.eddsaSign()
. I'm not opposed to that, its a bit odd, but then, EdDSA made some odd algorithm design choices so it doesn't fit so well into generice asym sign/verify APIs. Using the alg-specific name would allow us to do a full one-shot API later.
My skim of the RFCs gave me the impression that there was PureEdDSA and EdDSA, but this API doesn't allow a choice between them. Not a blocker, but I'm curious, does OpenSSL not support them, or did I misunderstand?
I think that even if the other key types are left unsupported for now and only EdDSA is implemented, if there is a one-shot API, it should be equivalent to the multi-shot API:
createSign(digest)
.update(input, inputEncoding)
.sign(privateKey+padding+saltLength, outputEncoding)
Its unfortunate IMO that we are squeezing the signature algorithm (technically, the padding method, but most APIs consider the padding to be part of the signature algorithm) into the privateKey, but that's how it is.
crypto.signOneShot(digest, data, dataEncoding, privateKey, outputEncoding)
would be one way (with private Key being all the possibilities of Sign.sign()
), but using an options object might be nicer, particularly since all 5 args can be string
, so its not easy to just not provide them and have the API provide default values.
The one-shot version would be expected to have a null
for the digest
with EdDSA.
Same general comment on the verify function below.
/cc @nodejs/collaborators |
61ca0f5
to
4179a02
Compare
I've added a commit now that adds support for all key types. Thoughts @nodejs/collaborators ? New CI: |
src/node_crypto.cc
Outdated
return CheckThrow(env, SignBase::Error::kSignPrivateKey); | ||
} | ||
|
||
// XXX(mscdex): unnecessary? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// XXX(mscdex): unnecessary? | |
// TODO(mscdex): unnecessary? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and just removed the line of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since don't love the names but code LGTM otherwise
4179a02
to
4152ec1
Compare
Thank you @mscdex ... I know those names weren't your preference but I definitely appreciate you being willing to accommodate. |
@sam-github With the recent changes do your concerns still stand? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, sorry it took a few days to get to re-reviewing.
* `crypto.constants.RSA_PKCS1_PADDING` (default) | ||
* `crypto.constants.RSA_PKCS1_PSS_PADDING` | ||
|
||
Note that `RSA_PKCS1_PSS_PADDING` will use MGF1 with the same hash function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather than repeating the docs from the streaming API, you could link to them. Or not. This isn't even a nit, just something to consider. I tend to try not to repeat docs, and emphasizing the relationship between the multi- and single-shot APIs might be worthwhile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. My only concern is that we are reinforcing the mixture of arguments and options objects, especially in crypto.verify
where the options are not the last parameter, but I am not even sure whether that is actually a problem. It will be if create***Key
ever accepts options that clash with options other crypto APIs accept.
I share the concern above, but it applies equally to the existing APIs. When we come up with a solution, it should be applied uniformly to both APIs. In the meantime, this API is internally consistent. |
7c60eb3
to
1d25465
Compare
d76b1e3
to
109c17f
Compare
These methods are added primarily to allow signing and verifying using Ed25519 and Ed448 keys, which do not support streaming of input data. However, any key type can be used with these new APIs, to allow better performance when only signing/verifying a single chunk. Fixes: nodejs#26320 PR-URL: nodejs#26611 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rod Vagg <rod@vagg.org> Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
109c17f
to
7d0e50d
Compare
These methods are added primarily to allow signing and verifying using Ed25519 and Ed448 keys, which do not support streaming of input data. However, any key type can be used with these new APIs, to allow better performance when only signing/verifying a single chunk. Fixes: #26320 PR-URL: #26611 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rod Vagg <rod@vagg.org> Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
These methods are to allow signing and verifying using Ed25519 and Ed448 keys, which do not support streaming of input data.
Fixes: #26320
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes