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

fix(ext/node): implement crypto.Sign (RSA/PEM/SHA{224,256,384,512}) #18471

Merged
merged 5 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ serde_bytes = "0.11"
serde_json = "1.0.85"
serde_repr = "=0.1.9"
sha2 = { version = "0.10.6", features = ["oid"] }
signature = "=1.6.4"
smallvec = "1.8"
socket2 = "0.4.7"
tar = "=0.4.38"
Expand All @@ -136,7 +137,7 @@ uuid = { version = "1.3.0", features = ["v4"] }
zstd = "=0.11.2"

# crypto
rsa = { version = "0.7.0", default-features = false, features = ["std", "pem"] }
rsa = { version = "0.7.0", default-features = false, features = ["std", "pem", "hazmat"] }
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a comment here why hazmat feature is used? "hazmat needed for PrehashSigner in ext/node"


# macros
proc-macro2 = "1"
Expand Down
47 changes: 47 additions & 0 deletions cli/tests/unit_node/crypto_sign_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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 { Buffer } from "node:buffer";

const rsaPrivatePem = Buffer.from(
await Deno.readFile(
new URL("./testdata/rsa_private.pem", import.meta.url),
),
);

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")
.sign(rsaPrivatePem, "hex");
assertEquals(signature, testCase.expected);
}
}
},
});
2 changes: 1 addition & 1 deletion ext/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ serde.workspace = true
serde_bytes.workspace = true
sha1 = { version = "0.10.5", features = ["oid"] }
sha2.workspace = true
signature = "1.6.4"
signature.workspace = true
spki = "0.6.0"
tokio.workspace = true
uuid.workspace = true
Expand Down
1 change: 1 addition & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ serde = "1.0.149"
sha-1 = "0.10.0"
sha2 = "0.10.6"
sha3 = "0.10.5"
signature.workspace = true
typenum = "1.15.0"
58 changes: 58 additions & 0 deletions ext/node/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,61 @@ pub fn op_node_decipheriv_final(
.map_err(|_| type_error("Cipher context is already in use"))?;
context.r#final(input, output)
}

#[op]
pub fn op_node_sign(
digest: &[u8],
digest_type: &str,
key: StringOrBuffer,
key_type: &str,
key_format: &str,
) -> Result<ZeroCopyBuf, AnyError> {
match key_type {
"rsa" => {
use rsa::pkcs1v15::SigningKey;
use signature::hazmat::PrehashSigner;
let key = match key_format {
"pem" => RsaPrivateKey::from_pkcs8_pem((&key).try_into()?)
.map_err(|_| type_error("Invalid RSA key"))?,
// TODO(kt3k): Support der and jwk formats
_ => {
return Err(type_error(format!(
"Unsupported key format: {}",
key_format
)))
}
};
Ok(
match digest_type {
"sha224" => {
let signing_key = SigningKey::<sha2::Sha224>::new_with_prefix(key);
signing_key.sign_prehash(digest)?.to_vec()
}
"sha256" => {
let signing_key = SigningKey::<sha2::Sha256>::new_with_prefix(key);
signing_key.sign_prehash(digest)?.to_vec()
}
"sha384" => {
let signing_key = SigningKey::<sha2::Sha384>::new_with_prefix(key);
signing_key.sign_prehash(digest)?.to_vec()
}
"sha512" => {
let signing_key = SigningKey::<sha2::Sha512>::new_with_prefix(key);
signing_key.sign_prehash(digest)?.to_vec()
}
_ => {
return Err(type_error(format!(
"Unknown digest algorithm: {}",
digest_type
)))
}
}
.into(),
)
}
_ => Err(type_error(format!(
"Signing with {} keys is not supported yet",
key_type
))),
}
}
3 changes: 2 additions & 1 deletion ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ deno_core::extension!(deno_node,
crypto::op_node_private_encrypt,
crypto::op_node_private_decrypt,
crypto::op_node_public_encrypt,
crypto::op_node_sign,
winerror::op_node_sys_to_uv_error,
v8::op_v8_cached_data_version_tag,
v8::op_v8_get_heap_statistics,
Expand Down Expand Up @@ -400,7 +401,7 @@ pub fn initialize_runtime(
let source_code = format!(
r#"(function loadBuiltinNodeModules(nodeGlobalThisName, usesLocalNodeModulesDir, argv0) {{
Deno[Deno.internal].node.initialize(
nodeGlobalThisName,
nodeGlobalThisName,
usesLocalNodeModulesDir,
argv0
);
Expand Down
2 changes: 1 addition & 1 deletion ext/node/polyfills/internal/crypto/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class Hash extends Transform {

if (typeof algorithm === "string") {
this.#context = ops.op_node_create_hash(
algorithm,
algorithm.toLowerCase(),
);
if (this.#context === 0) {
throw new TypeError(`Unknown hash algorithm: ${algorithm}`);
Expand Down
66 changes: 52 additions & 14 deletions ext/node/polyfills/internal/crypto/sig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import type {
PublicKeyInput,
} from "ext:deno_node/internal/crypto/types.ts";
import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts";
import { createHash, Hash } from "ext:deno_node/internal/crypto/hash.ts";
import { KeyFormat, KeyType } from "ext:deno_node/internal/crypto/types.ts";
import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts";

const { core } = globalThis.__bootstrap;
const { ops } = core;

export type DSAEncoding = "der" | "ieee-p1363";

Expand All @@ -37,30 +43,62 @@ export interface VerifyKeyObjectInput extends SigningOptions {
export type KeyLike = string | Buffer | KeyObject;

export class Sign extends Writable {
hash: Hash;
#digestType: string;

constructor(algorithm: string, _options?: WritableOptions) {
validateString(algorithm, "algorithm");

super();

notImplemented("crypto.Sign");
super({
write(chunk, enc, callback) {
this.update(chunk, enc);
callback();
},
});

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);
}

sign(privateKey: KeyLike | SignKeyObjectInput | SignPrivateKeyInput): Buffer;
sign(
privateKey: KeyLike | SignKeyObjectInput | SignPrivateKeyInput,
outputFormat: BinaryToTextEncoding,
): string;
sign(
_privateKey: KeyLike | SignKeyObjectInput | SignPrivateKeyInput,
_outputEncoding?: BinaryToTextEncoding,
encoding?: BinaryToTextEncoding,
): Buffer | string {
notImplemented("crypto.Sign.prototype.sign");
let keyData: Uint8Array;
let keyType: KeyType;
let keyFormat: KeyFormat;
if (typeof privateKey === "string" || isArrayBufferView(privateKey)) {
// if the key is BinaryLike, interpret it as a PEM encoded RSA key
keyData = privateKey;
keyType = "rsa";
keyFormat = "pem";
} else {
// TODO(kt3k): Add support for the case when privateKey is a KeyObject,
// CryptoKey, etc
notImplemented("crypto.Sign.prototype.sign with non BinaryLike input");
}
const ret = Buffer.from(ops.op_node_sign(
this.hash.digest(),
this.#digestType,
keyData!,
keyType,
keyFormat,
));
return encoding ? ret.toString(encoding) : ret;
}

update(data: BinaryLike): this;
update(data: string, inputEncoding: Encoding): this;
update(_data: BinaryLike | string, _inputEncoding?: Encoding): this {
notImplemented("crypto.Sign.prototype.update");
update(
data: BinaryLike | string,
encoding?: Encoding,
): this {
this.hash.update(data, encoding);
return this;
}
}

Expand Down