Repository: denoland/deno
Pull request: fix(ext/node): inspect X509Certificate class

Fixes #30874

Changed files:
filename: ext/node/polyfills/internal/crypto/x509.ts
patch: @@ -31,6 +31,9 @@ import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts";
 import { validateString } from "ext:deno_node/internal/validators.mjs";
 import { notImplemented } from "ext:deno_node/_utils.ts";
 import { BinaryLike } from "ext:deno_node/internal/crypto/types.ts";
+import { inspect } from "node:util";
+import { customInspectSymbol as kInspect } from "ext:deno_node/internal/util.mjs";
+import type { InspectOptions } from "node:util";
 
 // deno-lint-ignore no-explicit-any
 export type PeerCertificate = any;
@@ -77,6 +80,36 @@ export class X509Certificate {
     this.#handle = op_node_x509_parse(buffer);
   }
 
+  [kInspect](depth: number, options: InspectOptions) {
+    if (depth < 0) {
+      return this;
+    }
+
+    const opts = {
+      ...options,
+      depth: options.depth == null ? null : options.depth - 1,
+    };
+
+    return `X509Certificate ${
+      inspect({
+        subject: this.subject,
+        subjectAltName: this.subjectAltName,
+        issuer: this.issuer,
+        // TODO(Tango992): replace with the actual value once implemented
+        infoAccess: undefined,
+        validFrom: this.validFrom,
+        validTo: this.validTo,
+        validFromDate: this.validFromDate,
+        validToDate: this.validToDate,
+        fingerprint: this.fingerprint,
+        fingerprint256: this.fingerprint256,
+        fingerprint512: this.fingerprint512,
+        keyUsage: this.keyUsage,
+        serialNumber: this.serialNumber,
+      }, opts)
+    }`;
+  }
+
   get ca(): boolean {
     return op_node_x509_ca(this.#handle);
   }
@@ -168,7 +201,7 @@ export class X509Certificate {
   }
 
   get subject(): string {
-    return op_node_x509_get_subject(this.#handle);
+    return op_node_x509_get_subject(this.#handle) || undefined;
   }
 
   get subjectAltName(): string | undefined {
@@ -191,10 +224,18 @@ export class X509Certificate {
     return op_node_x509_get_valid_from(this.#handle);
   }
 
+  get validFromDate(): Date {
+    return new Date(this.validFrom);
+  }
+
   get validTo(): string {
     return op_node_x509_get_valid_to(this.#handle);
   }
 
+  get validToDate(): Date {
+    return new Date(this.validTo);
+  }
+
   verify(_publicKey: KeyObject): boolean {
     notImplemented("crypto.X509Certificate.prototype.verify");
   }

filename: tests/unit_node/crypto/crypto_key_test.ts
patch: @@ -38,6 +38,14 @@ const generateKeyPairAsync = promisify(
     ),
 );
 
+const testDir = new URL(".", import.meta.url);
+
+const pemBuffer = Buffer.from(
+  await Deno.readTextFile(
+    new URL("../testdata/x509.pem", import.meta.url),
+  ),
+);
+
 Deno.test({
   name: "create secret key",
   fn() {
@@ -760,6 +768,51 @@ Deno.test("X509Certificate checkHost", function () {
   assertEquals(cert.checkHost("agent1"), "agent1");
 });
 
+Deno.test("X509Certificate inspect", async function () {
+  const scriptPath = "../testdata/inspect-x509.ts";
+
+  const command = new Deno.Command(Deno.execPath(), {
+    args: ["run", "--allow-read", scriptPath],
+    stdin: "null",
+    stdout: "piped",
+    stderr: "null",
+    cwd: testDir,
+  });
+  const { stdout } = await command.output();
+  const trimmedStdout = new TextDecoder().decode(stdout).trim();
+
+  assertEquals(
+    trimmedStdout,
+    `X509Certificate {
+  subject: 'C=US\\nST=CA\\nL=SF\\nO=Joyent\\nOU=Node.js\\nCN=agent1\\nEmail=ry@tinyclouds.org',
+  subjectAltName: undefined,
+  issuer: 'C=US\\nST=CA\\nL=SF\\nO=Joyent\\nOU=Node.js\\nCN=ca1\\nEmail=ry@tinyclouds.org',
+  infoAccess: undefined,
+  validFrom: 'Sep  3 21:40:37 2022 +00:00',
+  validTo: 'Jun 17 21:40:37 2296 +00:00',
+  validFromDate: 2022-09-03T21:40:37.000Z,
+  validToDate: 2296-06-17T21:40:37.000Z,
+  fingerprint: '8B:89:16:C4:99:87:D2:13:1A:64:94:36:38:A5:32:01:F0:95:3B:53',
+  fingerprint256: '2C:62:59:16:91:89:AB:90:6A:3E:98:88:A6:D3:C5:58:58:6C:AE:FF:9C:33:22:7C:B6:77:D3:34:E7:53:4B:05',
+  fingerprint512: '0B:6F:D0:4D:6B:22:53:99:66:62:51:2D:2C:96:F2:58:3F:95:1C:CC:4C:44:9D:B5:59:AA:AD:A8:F6:2A:24:8A:BB:06:A5:26:42:52:30:A3:37:61:30:A9:5A:42:63:E0:21:2F:D6:70:63:07:96:6F:27:A7:78:12:08:02:7A:8B',
+  keyUsage: undefined,
+  serialNumber: '147D36C1C2F74206DE9FAB5F2226D78ADB00A426'
+}`,
+  );
+});
+
+Deno.test("X509Certificate validFromDate validToDate", function () {
+  const x509 = new X509Certificate(pemBuffer);
+  assertEquals(
+    x509.validFromDate,
+    new Date("2022-09-03T21:40:37.000Z"),
+  );
+  assertEquals(
+    x509.validToDate,
+    new Date("2296-06-17T21:40:37.000Z"),
+  );
+});
+
 // https://github.com/denoland/deno/issues/27972
 Deno.test("curve25519 generate valid private jwk", function () {
   const { publicKey, privateKey } = generateKeyPairSync("ed25519", {

filename: tests/unit_node/testdata/inspect-x509.ts
patch: @@ -0,0 +1,10 @@
+import console from "node:console";
+import crypto from "node:crypto";
+import fs from "node:fs";
+
+const pemBuffer = fs.readFileSync(
+  new URL("../testdata/x509.pem", import.meta.url),
+);
+
+const x509 = new crypto.X509Certificate(pemBuffer);
+console.log(x509);

filename: tests/unit_node/testdata/x509.cnf
patch: @@ -0,0 +1,26 @@
+[ req ]
+default_bits           = 2048
+days                   = 9999
+distinguished_name     = req_distinguished_name
+attributes             = req_attributes
+prompt                 = no
+x509_extensions        = v3_ca
+
+[ req_distinguished_name ]
+C                      = US
+ST                     = CA
+L                      = SF
+O                      = Joyent
+OU                     = Node.js
+CN                     = agent1
+emailAddress           = ry@tinyclouds.org
+
+[ req_attributes ]
+challengePassword              = A challenge password
+
+[ v3_ca ]
+authorityInfoAccess = @issuer_info
+
+[ issuer_info ]
+OCSP;URI.0 = http://ocsp.nodejs.org/
+caIssuers;URI.0 = http://ca.nodejs.org/ca.cert

filename: tests/unit_node/testdata/x509.pem
patch: @@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID6DCCAtCgAwIBAgIUFH02wcL3Qgben6tfIibXitsApCYwDQYJKoZIhvcNAQEL
+BQAwejELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G
+A1UECgwGSm95ZW50MRAwDgYDVQQLDAdOb2RlLmpzMQwwCgYDVQQDDANjYTExIDAe
+BgkqhkiG9w0BCQEWEXJ5QHRpbnljbG91ZHMub3JnMCAXDTIyMDkwMzIxNDAzN1oY
+DzIyOTYwNjE3MjE0MDM3WjB9MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExCzAJ
+BgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05vZGUuanMxDzAN
+BgNVBAMMBmFnZW50MTEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDUVjIK+yDTgnCT3CxChO0E
+37q9VuHdrlKeKLeQzUJW2yczSfNzX/0zfHpjY+zKWie39z3HCJqWxtiG2wxiOI8c
+3WqWOvzVmdWADlh6EfkIlg+E7VC6JaKDA+zabmhPvnuu3JzogBMnsWl68lCXzuPx
+deQAmEwNtqjrh74DtM+Ud0ulb//Ixjxo1q3rYKu+aaexSramuee6qJta2rjrB4l8
+B/bU+j1mDf9XQQfSjo9jRnp4hiTFdBl2k+lZzqE2L/rhu6EMjA2IhAq/7xA2MbLo
+9cObVUin6lfoo5+JKRgT9Fp2xEgDOit+2EA/S6oUfPNeLSVUqmXOSWlXlwlb9Nxr
+AgMBAAGjYTBfMF0GCCsGAQUFBwEBBFEwTzAjBggrBgEFBQcwAYYXaHR0cDovL29j
+c3Aubm9kZWpzLm9yZy8wKAYIKwYBBQUHMAKGHGh0dHA6Ly9jYS5ub2RlanMub3Jn
+L2NhLmNlcnQwDQYJKoZIhvcNAQELBQADggEBAMM0mBBjLMt9pYXePtUeNO0VTw9y
+FWCM8nAcAO2kRNwkJwcsispNpkcsHZ5o8Xf5mpCotdvziEWG1hyxwU6nAWyNOLcN
+G0a0KUfbMO3B6ZYe1GwPDjXaQnv75SkAdxgX5zOzca3xnhITcjUUGjQ0fbDfwFV5
+ix8mnzvfXjDONdEznVa7PFcN6QliFUMwR/h8pCRHtE5+a10OSPeJSrGG+FtrGnRW
+G1IJUv6oiGF/MvWCr84REVgc1j78xomGANJIu2hN7bnD1nEMON6em8IfnDOUtynV
+9wfWTqiQYD5Zifj6WcGa0aAHMuetyFG4lIfMAHmd3gaKpks7j9l26LwRPvI=
+-----END CERTIFICATE-----