import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createVerify, generateKeyPairSync } from "node:crypto"; import { ConfigService } from "@nestjs/config"; import { EimsCredentialsProvider } from "./eims-credentials.provider"; import { EimsSignerService, toSignedBody } from "./eims-signer.service"; /** * Test-only key material: generated per run, never a production key. The "certificate" fixture is * an arbitrary byte blob — the point is that its exact bytes survive base64 round-tripping, not * that it is a valid X.509 chain. */ const CERTIFICATE_FIXTURE = "Subject: CN=TEST\n-----BEGIN CERTIFICATE-----\nZm9vYmFy\n-----END CERTIFICATE-----\n"; let dir: string; let keyPath: string; let certPath: string; let publicKeyPem: string; let signer: EimsSignerService; beforeAll(() => { dir = mkdtempSync(join(tmpdir(), "eims-signer-")); keyPath = join(dir, "private_key.key"); certPath = join(dir, "certificate.pem.txt"); const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); writeFileSync(keyPath, privateKey.export({ type: "pkcs8", format: "pem" })); writeFileSync(certPath, CERTIFICATE_FIXTURE, "utf8"); publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString(); const config = { get: () => ({ privateKeyPath: keyPath, certificatePath: certPath }), } as unknown as ConfigService; signer = new EimsSignerService(new EimsCredentialsProvider(config)); }); afterAll(() => rmSync(dir, { recursive: true, force: true })); const login = () => ({ clientId: "cid", clientSecret: "secret", apikey: "key", tin: "0000000000" }); const verify = (payload: string, signature: string): boolean => createVerify("RSA-SHA512").update(payload, "utf8").verify(publicKeyPem, signature, "base64"); describe("EimsSignerService", () => { it("produces a signature that verifies against the matching public key", () => { const signed = signer.signRequest(login()); expect(verify(JSON.stringify(signed.request), signed.signature)).toBe(true); }); it("fails verification when a single request field changes", () => { const signed = signer.signRequest(login()); const tampered = JSON.stringify({ ...signed.request, tin: "9999999999" }); expect(verify(tampered, signed.signature)).toBe(false); }); it("emits a 256-byte signature for an RSA-2048 key", () => { const signed = signer.signRequest(login()); expect(Buffer.from(signed.signature, "base64")).toHaveLength(256); }); it("sends the certificate as base64 of the file's exact bytes", () => { const signed = signer.signRequest(login()); expect(signed.certificate).toBe(readFileSync(certPath).toString("base64")); expect(Buffer.from(signed.certificate, "base64").equals(readFileSync(certPath))).toBe(true); }); it("signs the inner request only, and the wire body carries those exact bytes", () => { const signed = signer.signRequest(login()); const body = toSignedBody(signed); // The signed string appears verbatim inside the transmitted envelope. expect(body).toContain(`"request":${JSON.stringify(signed.request)}`); // Compact, never pretty-printed. expect(body).not.toMatch(/\n/); expect(JSON.parse(body)).toEqual({ request: login(), signature: signed.signature, certificate: signed.certificate, }); }); it("does not mutate the request object", () => { const request = login(); const signed = signer.signRequest(request); expect(signed.request).toBe(request); expect(request).toEqual(login()); }); it("reuses the loaded key and certificate across calls", () => { const first = signer.signRequest(login()); const second = signer.signRequest(login()); // PKCS#1 v1.5 is deterministic: same key + same payload ⇒ identical signature. expect(second.signature).toBe(first.signature); expect(second.certificate).toBe(first.certificate); }); }); describe("EimsCredentialsProvider", () => { const providerFor = (paths: { privateKeyPath?: string; certificatePath?: string }) => new EimsCredentialsProvider({ get: () => paths } as unknown as ConfigService); it("fails clearly when the key path is unset", () => { expect(() => providerFor({}).getPrivateKey()).toThrow(/EIMS_PRIVATE_KEY_PATH is not set/); }); it("fails clearly when the key file is missing", () => { expect(() => providerFor({ privateKeyPath: join(dir, "nope.key") }).getPrivateKey()).toThrow( /could not be read or parsed/, ); }); it("fails clearly when the certificate file is empty", () => { const emptyPath = join(dir, "empty.txt"); writeFileSync(emptyPath, ""); expect(() => providerFor({ certificatePath: emptyPath }).getCertificateBase64()).toThrow(/is empty/); }); });