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 optional callback to crypto.sign and crypto.verify #37500

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -3822,10 +3822,13 @@ 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.sign(algorithm, data, key)`
### `crypto.sign(algorithm, data, key[, callback])`
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37500
description: Optional callback argument added.
- version:
- v13.2.0
- v12.16.0
Expand All @@ -3837,7 +3840,10 @@ changes:
* `algorithm` {string | null | undefined}
* `data` {ArrayBuffer|Buffer|TypedArray|DataView}
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey}
* Returns: {Buffer}
* `callback` {Function}
* `err` {Error}
* `signature` {Buffer}
* Returns: {Buffer} if the `callback` function is not provided.
<!--lint enable maximum-line-length remark-lint-->

Calculates and returns the signature for `data` using the given private key and
Expand All @@ -3864,6 +3870,8 @@ additional properties can be passed:
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.

If the `callback` function is provided this function uses libuv's threadpool.

### `crypto.timingSafeEqual(a, b)`
<!-- YAML
added: v6.6.0
Expand Down Expand Up @@ -3894,10 +3902,13 @@ Use of `crypto.timingSafeEqual` does not guarantee that the *surrounding* code
is timing-safe. Care should be taken to ensure that the surrounding code does
not introduce timing vulnerabilities.

### `crypto.verify(algorithm, data, key, signature)`
### `crypto.verify(algorithm, data, key, signature[, callback])`
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37500
description: Optional callback argument added.
- version: v15.0.0
pr-url: https://github.com/nodejs/node/pull/35093
description: The data, key, and signature arguments can also be ArrayBuffer.
Expand All @@ -3913,7 +3924,12 @@ changes:
* `data` {ArrayBuffer| Buffer|TypedArray|DataView}
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey}
* `signature` {ArrayBuffer|Buffer|TypedArray|DataView}
* Returns: {boolean}
* `callback` {Function}
* `err` {Error}
* `result` {boolean}
* Returns: {boolean} `true` or `false` depending on the validity of the
signature for the data and public key if the `callback` function is not
provided.
<!--lint enable maximum-line-length remark-lint-->

Verifies the given signature for `data` using the given key and algorithm. If
Expand Down Expand Up @@ -3945,6 +3961,8 @@ The `signature` argument is the previously calculated signature for the `data`.
Because public keys can be derived from private keys, a private key or a public
key may be passed for `key`.

If the `callback` function is provided this function uses libuv's threadpool.

### `crypto.webcrypto`
<!-- YAML
added: v15.0.0
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/crypto/dsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
KeyObjectHandle,
SignJob,
kCryptoJobAsync,
kSigEncDER,
kKeyTypePrivate,
kSignJobModeSign,
kSignJobModeVerify,
Expand Down Expand Up @@ -254,7 +255,8 @@ function dsaSignVerify(key, data, algorithm, signature) {
normalizeHashName(key.algorithm.hash.name),
undefined, // Salt-length is not used in DSA
undefined, // Padding is not used in DSA
signature));
signature,
kSigEncDER));
}

module.exports = {
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/crypto/ec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const {
kKeyTypePublic,
kSignJobModeSign,
kSignJobModeVerify,
kSigEncP1363,
} = internalBinding('crypto');

const {
Expand Down Expand Up @@ -470,7 +471,8 @@ function ecdsaSignVerify(key, data, { name, hash }, signature) {
hashname,
undefined, // Salt length, not used with ECDSA
undefined, // PSS Padding, not used with ECDSA
signature));
signature,
kSigEncP1363));
}

module.exports = {
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/crypto/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const {
} = require('internal/errors');

const {
validateInt32,
validateUint32,
} = require('internal/validators');

Expand Down Expand Up @@ -342,7 +343,7 @@ function rsaSignVerify(key, data, { saltLength }, signature) {
// TODO(@jasnell): Validate maximum size of saltLength
// based on the key size:
// Math.ceil((keySizeInBits - 1)/8) - digestSizeInBytes - 2
validateUint32(saltLength, 'algorithm.saltLength');
validateInt32(saltLength, 'algorithm.saltLength', -2);
}

const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
Expand Down
117 changes: 96 additions & 21 deletions lib/internal/crypto/sig.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
FunctionPrototypeCall,
ObjectSetPrototypeOf,
ReflectApply,
} = primordials;
Expand All @@ -14,17 +15,22 @@ const {
} = require('internal/errors');

const {
validateCallback,
validateEncoding,
validateString,
} = require('internal/validators');

const {
Sign: _Sign,
SignJob,
Verify: _Verify,
signOneShot: _signOneShot,
verifyOneShot: _verifyOneShot,
kCryptoJobAsync,
kSigEncDER,
kSigEncP1363,
kSignJobModeSign,
kSignJobModeVerify,
} = internalBinding('crypto');

const {
Expand All @@ -34,12 +40,18 @@ const {
} = require('internal/crypto/util');

const {
preparePublicOrPrivateKey,
createPrivateKey,
createPublicKey,
isCryptoKey,
isKeyObject,
preparePrivateKey,
preparePublicOrPrivateKey,
} = require('internal/crypto/keys');

const { Writable } = require('stream');

const { Buffer } = require('buffer');

const {
isArrayBufferView,
} = require('internal/util/types');
Expand Down Expand Up @@ -131,31 +143,62 @@ Sign.prototype.sign = function sign(options, encoding) {
return ret;
};

function signOneShot(algorithm, data, key) {
function signOneShot(algorithm, data, key, callback) {
if (algorithm != null)
validateString(algorithm, 'algorithm');

if (callback !== undefined)
validateCallback(callback);

data = getArrayBufferOrView(data, 'data');

if (!key)
throw new ERR_CRYPTO_SIGN_KEY_REQUIRED();

const {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase
} = preparePrivateKey(key);

// Options specific to RSA
const rsaPadding = getPadding(key);
const pssSaltLength = getSaltLength(key);

// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);

return _signOneShot(keyData, keyFormat, keyType, keyPassphrase, data,
algorithm, rsaPadding, pssSaltLength, dsaSigEnc);
if (!callback) {
const {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase
} = preparePrivateKey(key);

return _signOneShot(keyData, keyFormat, keyType, keyPassphrase, data,
algorithm, rsaPadding, pssSaltLength, dsaSigEnc);
}

let keyData;
if (isKeyObject(key) || isCryptoKey(key)) {
({ data: keyData } = preparePrivateKey(key));
} else if (key != null && (isKeyObject(key.key) || isCryptoKey(key.key))) {
({ data: keyData } = preparePrivateKey(key.key));
} else {
keyData = createPrivateKey(key)[kHandle];
}
Copy link
Member

Choose a reason for hiding this comment

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

Can't this block be replaced with a call to preparePrivateKey? Maybe I'm missing something.

Copy link
Member Author

@panva panva Mar 10, 2021

Choose a reason for hiding this comment

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

It cannot, SignJob expects a KeyObjectHandle and in case the key input is not already an existing key object abstraction (the first two if blocks) then preparePrivateKey (or preparePublicOrPrivateKey) returns the raw data instead of a handle.


const job = new SignJob(
kCryptoJobAsync,
kSignJobModeSign,
keyData,
data,
algorithm,
pssSaltLength,
rsaPadding,
undefined,
dsaSigEnc);

job.ondone = (error, signature) => {
if (error) return FunctionPrototypeCall(callback, job, error);
FunctionPrototypeCall(callback, job, null, Buffer.from(signature));
};
job.run();
}

function Verify(algorithm, options) {
Expand Down Expand Up @@ -197,10 +240,13 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) {
rsaPadding, pssSaltLength, dsaSigEnc);
};

function verifyOneShot(algorithm, data, key, signature) {
function verifyOneShot(algorithm, data, key, signature, callback) {
if (algorithm != null)
validateString(algorithm, 'algorithm');

if (callback !== undefined)
validateCallback(callback);

data = getArrayBufferOrView(data, 'data');

if (!isArrayBufferView(data)) {
Expand All @@ -211,13 +257,6 @@ function verifyOneShot(algorithm, data, key, signature) {
);
}

const {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase
} = preparePublicOrPrivateKey(key);

// Options specific to RSA
const rsaPadding = getPadding(key);
const pssSaltLength = getSaltLength(key);
Expand All @@ -233,8 +272,44 @@ function verifyOneShot(algorithm, data, key, signature) {
);
}

return _verifyOneShot(keyData, keyFormat, keyType, keyPassphrase, signature,
data, algorithm, rsaPadding, pssSaltLength, dsaSigEnc);
if (!callback) {
const {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase
} = preparePublicOrPrivateKey(key);

return _verifyOneShot(keyData, keyFormat, keyType, keyPassphrase,
signature, data, algorithm, rsaPadding,
pssSaltLength, dsaSigEnc);
}

let keyData;
if (isKeyObject(key) || isCryptoKey(key)) {
({ data: keyData } = preparePublicOrPrivateKey(key));
} else if (key != null && (isKeyObject(key.key) || isCryptoKey(key.key))) {
({ data: keyData } = preparePublicOrPrivateKey(key.key));
} else {
keyData = createPublicKey(key)[kHandle];
}
panva marked this conversation as resolved.
Show resolved Hide resolved

const job = new SignJob(
kCryptoJobAsync,
kSignJobModeVerify,
keyData,
data,
algorithm,
pssSaltLength,
rsaPadding,
signature,
dsaSigEnc);

job.ondone = (error, result) => {
if (error) return FunctionPrototypeCall(callback, job, error);
FunctionPrototypeCall(callback, job, null, result);
};
job.run();
}

module.exports = {
Expand Down
Loading