testServeTlsPinning

tcmts.cli.test.e2e.sync.testServeTlsPinning · #jrm37yak · term

tcm serve --tls: clients pin the certificate and never send the token to an impostor

import { plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import { codebase, startServerInfo } from "#codebase/tcmts/cli/test/e2e/sync";
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import tls from "node:tls";

export const testServeTlsPinning = () =>
  withChildren(async (keep) => {
    const root = tempDir("sync");
    const server = codebase(root, "tls-server");
    const client = codebase(root, "tls-client");
    client.tcm("add");
    const { url, fingerprint } = await startServerInfo(keep, server.dir, "tlstok", "--tls");
    assert.match(url, /^https:/);
    client.tcm("remote", "add", "s", url, "--token", "tlstok", "--fingerprint", fingerprint!);
    assert.match(client.tcm("run", "square", "7", "--on", "s"), /^49$/m);

    // an impostor with a different certificate: the handshake completes, but no request bytes may follow
    const other = codebase(root, "impostor");
    spawnSync(process.execPath, [tcmBin(), "serve", "--tls", "--port", "1"], { cwd: other.dir, env: plainEnv(), timeout: 5000 }); // creates other/.tcm/tls
    const received: Buffer[] = [];
    const impostor = tls.createServer(
      { cert: fs.readFileSync(path.join(other.dir, ".tcm/tls/cert.pem")), key: fs.readFileSync(path.join(other.dir, ".tcm/tls/key.pem")) },
      (socket) => socket.on("data", (d) => received.push(d)),
    );
    try {
      await new Promise<void>((r) => impostor.listen(0, "127.0.0.1", r));
      const port = (impostor.address() as { port: number }).port;
      client.tcm("remote", "add", "fake", `https://127.0.0.1:${port}`, "--token", "tlstok", "--fingerprint", fingerprint!);
      // run the client asynchronously: the impostor lives in this process and must keep serving the handshake
      const attempt = await new Promise<{ code: number | null; out: string }>((resolve) => {
        const child = keep(spawn(process.execPath, [tcmBin(), "run", "square", "7", "--on", "fake"], { cwd: client.dir, env: plainEnv() }));
        let out = "";
        child.stdout.on("data", (d) => (out += d));
        child.stderr.on("data", (d) => (out += d));
        child.on("close", (code) => resolve({ code, out }));
      });
      assert.equal(attempt.code, 1);
      assert.match(attempt.out, /fingerprint mismatch[\s\S]*refusing to send the token/);
      await new Promise((r) => setTimeout(r, 200));
    } finally {
      impostor.close();
    }
    assert.equal(Buffer.concat(received).length, 0, "nothing, not even headers, reached the impostor");

    assert.throws(() => server.tcm("serve", "--host", "0.0.0.0", "--port", "0"), /without TLS would send the token in clear text/);
  })

uses