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

mTLS Implementation Issues #16254

Open
keverw opened this issue Jan 8, 2025 · 0 comments
Open

mTLS Implementation Issues #16254

keverw opened this issue Jan 8, 2025 · 0 comments
Labels
bug Something isn't working node:http

Comments

@keverw
Copy link

keverw commented Jan 8, 2025

What version of Bun is running?

1.1.43+76800b049

What platform is your computer?

Darwin 23.6.0 arm64 arm

What steps can reproduce the bug?

mTLS-demo.ts:

/*************************************
 * bun run mTLS-demo.ts
 * Given Output: Server response: Client certificate required
 *
 *  OR NODE
 *
 * bun build mTLS-demo.ts --outfile mtls-node.js --target=node
 * node mtls-node.js
 * Expected output: Server response: Hello, mutual TLS world!
 *
 * Demonstrates how to:
 *  - Generate a CA (self-signed)
 *  - Generate Server + Client certs
 *  - Include SANs (Subject Alt Names) for localhost and 0.0.0.0
 *  - Spin up an mTLS Node server
 *  - Make a request using the client cert
 *************************************/

import * as forge from "node-forge";
import * as https from "https";
import { IncomingMessage, ServerResponse } from "http";

/**
 * The shape of our returned certs object.
 */
interface MTLSCerts {
  ca: {
    cert: string; // PEM-encoded CA cert
    key: string; // PEM-encoded CA private key
  };
  server: {
    cert: string; // PEM-encoded Server cert
    key: string; // PEM-encoded Server private key
  };
  client: {
    cert: string; // PEM-encoded Client cert
    key: string; // PEM-encoded Client private key
  };
}

/**
 * Generates an RSA key pair (2048 bits).
 */
function generateKeyPair(): forge.pki.KeyPair {
  return forge.pki.rsa.generateKeyPair({
    bits: 2048,
    e: 0x10001,
  });
}

/**
 * Certificate options interface for clarity.
 */
interface CertificateOptions {
  commonName: string;
  serialNumber: string;
  issuerCert?: forge.pki.Certificate;
  issuerKey?: forge.pki.PrivateKey;
  daysValid?: number;
  isCA?: boolean;
  /**
   * Optional Subject Alternative Names to include in the cert.
   * Defaults to DNS:commonName if none provided.
   */
  altNames?: forge.pki.CertificateExtensionSubjectAltName[];
}

/**
 * Creates an X.509 certificate.
 * If no issuer cert/key provided, it becomes self-signed (for CA).
 */
function createCertificate(
  keyPair: forge.pki.KeyPair,
  options: CertificateOptions
): forge.pki.Certificate {
  const {
    commonName,
    serialNumber,
    issuerCert,
    issuerKey,
    daysValid = 365,
    isCA = false,
    altNames,
  } = options;

  const cert = forge.pki.createCertificate();
  cert.publicKey = keyPair.publicKey;
  cert.serialNumber = serialNumber;

  // Validity
  const now = new Date();
  cert.validity.notBefore = new Date(now.getTime() - 5 * 60 * 1000); // 5 min in the past
  cert.validity.notAfter = new Date(
    now.getTime() + daysValid * 24 * 60 * 60 * 1000
  );

  // If no issuer, self-signed for CA. Otherwise, use issuer's subject.
  const issuerAttrs = issuerCert
    ? issuerCert.subject.attributes
    : [
        { name: "commonName", value: commonName },
        { name: "countryName", value: "US" },
        { shortName: "ST", value: "CA" },
        { name: "organizationName", value: "MyOrg Inc." },
      ];

  cert.setSubject([
    { name: "commonName", value: commonName },
    { name: "countryName", value: "US" },
    { shortName: "ST", value: "CA" },
    { name: "organizationName", value: "MyOrg Inc." },
  ]);
  cert.setIssuer(issuerAttrs);

  // Default altNames to DNS:commonName if not specified
  const altNamesToUse = altNames?.length
    ? altNames
    : [{ type: 2, value: commonName }];

  // Basic constraints, key usage, alt names
  cert.setExtensions([
    {
      name: "basicConstraints",
      cA: isCA,
      critical: isCA,
    },
    {
      name: "keyUsage",
      digitalSignature: true,
      keyEncipherment: true,
      dataEncipherment: true,
      keyCertSign: isCA,
      cRLSign: isCA,
    },
    {
      name: "subjectAltName",
      altNames: altNamesToUse,
    },
  ]);

  // Sign with issuer key (or self if CA)
  const signingKey = issuerKey || keyPair.privateKey;
  cert.sign(signingKey, forge.md.sha256.create());

  return cert;
}

/**
 * Generates a self-signed CA, plus server/client certificates
 * in PEM format. Returns them in a single object.
 */
function generateMTLSCerts(): MTLSCerts {
  // --- 1) CA (self-signed) ---
  const caKeyPair = generateKeyPair();
  const caCert = createCertificate(caKeyPair, {
    commonName: "MyRootCA",
    serialNumber: "01",
    isCA: true,
    daysValid: 3650, // 10-year CA
  });

  // --- 2) Server certificate (signed by CA) ---
  // Subject Alt Names include server.example.com, localhost, 127.0.0.1, and 0.0.0.0
  const serverKeyPair = generateKeyPair();
  const serverCert = createCertificate(serverKeyPair, {
    commonName: "server.example.com",
    serialNumber: "02",
    issuerCert: caCert,
    issuerKey: caKeyPair.privateKey,
    daysValid: 825, // ~2 years
    altNames: [
      { type: 2, value: "server.example.com" }, // DNS
      { type: 2, value: "localhost" }, // DNS
      { type: 7, ip: "127.0.0.1" }, // IP
      { type: 7, ip: "0.0.0.0" }, // IP
    ],
  });

  // --- 3) Client certificate (signed by CA) ---
  const clientKeyPair = generateKeyPair();
  const clientCert = createCertificate(clientKeyPair, {
    commonName: "client.example.com",
    serialNumber: "03",
    issuerCert: caCert,
    issuerKey: caKeyPair.privateKey,
    daysValid: 825,
  });

  return {
    ca: {
      cert: forge.pki.certificateToPem(caCert),
      key: forge.pki.privateKeyToPem(caKeyPair.privateKey),
    },
    server: {
      cert: forge.pki.certificateToPem(serverCert),
      key: forge.pki.privateKeyToPem(serverKeyPair.privateKey),
    },
    client: {
      cert: forge.pki.certificateToPem(clientCert),
      key: forge.pki.privateKeyToPem(clientKeyPair.privateKey),
    },
  };
}

// -------------------------------------------------------------------
// MAIN: Start an HTTPS server requiring client cert (mTLS).
//       Then make a request with our "client cert" to show it works.
// -------------------------------------------------------------------
async function main() {
  // Generate PEM-encoded CA/server/client certs
  const { ca, server, client } = generateMTLSCerts();
  const port = 8443;

  // Create the server with mTLS
  const serverOptions: https.ServerOptions = {
    key: server.key,
    cert: server.cert,

    // The server trusts our CA (so it’ll trust the client cert).
    ca: ca.cert,

    // Require client cert, otherwise reject.
    requestCert: true,
    rejectUnauthorized: true,
  };

  // Create the HTTPS server
  const httpsServer = https.createServer(
    serverOptions,
    (req: IncomingMessage, res: ServerResponse) => {
      if (req.socket.authorized) {
        res.writeHead(200);
        res.end("Hello, mutual TLS world!\n");
      } else {
        // If client cert wasn't validated
        res.writeHead(401);
        res.end("Client certificate required.\n");
      }
    }
  );

  httpsServer.listen(port, () => {
    console.log(`mTLS server is listening on https://localhost:${port}`);

    // Once the server is up, make a request using the client cert/key.
    const clientOptions: https.RequestOptions = {
      hostname: "localhost",
      port,
      method: "GET",
      path: "/",
      // Provide the client's key & cert
      key: client.key,
      cert: client.cert,
      // The client trusts the same CA
      ca: ca.cert,
      rejectUnauthorized: true,
    };

    const req = https.request(clientOptions, (res) => {
      let data = "";
      res.on("data", (chunk) => (data += chunk));
      res.on("end", () => {
        console.log("Server response:", data);

        // Cleanly close the server for this demo
        httpsServer.close();
      });
    });

    req.on("error", (err) => {
      console.error("Client request error:", err);
      httpsServer.close();
    });

    req.end();
  });
}

// Run the main function
main();

package.json:

{
  "type": "module",
  "dependencies": {
    "node-forge": "^1.3.1"
  }
}

What is the expected behavior?

On Bun, should be able to start the server and request to it , getting the message "Server response: Hello, mutual TLS world!"

What do you see instead?

Getting "Server response: Client certificate required". It seems like req.socket.authorized is always undefined.

Additional information

Was thinking of doing a server management utility, and mTLS would be great for internal cross-server communication, combined with the single binary option Bun has.

@keverw keverw added bug Something isn't working needs triage labels Jan 8, 2025
@keverw keverw changed the title mTLS mTLS implementation issues Jan 8, 2025
@keverw keverw changed the title mTLS implementation issues mTLS Implementation Issues Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working node:http
Projects
None yet
Development

No branches or pull requests

2 participants