Repository: denoland/deno
Pull request: fix(ext/node): implement X509Certificate#checkHost

Fixes https://github.com/denoland/deno/issues/27619

Changed files:
filename: ext/node/lib.rs
patch: @@ -301,6 +301,7 @@ deno_core::extension!(deno_node,
     ops::crypto::x509::op_node_x509_parse,
     ops::crypto::x509::op_node_x509_ca,
     ops::crypto::x509::op_node_x509_check_email,
+    ops::crypto::x509::op_node_x509_check_host,
     ops::crypto::x509::op_node_x509_fingerprint,
     ops::crypto::x509::op_node_x509_fingerprint256,
     ops::crypto::x509::op_node_x509_fingerprint512,

filename: ext/node/ops/crypto/x509.rs
patch: @@ -130,6 +130,43 @@ pub fn op_node_x509_check_email(
   false
 }
 
+#[op2(fast)]
+pub fn op_node_x509_check_host(
+  #[cppgc] cert: &Certificate,
+  #[string] host: &str,
+) -> bool {
+  let cert = cert.inner.get().deref();
+
+  let subject = cert.subject();
+  if subject
+    .iter_common_name()
+    .any(|e| e.as_str().unwrap_or("") == host)
+  {
+    return true;
+  }
+
+  let subject_alt = cert
+    .extensions()
+    .iter()
+    .find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME)
+    .and_then(|e| match e.parsed_extension() {
+      extensions::ParsedExtension::SubjectAlternativeName(s) => Some(s),
+      _ => None,
+    });
+
+  if let Some(subject_alt) = subject_alt {
+    for name in &subject_alt.general_names {
+      if let extensions::GeneralName::DNSName(n) = name {
+        if *n == host {
+          return true;
+        }
+      }
+    }
+  }
+
+  false
+}
+
 #[op2]
 #[string]
 pub fn op_node_x509_fingerprint(#[cppgc] cert: &Certificate) -> Option<String> {

filename: ext/node/polyfills/internal/crypto/x509.ts
patch: @@ -7,6 +7,7 @@
 import {
   op_node_x509_ca,
   op_node_x509_check_email,
+  op_node_x509_check_host,
   op_node_x509_fingerprint,
   op_node_x509_fingerprint256,
   op_node_x509_fingerprint512,
@@ -90,8 +91,11 @@ export class X509Certificate {
     }
   }
 
-  checkHost(_name: string, _options?: X509CheckOptions): string | undefined {
-    notImplemented("crypto.X509Certificate.prototype.checkHost");
+  checkHost(name: string, _options?: X509CheckOptions): string | undefined {
+    validateString(name, "name");
+    if (op_node_x509_check_host(this.#handle, name)) {
+      return name;
+    }
   }
 
   checkIP(_ip: string): string | undefined {

filename: tests/unit_node/crypto/crypto_key_test.ts
patch: @@ -12,6 +12,7 @@ import {
   generateKeyPairSync,
   KeyObject,
   randomBytes,
+  X509Certificate,
 } from "node:crypto";
 import { promisify } from "node:util";
 import { Buffer } from "node:buffer";
@@ -716,3 +717,41 @@ Deno.test("RSA export private JWK", function () {
   assertEquals((privateKey as any).kty, "RSA");
   assertEquals((privateKey as any).n, (publicKey as any).n);
 });
+
+Deno.test("X509Certificate checkHost", function () {
+  const der = Buffer.from(
+    "308203e8308202d0a0030201020214147d36c1c2f74206de9fab5f2226d78adb00a42630" +
+      "0d06092a864886f70d01010b0500307a310b3009060355040613025553310b3009060355" +
+      "04080c024341310b300906035504070c025346310f300d060355040a0c064a6f79656e74" +
+      "3110300e060355040b0c074e6f64652e6a73310c300a06035504030c036361313120301e" +
+      "06092a864886f70d010901161172794074696e79636c6f7564732e6f72673020170d3232" +
+      "303930333231343033375a180f32323936303631373231343033375a307d310b30090603" +
+      "55040613025553310b300906035504080c024341310b300906035504070c025346310f30" +
+      "0d060355040a0c064a6f79656e743110300e060355040b0c074e6f64652e6a73310f300d" +
+      "06035504030c066167656e74313120301e06092a864886f70d010901161172794074696e" +
+      "79636c6f7564732e6f726730820122300d06092a864886f70d01010105000382010f0030" +
+      "82010a0282010100d456320afb20d3827093dc2c4284ed04dfbabd56e1ddae529e28b790" +
+      "cd4256db273349f3735ffd337c7a6363ecca5a27b7f73dc7089a96c6d886db0c62388f1c" +
+      "dd6a963afcd599d5800e587a11f908960f84ed50ba25a28303ecda6e684fbe7baedc9ce8" +
+      "801327b1697af25097cee3f175e400984c0db6a8eb87be03b4cf94774ba56fffc8c63c68" +
+      "d6adeb60abbe69a7b14ab6a6b9e7baa89b5adab8eb07897c07f6d4fa3d660dff574107d2" +
+      "8e8f63467a788624c574197693e959cea1362ffae1bba10c8c0d88840abfef103631b2e8" +
+      "f5c39b5548a7ea57e8a39f89291813f45a76c448033a2b7ed8403f4baa147cf35e2d2554" +
+      "aa65ce49695797095bf4dc6b0203010001a361305f305d06082b06010505070101045130" +
+      "4f302306082b060105050730018617687474703a2f2f6f6373702e6e6f64656a732e6f72" +
+      "672f302806082b06010505073002861c687474703a2f2f63612e6e6f64656a732e6f7267" +
+      "2f63612e63657274300d06092a864886f70d01010b05000382010100c3349810632ccb7d" +
+      "a585de3ed51e34ed154f0f7215608cf2701c00eda444dc2427072c8aca4da6472c1d9e68" +
+      "f177f99a90a8b5dbf3884586d61cb1c14ea7016c8d38b70d1b46b42947db30edc1e9961e" +
+      "d46c0f0e35da427bfbe52900771817e733b371adf19e12137235141a34347db0dfc05579" +
+      "8b1f269f3bdf5e30ce35d1339d56bb3c570de9096215433047f87ca42447b44e7e6b5d0e" +
+      "48f7894ab186f85b6b1a74561b520952fea888617f32f582afce1111581cd63efcc68986" +
+      "00d248bb684dedb9c3d6710c38de9e9bc21f9c3394b729d5f707d64ea890603e5989f8fa" +
+      "59c19ad1a00732e7adc851b89487cc00799dde068aa64b3b8fd976e8bc113ef2",
+    "hex",
+  );
+
+  const cert = new X509Certificate(der);
+  assertEquals(cert.checkHost("www.google.com"), undefined);
+  assertEquals(cert.checkHost("agent1"), "agent1");
+});