From 5f44396a9e144383045063102e4b83277db480fa Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Tue, 18 Apr 2023 21:04:51 +0900 Subject: [PATCH] fix(ext/node): implement crypto.createVerify (#18703) --- cli/tests/node_compat/config.json | 1 + .../parallel/test-crypto-update-encoding.js | 29 +++++++ cli/tests/unit_node/crypto_sign_test.ts | 80 ++++++++++++------- ext/node/crypto/mod.rs | 54 ++++++++++++- ext/node/lib.rs | 1 + ext/node/polyfills/internal/crypto/sig.ts | 72 ++++++++++++----- 6 files changed, 188 insertions(+), 49 deletions(-) create mode 100644 cli/tests/node_compat/test/parallel/test-crypto-update-encoding.js diff --git a/cli/tests/node_compat/config.json b/cli/tests/node_compat/config.json index e77f008761..59670057a5 100644 --- a/cli/tests/node_compat/config.json +++ b/cli/tests/node_compat/config.json @@ -236,6 +236,7 @@ "test-crypto-hmac.js", "test-crypto-prime.js", "test-crypto-secret-keygen.js", + "test-crypto-update-encoding.js", "test-crypto-x509.js", "test-dgram-close-during-bind.js", "test-dgram-close-signal.js", diff --git a/cli/tests/node_compat/test/parallel/test-crypto-update-encoding.js b/cli/tests/node_compat/test/parallel/test-crypto-update-encoding.js new file mode 100644 index 0000000000..253de4e765 --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-crypto-update-encoding.js @@ -0,0 +1,29 @@ +// deno-fmt-ignore-file +// deno-lint-ignore-file + +// Copyright Joyent and Node contributors. All rights reserved. MIT license. +// Taken from Node 18.12.1 +// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually + +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const crypto = require('crypto'); + +const zeros = Buffer.alloc; +const key = zeros(16); +const iv = zeros(16); + +const cipher = () => crypto.createCipheriv('aes-128-cbc', key, iv); +const decipher = () => crypto.createDecipheriv('aes-128-cbc', key, iv); +const hash = () => crypto.createSign('sha256'); +const hmac = () => crypto.createHmac('sha256', key); +const sign = () => crypto.createSign('sha256'); +const verify = () => crypto.createVerify('sha256'); + +for (const f of [cipher, decipher, hash, hmac, sign, verify]) + for (const n of [15, 16]) + f().update(zeros(n), 'hex'); // Should ignore inputEncoding. diff --git a/cli/tests/unit_node/crypto_sign_test.ts b/cli/tests/unit_node/crypto_sign_test.ts index 1d3d2b3ece..1016d0f3e8 100644 --- a/cli/tests/unit_node/crypto_sign_test.ts +++ b/cli/tests/unit_node/crypto_sign_test.ts @@ -1,7 +1,10 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -import { assertEquals } from "../../../test_util/std/testing/asserts.ts"; -import { createSign } from "node:crypto"; +import { + assert, + assertEquals, +} from "../../../test_util/std/testing/asserts.ts"; +import { createSign, createVerify } from "node:crypto"; import { Buffer } from "node:buffer"; const rsaPrivatePem = Buffer.from( @@ -9,38 +12,61 @@ const rsaPrivatePem = Buffer.from( new URL("./testdata/rsa_private.pem", import.meta.url), ), ); +const rsaPublicPem = Buffer.from( + await Deno.readFile( + new URL("./testdata/rsa_public.pem", import.meta.url), + ), +); + +const table = [ + { + algorithms: ["sha224", "RSA-SHA224"], + signature: + "7ad162b288bd7f4ba9b8a31295ad4136d143a5fd11eb99a72379dc9b53e3e8b5c1b7c9dd8a3864a1f626d921e550c48056982bd8fe7e75333885311b5515de1ecbbfcc6a1dd930f422dff87bfceb7eb38882ac6b4fd9dea9efd462776775976e81b1d677f8db41f5ac8686abfa9838069125be939c59e404aa50550872d84befb8b5f6ce2dd051c62a8ba268f876b6f17a27af43b79938222e4ab8b90c4f5540d0f8b02508ef3e68279d685746956b924f00c92438b7981a3cfcb1e2a97305402d381ea62aeaa803f8707961bc3e10a258352e210772e9846ca4024e3dc0a956a50d6db1c03d2943826cc98c6f36d7bafacf1c94b6c438c7664c300a3be172b1", + }, + { + algorithms: ["sha256", "RSA-SHA256"], + signature: + "080313284d7398e1e0e27f6e44f198ceecedddc801e81af63a867d9245ad744e29018099c9ac3c27061c33cabfe27af1db38f44bac09cdcd2c4ab3b00a2a3020f68368f2239db5f911a2dbb7ea2dee322ca7d26d0c88d197482ca4aa1c29ac87b9e6c20075dc974ae71d2d76d2a5b2a15bd541033519465c3aea815cc73b0f1c3ffeedcfb93d6788416623789f86786870d23e86b982ab0df157d7a596097bd3cca3e752f3f47eff4b83754296868b52bc8ff741492dc8a401fe6dc035569e45d1fa1a71c8988d3aadce68fb1bf5c3e756c586af20c8e75c037436ff4c8389e6ce9d943ef7e2566977b84577272181fcec403077cc29e7db1166fff900b36a1d", + }, + { + algorithms: ["sha384", "RSA-SHA384"], + signature: + "2f77a5b7ac0168efd652c30ecb082075f3de30629e9c1f51b7e7e671f24b5c3a2606bb72159a217438220fc7aaba887d4b817e3f43fe0cc8f840747368df8cd65ec760c21a3f9296d01caedc80a335030e31d31ac451277fc4bcc1679c168b2c3185dfee21286514113c080af5238a61a677b03777344f476f25053108588aa6bdc02a6138c6b59a20de4d11e3d668482f17e748e75747f83c0512206283acfc64ed0ad963dddc9ec24589cfd459ee806b8e0e67b93cea16651e967762a5deef890f438ffb9db39247469289db06e2ed7fe262aa1df4ab9607e5b5219a17ddc9694283a61bf8643f58fd702f2c5d3b2d53dc7f36bb5e96461174d376950d6d19", + }, + { + algorithms: ["sha512", "RSA-SHA512"], + signature: + "072e20a433f255ab2f7e5e9ce69255d5c6d7c15a36af75c8389b9672c41abc6a9532fbd057d9d64270bb2483d3c9923f8f419fba4b59b838dcda82a1322009d245c06e2802a74febaea9cebc0b7f46f8761331c5f52ffb650245b5aefefcc604f209b44f6560fe45370cb239d236622e5f72fbb45377f08a0c733e16a8f15830897679ad4349d2e2e5e50a99796820302f4f47881ed444aede56a6d3330b71acaefc4218ae2e4a3bdfbb0c9432ffc5e5bac8c168278b2205d68a5d6905ccbb91282d519c11eccca52d42c86787de492b2a89679dce98cd14c37b0c183af8427e7a1ec86b1ed3f9b5bebf83f1ef81eb18748e69c716a0f263a8598fe627158647", + }, +]; Deno.test({ name: "crypto.Sign - RSA PEM with SHA224, SHA256, SHA384, SHA512 digests", fn() { - const table = [ - { - algorithms: ["sha224", "RSA-SHA224"], - expected: - "7ad162b288bd7f4ba9b8a31295ad4136d143a5fd11eb99a72379dc9b53e3e8b5c1b7c9dd8a3864a1f626d921e550c48056982bd8fe7e75333885311b5515de1ecbbfcc6a1dd930f422dff87bfceb7eb38882ac6b4fd9dea9efd462776775976e81b1d677f8db41f5ac8686abfa9838069125be939c59e404aa50550872d84befb8b5f6ce2dd051c62a8ba268f876b6f17a27af43b79938222e4ab8b90c4f5540d0f8b02508ef3e68279d685746956b924f00c92438b7981a3cfcb1e2a97305402d381ea62aeaa803f8707961bc3e10a258352e210772e9846ca4024e3dc0a956a50d6db1c03d2943826cc98c6f36d7bafacf1c94b6c438c7664c300a3be172b1", - }, - { - algorithms: ["sha256", "RSA-SHA256"], - expected: - "080313284d7398e1e0e27f6e44f198ceecedddc801e81af63a867d9245ad744e29018099c9ac3c27061c33cabfe27af1db38f44bac09cdcd2c4ab3b00a2a3020f68368f2239db5f911a2dbb7ea2dee322ca7d26d0c88d197482ca4aa1c29ac87b9e6c20075dc974ae71d2d76d2a5b2a15bd541033519465c3aea815cc73b0f1c3ffeedcfb93d6788416623789f86786870d23e86b982ab0df157d7a596097bd3cca3e752f3f47eff4b83754296868b52bc8ff741492dc8a401fe6dc035569e45d1fa1a71c8988d3aadce68fb1bf5c3e756c586af20c8e75c037436ff4c8389e6ce9d943ef7e2566977b84577272181fcec403077cc29e7db1166fff900b36a1d", - }, - { - algorithms: ["sha384", "RSA-SHA384"], - expected: - "2f77a5b7ac0168efd652c30ecb082075f3de30629e9c1f51b7e7e671f24b5c3a2606bb72159a217438220fc7aaba887d4b817e3f43fe0cc8f840747368df8cd65ec760c21a3f9296d01caedc80a335030e31d31ac451277fc4bcc1679c168b2c3185dfee21286514113c080af5238a61a677b03777344f476f25053108588aa6bdc02a6138c6b59a20de4d11e3d668482f17e748e75747f83c0512206283acfc64ed0ad963dddc9ec24589cfd459ee806b8e0e67b93cea16651e967762a5deef890f438ffb9db39247469289db06e2ed7fe262aa1df4ab9607e5b5219a17ddc9694283a61bf8643f58fd702f2c5d3b2d53dc7f36bb5e96461174d376950d6d19", - }, - { - algorithms: ["sha512", "RSA-SHA512"], - expected: - "072e20a433f255ab2f7e5e9ce69255d5c6d7c15a36af75c8389b9672c41abc6a9532fbd057d9d64270bb2483d3c9923f8f419fba4b59b838dcda82a1322009d245c06e2802a74febaea9cebc0b7f46f8761331c5f52ffb650245b5aefefcc604f209b44f6560fe45370cb239d236622e5f72fbb45377f08a0c733e16a8f15830897679ad4349d2e2e5e50a99796820302f4f47881ed444aede56a6d3330b71acaefc4218ae2e4a3bdfbb0c9432ffc5e5bac8c168278b2205d68a5d6905ccbb91282d519c11eccca52d42c86787de492b2a89679dce98cd14c37b0c183af8427e7a1ec86b1ed3f9b5bebf83f1ef81eb18748e69c716a0f263a8598fe627158647", - }, - ]; - for (const testCase of table) { for (const algorithm of testCase.algorithms) { - const signature = createSign(algorithm).update("some data to sign") + const signature = createSign(algorithm) + .update("some data to sign") .sign(rsaPrivatePem, "hex"); - assertEquals(signature, testCase.expected); + assertEquals(signature, testCase.signature); + } + } + }, +}); + +Deno.test({ + name: "crypto.Verify - RSA PEM with SHA224, SHA256, SHA384, SHA512 digests", + fn() { + for (const testCase of table) { + for (const algorithm of testCase.algorithms) { + assert( + createVerify(algorithm).update("some data to sign").verify( + rsaPublicPem, + testCase.signature, + "hex", + ), + ); } } }, diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index f818b96af5..b45f361448 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -309,7 +309,7 @@ pub fn op_node_sign( use signature::hazmat::PrehashSigner; let key = match key_format { "pem" => RsaPrivateKey::from_pkcs8_pem((&key).try_into()?) - .map_err(|_| type_error("Invalid RSA key"))?, + .map_err(|_| type_error("Invalid RSA private key"))?, // TODO(kt3k): Support der and jwk formats _ => { return Err(type_error(format!( @@ -353,6 +353,58 @@ pub fn op_node_sign( } } +#[op] +fn op_node_verify( + digest: &[u8], + digest_type: &str, + key: StringOrBuffer, + key_type: &str, + key_format: &str, + signature: &[u8], +) -> Result { + match key_type { + "rsa" => { + use rsa::pkcs1v15::VerifyingKey; + use signature::hazmat::PrehashVerifier; + let key = match key_format { + "pem" => RsaPublicKey::from_public_key_pem((&key).try_into()?) + .map_err(|_| type_error("Invalid RSA public key"))?, + // TODO(kt3k): Support der and jwk formats + _ => { + return Err(type_error(format!( + "Unsupported key format: {}", + key_format + ))) + } + }; + Ok(match digest_type { + "sha224" => VerifyingKey::::new_with_prefix(key) + .verify_prehash(digest, &signature.to_vec().try_into()?) + .is_ok(), + "sha256" => VerifyingKey::::new_with_prefix(key) + .verify_prehash(digest, &signature.to_vec().try_into()?) + .is_ok(), + "sha384" => VerifyingKey::::new_with_prefix(key) + .verify_prehash(digest, &signature.to_vec().try_into()?) + .is_ok(), + "sha512" => VerifyingKey::::new_with_prefix(key) + .verify_prehash(digest, &signature.to_vec().try_into()?) + .is_ok(), + _ => { + return Err(type_error(format!( + "Unknown digest algorithm: {}", + digest_type + ))) + } + }) + } + _ => Err(type_error(format!( + "Verifying with {} keys is not supported yet", + key_type + ))), + } +} + fn pbkdf2_sync( password: &[u8], salt: &[u8], diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f4b4d0a39b..b09cb1c905 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -194,6 +194,7 @@ deno_core::extension!(deno_node, crypto::op_node_generate_secret, crypto::op_node_generate_secret_async, crypto::op_node_sign, + crypto::op_node_verify, crypto::op_node_random_int, crypto::x509::op_node_x509_parse, crypto::x509::op_node_x509_ca, diff --git a/ext/node/polyfills/internal/crypto/sig.ts b/ext/node/polyfills/internal/crypto/sig.ts index e49128b1ed..2996cb2cab 100644 --- a/ext/node/polyfills/internal/crypto/sig.ts +++ b/ext/node/polyfills/internal/crypto/sig.ts @@ -67,7 +67,7 @@ export class Sign extends Writable { } sign( - privateKey: KeyLike | SignKeyObjectInput | SignPrivateKeyInput, + privateKey: BinaryLike | SignKeyObjectInput | SignPrivateKeyInput, encoding?: BinaryToTextEncoding, ): Buffer | string { let keyData: Uint8Array; @@ -75,7 +75,8 @@ export class Sign extends Writable { let keyFormat: KeyFormat; if (typeof privateKey === "string" || isArrayBufferView(privateKey)) { // if the key is BinaryLike, interpret it as a PEM encoded RSA key - keyData = privateKey; + // deno-lint-ignore no-explicit-any + keyData = privateKey as any; keyType = "rsa"; keyFormat = "pem"; } else { @@ -103,35 +104,64 @@ export class Sign extends Writable { } export class Verify extends Writable { + hash: Hash; + #digestType: string; + constructor(algorithm: string, _options?: WritableOptions) { validateString(algorithm, "algorithm"); - super(); + super({ + write(chunk, enc, callback) { + this.update(chunk, enc); + callback(); + }, + }); - notImplemented("crypto.Verify"); + algorithm = algorithm.toLowerCase(); + + if (algorithm.startsWith("rsa-")) { + // Allows RSA-[digest_algorithm] as a valid algorithm + algorithm = algorithm.slice(4); + } + + this.#digestType = algorithm; + this.hash = createHash(this.#digestType); } - update(data: BinaryLike): this; - update(data: string, inputEncoding: Encoding): this; - update(_data: BinaryLike, _inputEncoding?: string): this { - notImplemented("crypto.Sign.prototype.update"); + update(data: BinaryLike, encoding?: string): this { + this.hash.update(data, encoding); + return this; } verify( - object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, - signature: ArrayBufferView, - ): boolean; - verify( - object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, - signature: string, - signatureEncoding?: BinaryToTextEncoding, - ): boolean; - verify( - _object: KeyLike | VerifyKeyObjectInput | VerifyPublicKeyInput, - _signature: ArrayBufferView | string, - _signatureEncoding?: BinaryToTextEncoding, + publicKey: BinaryLike | VerifyKeyObjectInput | VerifyPublicKeyInput, + signature: BinaryLike, + encoding?: BinaryToTextEncoding, ): boolean { - notImplemented("crypto.Sign.prototype.sign"); + let keyData: BinaryLike; + let keyType: KeyType; + let keyFormat: KeyFormat; + if (typeof publicKey === "string" || isArrayBufferView(publicKey)) { + // if the key is BinaryLike, interpret it as a PEM encoded RSA key + // deno-lint-ignore no-explicit-any + keyData = publicKey as any; + keyType = "rsa"; + keyFormat = "pem"; + } else { + // TODO(kt3k): Add support for the case when publicKey is a KeyObject, + // CryptoKey, etc + notImplemented( + "crypto.Verify.prototype.verify with non BinaryLike input", + ); + } + return ops.op_node_verify( + this.hash.digest(), + this.#digestType, + keyData!, + keyType, + keyFormat, + Buffer.from(signature, encoding), + ); } }