Files
Termix/plugins/step-ca/tests/backend/client.test.ts
T

137 lines
4.6 KiB
TypeScript

import crypto from "node:crypto";
import { describe, expect, it } from "vitest";
import {
buildAuthorizationUrl,
certificateFingerprint,
createPkce,
decodeJwtClaims,
fetchRootCertificate,
findOidcProvisioner,
generateSshKeyPair,
normalizeCaUrl,
normalizeFingerprint,
parseSshCertificate,
} from "../../src/backend/client.js";
import { parseHostList } from "../../src/backend/auth-session.js";
import { CA_URL, createFakeCa, createSshCa, fakeRoot } from "./fake-ca.js";
describe("step-ca client helpers", () => {
it("normalizes the CA url and fingerprint the way step does", () => {
expect(normalizeCaUrl(" https://ca.internal:9000/ ")).toBe(
"https://ca.internal:9000",
);
expect(() => normalizeCaUrl("http://ca.internal")).toThrow(/https/);
const fp = "AB:cd".repeat(16).replace(/:/g, "");
expect(normalizeFingerprint("AB:cd".repeat(16))).toBe(fp.toLowerCase());
expect(() => normalizeFingerprint("abcd")).toThrow(/SHA-256/);
});
it("fingerprints a PEM certificate by the sha256 of its DER", () => {
const root = fakeRoot();
expect(certificateFingerprint(root.pem)).toBe(root.fingerprint);
});
it("builds a PKCE authorization request", () => {
const { verifier, challenge } = createPkce();
expect(challenge).toBe(
crypto.createHash("sha256").update(verifier).digest("base64url"),
);
const url = new URL(
buildAuthorizationUrl({
authorizationEndpoint: "https://idp.example/auth?tenant=x",
clientId: "cid",
redirectUri: "https://termix.example/callback",
state: "s",
nonce: "n",
codeChallenge: challenge,
}),
);
expect(url.searchParams.get("tenant")).toBe("x");
expect(url.searchParams.get("response_type")).toBe("code");
expect(url.searchParams.get("code_challenge_method")).toBe("S256");
expect(url.searchParams.get("scope")).toContain("openid");
});
it("generates an ed25519 key a CA can certify, and reads the cert back", () => {
const { publicKeyLine, privateKeyPem } = generateSshKeyPair();
expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/=]+$/);
expect(privateKeyPem).toContain("BEGIN PRIVATE KEY");
const cert = createSshCa().sign({
publicKeyBlob: publicKeyLine.split(" ")[1],
keyId: "alice@example",
principals: ["alice", "ops"],
validAfter: 1_700_000_000,
validBefore: 1_700_057_600,
});
const info = parseSshCertificate(cert);
expect(info).toMatchObject({
keyType: "ssh-ed25519-cert-v01@openssh.com",
publicKeyLine,
keyId: "alice@example",
principals: ["alice", "ops"],
});
expect(info.validAfter.toISOString()).toBe("2023-11-14T22:13:20.000Z");
expect(info.validBefore.getTime() - info.validAfter.getTime()).toBe(
16 * 3600 * 1000,
);
expect(() => parseSshCertificate(publicKeyLine)).toThrow(/certificate/);
expect(() =>
parseSshCertificate("ssh-ed25519-cert-v01@openssh.com AAAA"),
).toThrow(/certificate/);
});
it("reads the private endpoint list with commas or new lines", () => {
expect(parseHostList("ca.internal, SSO.internal\nidp.lan")).toEqual([
"ca.internal",
"sso.internal",
"idp.lan",
]);
expect(parseHostList(undefined)).toEqual([]);
});
});
describe("talking to the CA", () => {
it("refuses a root whose fingerprint does not match", async () => {
const ca = createFakeCa({ nonce: () => "" });
await expect(
fetchRootCertificate({
fetch: ca.fetch,
caUrl: CA_URL,
fingerprint: "b".repeat(64),
allowedPrivateHosts: [],
}),
).rejects.toThrow();
});
it("finds the OIDC provisioner by name and passes the private hosts", async () => {
const ca = createFakeCa({ nonce: () => "" });
const target = {
fetch: ca.fetch,
caUrl: CA_URL,
fingerprint: ca.root.fingerprint,
allowedPrivateHosts: ["ca.test"],
};
const root = await fetchRootCertificate(target);
const provisioner = await findOidcProvisioner(target, root, "oidc");
expect(provisioner.clientID).toBe("termix");
expect(ca.calls.every((call) => call.init?.allowPrivateHosts)).toBe(true);
await expect(findOidcProvisioner(target, root, "jwk")).rejects.toThrow(
/not found/,
);
});
});
describe("decodeJwtClaims", () => {
it("reads the payload without verifying and tolerates junk", () => {
const payload = Buffer.from(
JSON.stringify({ email: "a@b.c", nonce: "n1" }),
).toString("base64url");
expect(decodeJwtClaims(`x.${payload}.y`)).toEqual({
email: "a@b.c",
nonce: "n1",
});
expect(decodeJwtClaims("not-a-jwt")).toEqual({});
});
});