Skip to content
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

Merged
merged 1 commit into from
Mar 29, 2019

Conversation

mscdex
Copy link
Contributor

@mscdex mscdex commented Mar 12, 2019

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), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@mscdex mscdex added crypto Issues and PRs related to the crypto subsystem. lts-watch-v10.x labels Mar 12, 2019
@nodejs-github-bot nodejs-github-bot added the c++ Issues and PRs that require attention from people who are familiar with C++. label Mar 12, 2019
@mscdex
Copy link
Contributor Author

mscdex commented Mar 12, 2019

CI: https://ci.nodejs.org/job/node-test-pull-request/21456/

/cc @nodejs/crypto

@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch from e1ee2d6 to 43be5b7 Compare March 12, 2019 14:30
Copy link
Member

@tniessen tniessen left a 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?

@@ -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)
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

added: REPLACEME
-->
* `data` {Buffer | TypedArray | DataView}
* `privateKey` {Buffer | KeyObject}
Copy link
Member

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 strings and certain objects.

Copy link
Contributor Author

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...

Copy link
Member

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.

Copy link
Contributor Author

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).

Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc updated.

added: REPLACEME
-->
* `data` {Buffer | TypedArray | DataView}
* `key` {Buffer | KeyObject}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


Calculates and returns the signature for `data` using the given private key.

This method only supports Ed25519 and Ed448 keys.
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@mscdex mscdex Mar 16, 2019

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.

Copy link
Contributor

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);
Copy link
Member

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 12, 2019

Maybe we should consider other names?

I had to choose something.

Do we need to reserve the names sign, verify, hash etc. for something else?

I think having a static method that's named the same as an instance method could be confusing.

@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch from 43be5b7 to 000ccf0 Compare March 12, 2019 16:55
@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch from 000ccf0 to e3af7fb Compare March 13, 2019 09:52
@@ -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)
Copy link
Member

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()

Copy link
Member

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)

Copy link
Contributor

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.

case EVP_PKEY_ED448:
break;
default:
return env->ThrowError("Unsupported key type");
Copy link
Member

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.

Copy link
Contributor Author

@mscdex mscdex Mar 13, 2019

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 14, 2019

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.

@@ -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)
Copy link
Contributor

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.


Calculates and returns the signature for `data` using the given private key.

This method only supports Ed25519 and Ed448 keys.
Copy link
Contributor

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.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 18, 2019

/cc @nodejs/collaborators

@mscdex
Copy link
Contributor Author

mscdex commented Mar 25, 2019

I've added a commit now that adds support for all key types.

Thoughts @nodejs/collaborators ?

New CI: https://ci.nodejs.org/job/node-test-pull-request/21871/ https://ci.nodejs.org/job/node-test-pull-request/21875/

@nodejs-github-bot
Copy link
Collaborator

return CheckThrow(env, SignBase::Error::kSignPrivateKey);
}

// XXX(mscdex): unnecessary?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// XXX(mscdex): unnecessary?
// TODO(mscdex): unnecessary?

Copy link
Contributor Author

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.

Copy link
Member

@jasnell jasnell left a 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

@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch from 4179a02 to 4152ec1 Compare March 25, 2019 15:44
@nodejs-github-bot
Copy link
Collaborator

@jasnell
Copy link
Member

jasnell commented Mar 26, 2019

Thank you @mscdex ... I know those names weren't your preference but I definitely appreciate you being willing to accommodate.

@mscdex
Copy link
Contributor Author

mscdex commented Mar 26, 2019

@sam-github With the recent changes do your concerns still stand?

Copy link
Contributor

@sam-github sam-github left a 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
Copy link
Contributor

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.

Copy link
Member

@tniessen tniessen left a 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.

@sam-github
Copy link
Contributor

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.

@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch from 7c60eb3 to 1d25465 Compare March 28, 2019 17:47
@mscdex mscdex added the semver-minor PRs that contain new features and should be released in the next minor version. label Mar 28, 2019
@nodejs-github-bot
Copy link
Collaborator

@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch 4 times, most recently from d76b1e3 to 109c17f Compare March 28, 2019 23:01
@nodejs-github-bot
Copy link
Collaborator

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>
@mscdex mscdex force-pushed the crypto-sign-verify-eddsa-support branch from 109c17f to 7d0e50d Compare March 29, 2019 02:00
@mscdex mscdex merged commit 7d0e50d into nodejs:master Mar 29, 2019
@mscdex mscdex deleted the crypto-sign-verify-eddsa-support branch March 29, 2019 11:39
BethGriggs pushed a commit that referenced this pull request Apr 5, 2019
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants