Files
documenso/packages/ee/server-only/signing/csc/tsp-timestamp-authority.ts
T
Lucas Smith d5ce222482 feat: add CSC AES/QES signing (v1 instance-wide config) (#2874)
Adds Cloud Signature Consortium (CSC) integration for AES/QES signing
against a configured TSP. v1 ships as instance-wide configuration via
environment variables, with per-envelope signature level selection,
license gating, and an OAuth-driven signing flow (capture + FIFO
signers, SAD session, blocking/in-progress recipient pages).

Includes signature level compatibility checks (role, signing order,
dictate next signer), envelope mutability assertions, Prisma migration
for signature level and CSC tables, and docs for the new signing
certificate options.
2026-06-16 23:37:34 +10:00

83 lines
3.1 KiB
TypeScript

import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { DigestAlgorithm, TimestampAuthority } from '@libpdf/core';
import { hashOidForDigest } from './algorithm-resolver';
import { cscTimestamp } from './client/signatures';
import type { CscTransport } from './transport';
/**
* libpdf {@link TimestampAuthority} backed by the CSC TSP's
* `signatures/timestamp` endpoint (§11.10).
*
* Used only at sign time, per recipient, when {@link resolveCscSignTimeTsa}
* selects the TSP source — that is, when the TSP advertises
* `signatures/timestamp` in `info.methods`. The token wired in is the
* current recipient's own service-scope bearer (the same one authorising
* the `signatures/signHash` call alongside it), so the timestamp gets
* attributed to the same identity that just authorised the signature.
*
* Seal-time archival timestamps do not use this class — they go through
* the env-only path in `finalize-tsp-completion.ts`.
*
* Failure semantics: a single `signatures/timestamp` call. On any error
* (HTTP, schema, expired token) we surface `CSC_PROVIDER_NO_TSA` with the
* upstream message folded in. There's no try-in-order — at sign time the
* recipient is fixed, so there's no other token to fall through to.
*/
type CscTspTimestampAuthorityOptions = {
transport: CscTransport;
/** Decrypted service-scope access token for the current recipient. */
serviceToken: string;
/** Optional deadline for the `signatures/timestamp` call. */
signal?: AbortSignal;
};
export class CscTspTimestampAuthority implements TimestampAuthority {
private readonly transport: CscTransport;
private readonly serviceToken: string;
private readonly signal?: AbortSignal;
constructor(opts: CscTspTimestampAuthorityOptions) {
this.transport = opts.transport;
this.serviceToken = opts.serviceToken;
this.signal = opts.signal;
}
/**
* Request a CSC §11.10 timestamp for the supplied digest, authorised with
* the recipient's service-scope bearer. Returns the decoded TimeStampToken
* bytes. Throws `CSC_PROVIDER_NO_TSA` carrying the upstream error message
* on failure.
*
* `algorithm` is libpdf's `DigestAlgorithm` (`SHA-256` / `SHA-384` /
* `SHA-512`), translated to the matching `hashAlgo` OID via the existing
* {@link hashOidForDigest} mapping so the spec's OID-typed payload stays
* in one place.
*/
async timestamp(digest: Uint8Array, algorithm: DigestAlgorithm): Promise<Uint8Array> {
const hash = Buffer.from(digest).toString('base64');
const hashAlgo = hashOidForDigest(algorithm);
try {
const response = await cscTimestamp({
baseUrl: this.transport.serviceBaseUrl,
accessToken: this.serviceToken,
hash,
hashAlgo,
signal: this.signal,
});
return Buffer.from(response.timestamp, 'base64');
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new AppError(AppErrorCode.CSC_PROVIDER_NO_TSA, {
message: `CSC TSP timestamp endpoint refused the recipient's service token: ${message}.`,
});
}
}
}