mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-11-12 12:36:55 +00:00
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|||
|
|
import crypto from "crypto";
|
||
|
|
import {
|
||
|
|
buildAuthorizationUrl,
|
||
|
|
certificateFingerprint,
|
||
|
|
createPkce,
|
||
|
|
decodeJwtClaims,
|
||
|
|
generateSshKeyPair,
|
||
|
|
normalizeCaUrl,
|
||
|
|
normalizeFingerprint,
|
||
|
|
parseSshCertificate,
|
||
|
|
} from "../../utils/step-ca-client.js";
|
||
|
|
import {
|
||
|
|
generateCa,
|
||
|
|
signUserCertificate,
|
||
|
|
} from "../../database/routes/ssh-certificate.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 der = crypto.randomBytes(64);
|
||
|
|
const pem = `-----BEGIN CERTIFICATE-----\n${der.toString("base64")}\n-----END CERTIFICATE-----\n`;
|
||
|
|
expect(certificateFingerprint(pem)).toBe(
|
||
|
|
crypto.createHash("sha256").update(der).digest("hex"),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
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 whose public line 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 ca = generateCa();
|
||
|
|
const cert = signUserCertificate({
|
||
|
|
userPublicKeyLine: publicKeyLine,
|
||
|
|
caPrivateKeyPem: ca.privateKeyPem,
|
||
|
|
caPublicKeyLine: ca.publicKeyLine,
|
||
|
|
keyId: "alice@example",
|
||
|
|
principals: ["alice", "ops"],
|
||
|
|
validAfter: 1_700_000_000,
|
||
|
|
validBefore: 1_700_057_600,
|
||
|
|
});
|
||
|
|
expect(cert).not.toBeNull();
|
||
|
|
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/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
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({});
|
||
|
|
});
|
||
|
|
});
|