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 spkac certificate API #24999

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P>,
ops::fs::op_node_fs_exists<P>,
ops::fs::op_node_cp_sync<P>,
Expand Down
1 change: 1 addition & 0 deletions ext/node/ops/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion ext/node/ops/crypto/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
109 changes: 109 additions & 0 deletions ext/node/ops/crypto/spkac.rs
Original file line number Diff line number Diff line change
@@ -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 <keygen> element.
//!
//! No browser supports <keygen> 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<asn1::AnyRef<'a>>,
}

struct Certificate;

impl Certificate {
fn export_challenge(spkac: &[u8]) -> Result<Box<[u8]>, 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<bool, AnyError> {
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)
Comment on lines +72 to +76
Copy link
Member Author

Choose a reason for hiding this comment

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

todo: sig_alg oid -> hash conversion

}

fn export_public_key(spkac: &[u8]) -> Result<Box<[u8]>, 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<Box<[u8]>, AnyError> {
Certificate::export_challenge(spkac)
}

#[op2(fast)]
pub fn op_node_verify_spkac(#[buffer] spkac: &[u8]) -> Result<bool, AnyError> {
Certificate::verify_spkac(spkac)
}

#[op2]
#[buffer]
pub fn op_node_export_public_key(
#[buffer] spkac: &[u8],
) -> Result<Box<[u8]>, AnyError> {
Certificate::export_public_key(spkac)
}
18 changes: 11 additions & 7 deletions ext/node/polyfills/internal/crypto/certificate.ts
Original file line number Diff line number Diff line change
@@ -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));
}
}

Expand Down
26 changes: 26 additions & 0 deletions test.mjs
Original file line number Diff line number Diff line change
@@ -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);