Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
crypto: add support for SM2 one-shot signatures
  • Loading branch information
tniessen committed Jan 25, 2021
commit 4282617daf5ab9c3aa71b88ca40f6a7e64b05db9
15 changes: 13 additions & 2 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -3637,6 +3637,9 @@ Throws an error if FIPS mode is not available.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: ???
description: Add support for SM2.
- version:
- v13.2.0
- v12.16.0
Expand All @@ -3653,7 +3656,7 @@ changes:

Calculates and returns the signature for `data` using the given private key and
algorithm. If `algorithm` is `null` or `undefined`, then the algorithm is
dependent upon the key type (especially Ed25519 and Ed448).
dependent upon the key type (especially Ed25519, Ed448, and SM2).

If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPrivateKey()`][]. If it is an object, the following
Expand All @@ -3674,6 +3677,9 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `sm2Identifier` {ArrayBuffer|Buffer|TypedArray|DataView} For SM2, this option
specifies the SM2 identifier. The same identifier must be specified during
verification.

### `crypto.timingSafeEqual(a, b)`
<!-- YAML
Expand Down Expand Up @@ -3709,6 +3715,9 @@ not introduce timing vulnerabilities.
<!-- YAML
added: v12.0.0
changes:
- version: REPLACEME
pr-url: ???
description: Add support for SM2.
- 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 @@ -3729,7 +3738,7 @@ changes:

Verifies the given signature for `data` using the given key and algorithm. If
`algorithm` is `null` or `undefined`, then the algorithm is dependent upon the
key type (especially Ed25519 and Ed448).
key type (especially Ed25519, Ed448, and SM2).

If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPublicKey()`][]. If it is an object, the following
Expand All @@ -3750,6 +3759,8 @@ additional properties can be passed:
`crypto.constants.RSA_PSS_SALTLEN_DIGEST` sets the salt length to the digest
size, `crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN` (default) sets it to the
maximum permissible value.
* `sm2Identifier` {ArrayBuffer|Buffer|TypedArray|DataView} For SM2, this option
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd prefer to make this a more generic name, e.g. just identifier perhaps.

specifies the SM2 identifier.

The `signature` argument is the previously calculated signature for the `data`.

Expand Down
22 changes: 20 additions & 2 deletions lib/internal/crypto/sig.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ function getIntOption(name, options) {
return undefined;
}

function getSm2Identifier(options) {
if (typeof options === 'object') {
const { sm2Identifier } = options;
if (sm2Identifier != null) {
return getArrayBufferOrView(sm2Identifier, 'sm2Identifier');
}
}
return undefined;
}

Sign.prototype.sign = function sign(options, encoding) {
if (!options)
throw new ERR_CRYPTO_SIGN_KEY_REQUIRED();
Expand Down Expand Up @@ -154,8 +164,12 @@ function signOneShot(algorithm, data, key) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);

// Option specific to SM2.
const sm2Identifier = getSm2Identifier(key);

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

function Verify(algorithm, options) {
Expand Down Expand Up @@ -225,6 +239,9 @@ function verifyOneShot(algorithm, data, key, signature) {
// Options specific to (EC)DSA
const dsaSigEnc = getDSASignatureEncoding(key);

// Option specific to SM2.
const sm2Identifier = getSm2Identifier(key);

if (!isArrayBufferView(signature)) {
throw new ERR_INVALID_ARG_TYPE(
'signature',
Expand All @@ -234,7 +251,8 @@ function verifyOneShot(algorithm, data, key, signature) {
}

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

module.exports = {
Expand Down
67 changes: 53 additions & 14 deletions src/crypto/crypto_sig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ void CheckThrow(Environment* env, SignBase::Error error) {
HandleScope scope(env->isolate());

switch (error) {
case SignBase::Error::kSignUnknownDigest:
case SignBase::Error::kSignInvalidDigest:
return THROW_ERR_CRYPTO_INVALID_DIGEST(env);

case SignBase::Error::kSignNotInitialised:
Expand Down Expand Up @@ -245,7 +245,7 @@ SignBase::Error SignBase::Init(const char* sign_type) {
}
const EVP_MD* md = EVP_get_digestbyname(sign_type);
if (md == nullptr)
return kSignUnknownDigest;
return kSignInvalidDigest;

mdctx_.reset(EVP_MD_CTX_new());
if (!mdctx_ || !EVP_DigestInit_ex(mdctx_.get(), md, nullptr)) {
Expand Down Expand Up @@ -513,6 +513,12 @@ void Verify::VerifyFinal(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(verify_result);
}

static bool DigestIsValidForKey(const ManagedEVPPKey& key, const EVP_MD* md) {
// OpenSSL 1.1.1 does not enforce this, but future versions will. See
// https://github.com/openssl/openssl/commit/ef077ba0d2fa28fd1b481f80335bda723
return EVP_PKEY_id(key.get()) != EVP_PKEY_SM2 || EVP_MD_type(md) == NID_sm3;
}

void Sign::SignSync(const FunctionCallbackInfo<Value>& args) {
ClearErrorOnReturn clear_error_on_return;
Environment* env = Environment::GetCurrent(args);
Expand All @@ -535,8 +541,8 @@ void Sign::SignSync(const FunctionCallbackInfo<Value>& args) {
} else {
const node::Utf8Value sign_type(args.GetIsolate(), args[offset + 1]);
md = EVP_get_digestbyname(*sign_type);
if (md == nullptr)
return crypto::CheckThrow(env, SignBase::Error::kSignUnknownDigest);
if (md == nullptr || !DigestIsValidForKey(key, md))
return crypto::CheckThrow(env, SignBase::Error::kSignInvalidDigest);
}

int rsa_padding = GetDefaultSignPadding(key);
Expand All @@ -555,15 +561,31 @@ void Sign::SignSync(const FunctionCallbackInfo<Value>& args) {
DSASigEnc dsa_sig_enc =
static_cast<DSASigEnc>(args[offset + 4].As<Int32>()->Value());

EVP_PKEY_CTX* pkctx = nullptr;
// Usually, we'd only need to allocate the EVP_MD_CTX ourselves and let
// OpenSSL take care of the EVP_PKEY_CTX in EVP_DigestSignInit, but to support
// SM2, we need to customize the EVP_PKEY_CTX before calling
// EVP_DigestSignInit.
EVPMDPointer mdctx(EVP_MD_CTX_new());
EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(key.get(), nullptr));
if (!mdctx || !pkctx) {
return crypto::CheckThrow(env, SignBase::Error::kSignInit);
}

if (!args[offset + 5]->IsUndefined()) {
ArrayBufferOrViewContents<char> sm2_id(args[offset + 5]);
if (EVP_PKEY_CTX_set1_id(pkctx.get(), sm2_id.data(), sm2_id.size()) <= 0)
return crypto::CheckThrow(env, SignBase::Error::kSignInit);
} else if (EVP_PKEY_id(key.get()) == EVP_PKEY_SM2) {
return THROW_ERR_MISSING_OPTION(env, "sm2Identifier is required");
}

EVP_MD_CTX_set_pkey_ctx(mdctx.get(), pkctx.get());

if (!mdctx ||
!EVP_DigestSignInit(mdctx.get(), &pkctx, md, nullptr, key.get())) {
if (EVP_DigestSignInit(mdctx.get(), nullptr, md, nullptr, key.get()) <= 0) {
return crypto::CheckThrow(env, SignBase::Error::kSignInit);
}

if (!ApplyRSAOptions(key, pkctx, rsa_padding, rsa_salt_len))
if (!ApplyRSAOptions(key, pkctx.get(), rsa_padding, rsa_salt_len))
return crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey);

const unsigned char* input =
Expand Down Expand Up @@ -614,8 +636,8 @@ void Verify::VerifySync(const FunctionCallbackInfo<Value>& args) {
} else {
const node::Utf8Value sign_type(args.GetIsolate(), args[offset + 2]);
md = EVP_get_digestbyname(*sign_type);
if (md == nullptr)
return crypto::CheckThrow(env, SignBase::Error::kSignUnknownDigest);
if (md == nullptr || !DigestIsValidForKey(key, md))
return crypto::CheckThrow(env, SignBase::Error::kSignInvalidDigest);
}

int rsa_padding = GetDefaultSignPadding(key);
Expand All @@ -634,14 +656,31 @@ void Verify::VerifySync(const FunctionCallbackInfo<Value>& args) {
DSASigEnc dsa_sig_enc =
static_cast<DSASigEnc>(args[offset + 5].As<Int32>()->Value());

EVP_PKEY_CTX* pkctx = nullptr;
// Usually, we'd only need to allocate the EVP_MD_CTX ourselves and let
// OpenSSL take care of the EVP_PKEY_CTX in EVP_DigestSignInit, but to support
// SM2, we need to customize the EVP_PKEY_CTX before calling
// EVP_DigestSignInit.
EVPMDPointer mdctx(EVP_MD_CTX_new());
if (!mdctx ||
!EVP_DigestVerifyInit(mdctx.get(), &pkctx, md, nullptr, key.get())) {
EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(key.get(), nullptr));
if (!mdctx || !pkctx) {
return crypto::CheckThrow(env, SignBase::Error::kSignInit);
}

if (!args[offset + 6]->IsUndefined()) {
ArrayBufferOrViewContents<char> sm2_id(args[offset + 6]);
if (EVP_PKEY_CTX_set1_id(pkctx.get(), sm2_id.data(), sm2_id.size()) <= 0)
return crypto::CheckThrow(env, SignBase::Error::kSignInit);
} else if (EVP_PKEY_id(key.get()) == EVP_PKEY_SM2) {
return THROW_ERR_MISSING_OPTION(env, "sm2Identifier is required");
}

EVP_MD_CTX_set_pkey_ctx(mdctx.get(), pkctx.get());

if (EVP_DigestVerifyInit(mdctx.get(), nullptr, md, nullptr, key.get()) <= 0) {
return crypto::CheckThrow(env, SignBase::Error::kSignInit);
}

if (!ApplyRSAOptions(key, pkctx, rsa_padding, rsa_salt_len))
if (!ApplyRSAOptions(key, pkctx.get(), rsa_padding, rsa_salt_len))
return crypto::CheckThrow(env, SignBase::Error::kSignPublicKey);

ByteSource sig_bytes = ByteSource::Foreign(sig.data(), sig.size());
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/crypto_sig.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class SignBase : public BaseObject {
public:
typedef enum {
kSignOk,
kSignUnknownDigest,
kSignInvalidDigest,
kSignInit,
kSignNotInitialised,
kSignUpdate,
Expand Down
1 change: 1 addition & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ void OnFatalError(const char* location, const char* message);
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, Error) \
V(ERR_MISSING_ARGS, TypeError) \
V(ERR_MISSING_OPTION, TypeError) \
V(ERR_MISSING_TRANSFERABLE_IN_TRANSFER_LIST, TypeError) \
V(ERR_MISSING_PASSPHRASE, TypeError) \
V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \
Expand Down
110 changes: 109 additions & 1 deletion test/parallel/test-crypto-sm2.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ if (!common.hasCrypto)

const assert = require('assert');
const {
generateKeyPairSync
generateKeyPairSync,
getHashes,
randomBytes,
sign,
verify
}= require('crypto');


Expand Down Expand Up @@ -34,3 +38,107 @@ const {
assert.strictEqual(ecdsaPublicKey.asymmetricKeyType, 'ec');
assert.strictEqual(ecdsaPrivateKey.asymmetricKeyType, 'ec');
}

{
// Test the basic behavior of SM2 signatures.

const sizes = [0, 10, 100, 1000];

for (const dataSize of [0, 10, 100, 1000]) {
const data = randomBytes(dataSize);

for (const idSize of [0, 10, 100, 1000]) {
const sm2Identifier = randomBytes(idSize);
const otherSm2Identifier = randomBytes(idSize === 0 ? 1 : idSize);

// Generate valid signatures.
const validSignatures = [null, 'SM3'].map((algo) => {
return sign(algo, data, {
key: sm2PrivateKey,
sm2Identifier
});
});

// Test the generated signatures.
for (const signature of validSignatures) {
for (const algo of [null, 'SM3']) {
// Ensure that signatures are validated correctly.
const signatureIsValid = verify(algo, data, {
key: sm2PublicKey,
sm2Identifier
}, signature);
assert.strictEqual(signatureIsValid, true);

// Providing a different identifier should cause verification to fail.
const isValidWithWrongId = verify(algo, data, {
key: sm2PublicKey,
sm2Identifier: otherSm2Identifier
}, signature);
assert.strictEqual(isValidWithWrongId, false);
}
}
}
}
}

{
// Test that no hash functions other than SM3 are allowed.

const data = randomBytes(100);
const options = {
key: sm2PrivateKey,
sm2Identifier: randomBytes(10)
};
const sigBuf = Buffer.alloc(10);

for (const hash of getHashes()) {
if (!hash.startsWith('sm3') && !hash.endsWith('SM3')) {
const args = [hash, data, options];
for (const fn of [() => sign(...args), () => verify(...args, sigBuf)]) {
assert.throws(fn, {
code: 'ERR_CRYPTO_INVALID_DIGEST',
name: 'TypeError'
});
}
}
}
}

{
// Test without identifier.

const possibleOptions = [
sm2PrivateKey,
{ key: sm2PrivateKey },
{ key: sm2PrivateKey, sm2Identifier: null }
];
const sigBuf = Buffer.alloc(10);

for (const options of possibleOptions) {
const args = [null, Buffer.alloc(0), options];
for (const fn of [() => sign(...args), () => verify(...args, sigBuf)]) {
assert.throws(fn, {
code: 'ERR_MISSING_OPTION',
message: 'sm2Identifier is required',
name: 'TypeError'
});
}
}
}

{
// Test invalid identifiers.

const key = sm2PrivateKey;
const sigBuf = Buffer.alloc(10);

for (const sm2Identifier of [true, false, 0, 1, () => {}, {}, []]) {
const args = [null, Buffer.alloc(0), { key, sm2Identifier }];
for (const fn of [() => sign(...args), () => verify(...args, sigBuf)]) {
assert.throws(fn, {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
});
}
}
}