Skip to content
Open
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
4 changes: 4 additions & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,16 @@ deno_core::extension!(deno_node,
ops::crypto::x509::op_node_x509_fingerprint256,
ops::crypto::x509::op_node_x509_fingerprint512,
ops::crypto::x509::op_node_x509_get_issuer,
ops::crypto::x509::op_node_x509_info_access,
ops::crypto::x509::op_node_x509_get_subject,
ops::crypto::x509::op_node_x509_get_valid_from,
ops::crypto::x509::op_node_x509_get_valid_to,
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::x509::op_node_x509_raw,
ops::crypto::x509::op_node_x509_to_string,
ops::crypto::x509::op_node_x509_verify,
ops::fs::op_node_fs_exists_sync<P>,
ops::fs::op_node_fs_exists<P>,
ops::fs::op_node_cp_sync<P>,
Expand Down
20 changes: 20 additions & 0 deletions ext/node/ops/crypto/testdata/x509-test-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgIUCNhWDISbT5DJKeTWc5nlP5O37s8wDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJeDUwOS10ZXN0MB4XDTI2MTAwODAyNDcxM1oXDTM2MTAw
NTAyNDcxM1owFDESMBAGA1UEAwwJeDUwOS10ZXN0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAjeKeEYUUEPxpkHW2Y+dioOCosSwdYkxbnC+mMSFVFuSy
IpsnBY4oLpm63Xa9BG1hAYz8M5quozUatSBzWW298i3+a8O6CXMli2G9UgMOr0jr
eb2mYvR2MQs4FSuaLPt9A3QG9+mbxCWXUxdQ14XqPz/kqaoUiCQgD0zklxsZOT74
Mdbvsx46cqdU7II6yOd2dmhIXK+BR6o3ntNIYaH4QEr2b7/bhAudozRo9fCygeFK
T3m2L9D5fmE8vEV2pda5Whnx2VcGfx5k3O2BBmWX6YMicN8YvhqnSX4MWEfAQN/w
a7vBSXnbV5wSvt9gLcO/3WMWCPX7oJCmqyUi+GlFhQIDAQABo4GKMIGHMB0GA1Ud
DgQWBBSJssJQcXpDcfr3s6m9QeKccMtmtjAfBgNVHSMEGDAWgBSJssJQcXpDcfr3
s6m9QeKccMtmtjAPBgNVHRMBAf8EBTADAQH/MDQGCCsGAQUFBwEBBCgwJjAkBggr
BgEFBQcwAYYYaHR0cDovL29jc3AuZXhhbXBsZS50ZXN0MA0GCSqGSIb3DQEBCwUA
A4IBAQBXm1Mny5ceQdKfScLNfikw5wIfnerqGsAWOks4mo5Kpdb+AKNDo+piwo4s
h9HQKTzssWH6PGWkcz7gGU0x6g67NZcKNTBytLX9K2kDNTUsyhbrHcQB4tFv7OUC
uMYI31+A0TRBngveCEUtfNCi76B7P0JL7+Cdqsqrsx/wb9NVBCt/L8kHGcvnq+yI
syjsdzOEOBs7fp4+IS/1kDPa4zX+E67e2lwOed1nKLC67K8x5pKJVBsDpBJlHASt
dlgnNxbw7RPpzbt+9xFndj4Maco/imc0DbNL633eCPnoiVTwIcet/C2iP6Zg6kmo
QEoIm21twcBieiwKlWj9d4WNu/Aa
-----END CERTIFICATE-----
144 changes: 144 additions & 0 deletions ext/node/ops/crypto/x509.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use x509_parser::extensions;
use x509_parser::pem;
use x509_parser::prelude::*;

use super::sign::KeyObjectHandlePrehashedSignAndVerifyError;
use super::KeyObjectHandle;

use std::ops::Deref;
Expand All @@ -35,6 +36,13 @@ pub(crate) struct Certificate {
impl deno_core::GarbageCollected for Certificate {}

impl Certificate {
fn der_bytes(&self) -> &[u8] {
match self.inner.backing_cart().as_ref() {
CertificateSources::Der(der) => der,
CertificateSources::Pem(pem) => &pem.contents,
}
}

fn fingerprint<D: Digest>(&self) -> Option<String> {
if let CertificateSources::Pem(pem) = self.inner.backing_cart().as_ref() {
let mut hasher = D::new();
Expand Down Expand Up @@ -66,6 +74,10 @@ impl<'a> Deref for CertificateView<'a> {
pub fn op_node_x509_parse(
#[buffer] buf: &[u8],
) -> Result<Certificate, X509Error> {
parse_certificate(buf)
}

fn parse_certificate(buf: &[u8]) -> Result<Certificate, X509Error> {
let source = match pem::parse_x509_pem(buf) {
Ok((_, pem)) => CertificateSources::Pem(pem),
Err(_) => CertificateSources::Der(buf.to_vec().into_boxed_slice()),
Expand Down Expand Up @@ -181,6 +193,107 @@ pub fn op_node_x509_public_key(
KeyObjectHandle::new_x509_public_key(public_key)
}

#[op2]
#[buffer]
pub fn op_node_x509_raw(#[cppgc] cert: &Certificate) -> Box<[u8]> {
cert.der_bytes().to_vec().into_boxed_slice()
}

#[op2]
#[string]
pub fn op_node_x509_to_string(#[cppgc] cert: &Certificate) -> String {
certificate_to_pem(cert)
}

fn certificate_to_pem(cert: &Certificate) -> String {
let encoded = data_encoding::BASE64.encode(cert.der_bytes());
let mut pem = String::from("-----BEGIN CERTIFICATE-----\n");
for line in encoded.as_bytes().chunks(64) {
pem.push_str(std::str::from_utf8(line).expect("base64 is ASCII"));
pem.push('\n');
}
pem.push_str("-----END CERTIFICATE-----\n");
pem
}

#[op2]
#[string]
pub fn op_node_x509_info_access(#[cppgc] cert: &Certificate) -> Option<String> {
certificate_info_access(cert)
}

fn certificate_info_access(cert: &Certificate) -> Option<String> {
let cert = cert.inner.get().deref();
let aia = cert.extensions().iter().find_map(|ext| {
if let extensions::ParsedExtension::AuthorityInfoAccess(aia) =
ext.parsed_extension()
{
Some(aia)
} else {
None
}
})?;

let descriptions: Vec<String> = aia
.iter()
.map(|entry| {
let method = match entry.access_method.to_id_string().as_str() {
"1.3.6.1.5.5.7.48.1" => "OCSP".to_string(),
"1.3.6.1.5.5.7.48.2" => "CA Issuers".to_string(),
_ => entry.access_method.to_id_string(),
};
match &entry.access_location {
extensions::GeneralName::URI(uri) => format!("{method} - URI:{uri}"),
location => format!("{method} - {location}"),
}
})
.collect();
Some(descriptions.join("\n"))
}

#[derive(Debug, thiserror::Error)]
pub enum X509VerifyError {
#[error("unsupported X.509 signature algorithm: {0}")]
UnsupportedSignatureAlgorithm(String),
#[error(transparent)]
Key(#[from] KeyObjectHandlePrehashedSignAndVerifyError),
}

#[op2(fast)]
pub fn op_node_x509_verify(
#[cppgc] cert: &Certificate,
#[cppgc] public_key: &KeyObjectHandle,
) -> Result<bool, X509VerifyError> {
verify_certificate(cert, public_key)
}

fn verify_certificate(
cert: &Certificate,
public_key: &KeyObjectHandle,
) -> Result<bool, X509VerifyError> {
let cert = cert.inner.get().deref();
if cert.signature_algorithm != cert.tbs_certificate.signature {
return Ok(false);
}
let tbs = cert.tbs_certificate.as_ref();
let signature = cert.signature_value.data.as_ref();
let oid = cert.signature_algorithm.algorithm.to_id_string();
let (digest_name, digest): (&str, Vec<u8>) = match oid.as_str() {
"1.2.840.113549.1.1.5" => ("sha1", sha1::Sha1::digest(tbs).to_vec()),
"1.2.840.113549.1.1.11" | "1.2.840.10045.4.3.2" => {
("sha256", sha2::Sha256::digest(tbs).to_vec())
}
"1.2.840.113549.1.1.12" | "1.2.840.10045.4.3.3" => {
("sha384", sha2::Sha384::digest(tbs).to_vec())
}
"1.2.840.113549.1.1.13" | "1.2.840.10045.4.3.4" => {
("sha512", sha2::Sha512::digest(tbs).to_vec())
}
_ => return Err(X509VerifyError::UnsupportedSignatureAlgorithm(oid)),
};
Ok(public_key.verify_prehashed(digest_name, &digest, signature, None, 0)?)
}

// Attempt to convert attribute to string. If type is not a string, return value is the hex
// encoding of the attribute value
fn attribute_value_to_string(
Expand Down Expand Up @@ -278,3 +391,34 @@ pub fn op_node_x509_key_usage(#[cppgc] cert: &Certificate) -> u16 {

key_usage.map(|k| k.flags).unwrap_or(0)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn x509_certificate_encoding_access_and_signature() {
let pem = include_bytes!("testdata/x509-test-cert.pem");
let cert = parse_certificate(pem).unwrap();
let der = cert.der_bytes();

assert_eq!(certificate_to_pem(&cert).as_bytes(), pem);
assert_eq!(
certificate_info_access(&cert).as_deref(),
Some("OCSP - URI:http://ocsp.example.test")
);
assert_eq!(parse_certificate(der).unwrap().der_bytes(), der);

let public_key = KeyObjectHandle::new_x509_public_key(
&cert.inner.get().deref().tbs_certificate.subject_pki,
)
.unwrap();
assert!(verify_certificate(&cert, &public_key).unwrap());

let mut corrupted = der.to_vec();
let last = corrupted.last_mut().unwrap();
*last ^= 1;
let tampered = parse_certificate(&corrupted).unwrap();
assert!(!verify_certificate(&tampered, &public_key).unwrap());
}
}
26 changes: 17 additions & 9 deletions ext/node/polyfills/internal/crypto/x509.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ import {
op_node_x509_get_subject,
op_node_x509_get_valid_from,
op_node_x509_get_valid_to,
op_node_x509_info_access,
op_node_x509_key_usage,
op_node_x509_parse,
op_node_x509_public_key,
op_node_x509_raw,
op_node_x509_to_string,
op_node_x509_verify,
} from "ext:core/ops";

import {
getKeyObjectHandle,
kConsumePublic,
KeyObject,
PublicKeyObject,
} from "ext:deno_node/internal/crypto/keys.ts";
Expand Down Expand Up @@ -119,9 +125,7 @@ export class X509Certificate {
}

get infoAccess(): string | undefined {
notImplemented("crypto.X509Certificate.prototype.infoAccess");

return "";
return op_node_x509_info_access(this.#handle);
}

get issuer(): string {
Expand Down Expand Up @@ -154,9 +158,7 @@ export class X509Certificate {
}

get raw(): Buffer {
notImplemented("crypto.X509Certificate.prototype.raw");

return {} as Buffer;
return Buffer.from(op_node_x509_raw(this.#handle));
}

get serialNumber(): string {
Expand All @@ -180,7 +182,7 @@ export class X509Certificate {
}

toString(): string {
notImplemented("crypto.X509Certificate.prototype.toString");
return op_node_x509_to_string(this.#handle);
}

get validFrom(): string {
Expand All @@ -191,8 +193,14 @@ export class X509Certificate {
return op_node_x509_get_valid_to(this.#handle);
}

verify(_publicKey: KeyObject): boolean {
notImplemented("crypto.X509Certificate.prototype.verify");
verify(publicKey: KeyObject): boolean {
if (!(publicKey instanceof KeyObject)) {
throw new ERR_INVALID_ARG_TYPE("publicKey", "KeyObject", publicKey);
}
return op_node_x509_verify(
this.#handle,
getKeyObjectHandle(publicKey, kConsumePublic),
);
}
}

Expand Down