From c4f3e7d0aab53fc9b32d0bcd5477f75152359522 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 12 Aug 2024 09:13:24 +0530 Subject: [PATCH] fix(ext/node): implement spkac certificate API Fixes https://github.com/denoland/deno/issues/21808 --- ext/node/lib.rs | 3 + ext/node/ops/crypto/mod.rs | 1 + ext/node/ops/crypto/sign.rs | 3 +- ext/node/ops/crypto/spkac.rs | 109 ++++++++++++++++++ .../polyfills/internal/crypto/certificate.ts | 18 +-- test.mjs | 26 +++++ 6 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 ext/node/ops/crypto/spkac.rs create mode 100644 test.mjs diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 73b4497fc79b13..73486d0c9920f9 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -312,6 +312,9 @@ deno_core::extension!(deno_node, ops::crypto::x509::op_node_x509_get_serial_number, ops::crypto::x509::op_node_x509_key_usage, ops::crypto::x509::op_node_x509_public_key, + ops::crypto::spkac::op_node_export_challenge, + ops::crypto::spkac::op_node_verify_spkac, + ops::crypto::spkac::op_node_export_public_key, ops::fs::op_node_fs_exists_sync

, ops::fs::op_node_fs_exists

, ops::fs::op_node_cp_sync

, diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 7384375773c454..8aa42d5a039f5e 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -42,6 +42,7 @@ mod md5_sha1; mod pkcs3; mod primes; mod sign; +pub mod spkac; pub mod x509; use self::digest::match_fixed_digest_with_eager_block_buffer; diff --git a/ext/node/ops/crypto/sign.rs b/ext/node/ops/crypto/sign.rs index b7779a5d807e44..d1388a58352eac 100644 --- a/ext/node/ops/crypto/sign.rs +++ b/ext/node/ops/crypto/sign.rs @@ -210,7 +210,8 @@ impl KeyObjectHandle { ) }; - Ok(signer.verify(key, digest, signature).is_ok()) + signer.verify(key, digest, signature).unwrap(); + Ok(true) } AsymmetricPublicKey::RsaPss(key) => { let mut hash_algorithm = None; diff --git a/ext/node/ops/crypto/spkac.rs b/ext/node/ops/crypto/spkac.rs new file mode 100644 index 00000000000000..ea41d92c06f3bf --- /dev/null +++ b/ext/node/ops/crypto/spkac.rs @@ -0,0 +1,109 @@ +//! SPKAC (Signed Public Key and Challenge) is a format for public keys. It is used for CSR +//! and encodes the public key. It is usually obtained using HTML5 element. +//! +//! No browser supports anymore and SPKAC is now legacy so you should not use it. + +use spki::der::asn1; +use spki::der::Decode; +use spki::der::Encode; +use spki::der::EncodePem; +use spki::der::Sequence; +use spki::SubjectPublicKeyInfoRef; + +use super::digest::Hash; +use super::KeyObjectHandle; + +use deno_core::error::AnyError; +use deno_core::op2; + +/// SignedPublicKeyAndChallenge ::= SEQUENCE { +/// publicKeyAndChallenge PublicKeyAndChallenge, +/// signatureAlgorithm AlgorithmIdentifier, +/// signature BIT STRING +/// } +#[derive(Sequence)] +struct SignedPublicKeyAndChallenge<'a> { + public_key_and_challenge: PublicKeyAndChallenge<'a>, + signature_algorithm: AlgorithmIdentifier<'a>, + signature: asn1::BitStringRef<'a>, +} + +/// PublicKeyAndChallenge ::= SEQUENCE { +/// spki SubjectPublicKeyInfo, +/// challenge IA5STRING +/// } +#[derive(Sequence)] +struct PublicKeyAndChallenge<'a> { + spki: SubjectPublicKeyInfoRef<'a>, + challenge: asn1::Ia5StringRef<'a>, +} + +#[derive(Sequence)] +struct AlgorithmIdentifier<'a> { + algorithm: asn1::ObjectIdentifier, + parameters: Option>, +} + +struct Certificate; + +impl Certificate { + fn export_challenge(spkac: &[u8]) -> Result, AnyError> { + let spkac = base64::decode(spkac)?; + let spkac = SignedPublicKeyAndChallenge::from_der(&spkac)?; + + let challenge = spkac.public_key_and_challenge.challenge; + Ok(challenge.as_bytes().to_vec().into_boxed_slice()) + } + + fn verify_spkac(spkac: &[u8]) -> Result { + let spkac = base64::decode(spkac)?; + let spkac = SignedPublicKeyAndChallenge::from_der(&spkac)?; + + let spki = spkac.public_key_and_challenge.spki; + let spki_der = spki.to_der()?; + + let key = KeyObjectHandle::new_asymmetric_public_key_from_js( + &spki_der, "der", "spki", None, + )?; + let Some(signature) = spkac.signature.as_bytes() else { + return Ok(false); + }; + + let mut hasher = Hash::new("rsa-md5", None)?; + hasher.update(&spki_der); + let hash = hasher.digest_and_drop(); + + key.verify_prehashed("rsa-md5", &hash, signature, None, 0) + } + + fn export_public_key(spkac: &[u8]) -> Result, AnyError> { + let spkac = base64::decode(spkac)?; + let spkac = SignedPublicKeyAndChallenge::from_der(&spkac)?; + + let spki = spkac.public_key_and_challenge.spki; + + let pem = spki.to_pem(Default::default())?; + Ok(pem.as_bytes().to_vec().into_boxed_slice()) + } +} + +#[op2] +#[buffer] +pub fn op_node_export_challenge( + #[buffer] spkac: &[u8], +) -> Result, AnyError> { + Certificate::export_challenge(spkac) +} + +#[op2(fast)] +pub fn op_node_verify_spkac(#[buffer] spkac: &[u8]) -> Result { + Certificate::verify_spkac(spkac) +} + +#[op2] +#[buffer] +pub fn op_node_export_public_key( + #[buffer] spkac: &[u8], +) -> Result, AnyError> { + Certificate::export_public_key(spkac) +} diff --git a/ext/node/polyfills/internal/crypto/certificate.ts b/ext/node/polyfills/internal/crypto/certificate.ts index c15236c5cb6627..cda333de05597a 100644 --- a/ext/node/polyfills/internal/crypto/certificate.ts +++ b/ext/node/polyfills/internal/crypto/certificate.ts @@ -1,22 +1,26 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. -import { notImplemented } from "ext:deno_node/_utils.ts"; +import { + op_node_export_challenge, + op_node_export_public_key, + op_node_verify_spkac, +} from "ext:core/ops"; import { Buffer } from "node:buffer"; import { BinaryLike } from "ext:deno_node/internal/crypto/types.ts"; export class Certificate { static Certificate = Certificate; - static exportChallenge(_spkac: BinaryLike, _encoding?: string): Buffer { - notImplemented("crypto.Certificate.exportChallenge"); + static exportChallenge(spkac: BinaryLike, encoding?: string): Buffer { + return Buffer.from(op_node_export_challenge(Buffer.from(spkac, encoding))); } - static exportPublicKey(_spkac: BinaryLike, _encoding?: string): Buffer { - notImplemented("crypto.Certificate.exportPublicKey"); + static exportPublicKey(spkac: BinaryLike, encoding?: string): Buffer { + return Buffer.from(op_node_export_public_key(Buffer.from(spkac, encoding))); } - static verifySpkac(_spkac: BinaryLike, _encoding?: string): boolean { - notImplemented("crypto.Certificate.verifySpkac"); + static verifySpkac(spkac: BinaryLike, encoding?: string): boolean { + return op_node_verify_spkac(Buffer.from(spkac, encoding)); } } diff --git a/test.mjs b/test.mjs new file mode 100644 index 00000000000000..4cd21c1d60ff20 --- /dev/null +++ b/test.mjs @@ -0,0 +1,26 @@ +import { Certificate } from "node:crypto"; + +const rsaSpkac = + `MIICUzCCATswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC33FiIiiexwLe/P8DZx5HsqFlmUO7/lvJ7necJVNwqdZ3ax5jpQB0p6uxfqeOvzcN3k5V7UFb/Am+nkSNZMAZhsWzCU2Z4Pjh50QYz3f0Hour7/yIGStOLyYY3hgLK2K8TbhgjQPhdkw9+QtKlpvbL8fLgONAoGrVOFnRQGcr70iFffsm79mgZhKVMgYiHPJqJgGHvCtkGg9zMgS7p63+Q3ZWedtFS2RhMX3uCBy/mH6EOlRCNBbRmA4xxNzyf5GQaki3T+Iz9tOMjdPP+CwV2LqEdylmBuik8vrfTb3qIHLKKBAI8lXN26wWtA3kN4L7NP+cbKlCRlqctvhmylLH1AgMBAAEWE3RoaXMtaXMtYS1jaGFsbGVuZ2UwDQYJKoZIhvcNAQEEBQADggEBAIozmeW1kfDfAVwRQKileZGLRGCD7AjdHLYEe16xTBPve8Af1bDOyuWsAm4qQLYA4FAFROiKeGqxCtIErEvm87/09tCfF1My/1Uj+INjAk39DK9J9alLlTsrwSgd1lb3YlXY7TyitCmh7iXLo4pVhA2chNA3njiMq3CUpSvGbpzrESL2dv97lv590gUD988wkTDVyYsf0T8+X0Kww3AgPWGji+2f2i5/jTfD/s1lK1nqi7ZxFm0pGZoy1MJ51SCEy7Y82ajroI+5786nC02mo9ak7samca4YDZOoxN4d3tax4B/HDF5dqJSm1/31xYLDTfujCM5FkSjRc4m6hnriEkc=`; + +const challenge = Certificate.exportChallenge(rsaSpkac); +console.log(challenge.toString()); + +const publicKey = Certificate.exportPublicKey(rsaSpkac); +const expected = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt9xYiIonscC3vz/A2ceR\n" + + "7KhZZlDu/5bye53nCVTcKnWd2seY6UAdKersX6njr83Dd5OVe1BW/wJvp5EjWTAG\n" + + "YbFswlNmeD44edEGM939B6Lq+/8iBkrTi8mGN4YCytivE24YI0D4XZMPfkLSpab2\n" + + "y/Hy4DjQKBq1ThZ0UBnK+9IhX37Ju/ZoGYSlTIGIhzyaiYBh7wrZBoPczIEu6et/\n" + + "kN2VnnbRUtkYTF97ggcv5h+hDpUQjQW0ZgOMcTc8n+RkGpIt0/iM/bTjI3Tz/gsF\n" + + "di6hHcpZgbopPL630296iByyigQCPJVzdusFrQN5DeC+zT/nGypQkZanLb4ZspSx\n" + + "9QIDAQAB\n" + + "-----END PUBLIC KEY-----\n"; +if (publicKey.toString() != expected) { + console.log("Expected: \n" + expected); + console.log("Actual: \n" + publicKey.toString()); + throw new Error("Public key mismatch"); +} + +const verify = Certificate.verifySpkac(rsaSpkac); +console.log(verify);