import { createSign } from "node:crypto"; import { Injectable } from "@nestjs/common"; import { EimsCredentialsProvider } from "./eims-credentials.provider"; import { EimsSignedRequest } from "./eims.types"; /** * Signs EIMS request objects, reproducing the process that produced a working live access token: * * 1. compact `JSON.stringify` of the **inner** request object only, * 2. those exact UTF-8 bytes, * 3. RSA + SHA-512 (`SHA512withRSA`, PKCS#1 v1.5 — Node's default RSA padding). Confirmed, not * assumed: MoR's own "Guide to Generating and Using Certificate for E-Invoicing" names * `SHA512withRSA` explicitly, which is PKCS#1v1.5 in Java (PSS would be named * `SHA512withRSAandMGF1`) — the same padding `createSign("RSA-SHA512")` uses by default. * 4. base64 of the raw signature bytes (256 bytes for an RSA-2048 key), * 5. base64 of the certificate file's exact bytes. Also confirmed by the same guide: its own * worked example certificate is the identical `Subject:`/`Issuer:` header + 3-cert PEM chain * text-file format ours is, base64'd with no re-encoding. * * The outer `{request, signature, certificate}` envelope is never itself signed, and the request * object is never mutated after serialization. */ @Injectable() export class EimsSignerService { constructor(private readonly credentials: EimsCredentialsProvider) {} signRequest(request: T): EimsSignedRequest { const payload = JSON.stringify(request); const signature = createSign("RSA-SHA512") .update(payload, "utf8") .sign(this.credentials.getPrivateKey(), "base64"); return { request, signature, certificate: this.credentials.getCertificateBase64() }; } } /** * Exact wire body for a signed envelope. Serializing here (rather than handing axios an object) * keeps one serializer in play: the `request` segment of this string is byte-identical to the * string that was signed. */ export const toSignedBody = (signed: EimsSignedRequest): string => JSON.stringify(signed);