Files
edr-platform/apps/edr-freight-api/src/modules/eims/eims-signer.service.ts
Hagernesh 8d53dc17ce docs(eims): confirm signing algorithm and cert format against MoR's guide
Cross-checked our RSA-SHA512 signing and raw-bytes certificate encoding
against MoR's own "Guide to Generating and Using Certificate for E-Invoicing"
(supplied today). Both were previously documented as our best inference from
the Postman collection; the guide names SHA512withRSA explicitly (PKCS#1v1.5,
matching Node's createSign default) and its own worked example certificate is
byte-for-byte the same Subject:/Issuer: + 3-cert PEM chain text-file format
ours is. No behavior change -- the comment now says confirmed, not assumed.

Field order, section names, date format and the {request, signature,
certificate} envelope in the guide's worked example all match our mapper
exactly (order doesn't matter per the guide, but it's a further concordance
check). The one guide/live disagreement -- its example shows "NatureOfSupplies":
"Goods" where our actual 400 SCHEMA ERROR demanded lowercase "goods"/"service"
-- is left as-is: the live, machine-generated schema error outranks a static
doc example that may predate a schema change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:08:51 +00:00

43 lines
2.0 KiB
TypeScript

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<T>(request: T): EimsSignedRequest<T> {
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 = <T>(signed: EimsSignedRequest<T>): string => JSON.stringify(signed);