mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
d5ce222482
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.
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import { cscJsonPost, joinCscUrl } from './http';
|
|
import {
|
|
type TCscSignHashRequest,
|
|
type TCscSignHashResponse,
|
|
type TCscTimestampRequest,
|
|
type TCscTimestampResponse,
|
|
ZCscSignHashResponseSchema,
|
|
ZCscTimestampResponseSchema,
|
|
} from './types';
|
|
|
|
type CscSignHashOptions = TCscSignHashRequest & {
|
|
baseUrl: string;
|
|
/** Service-scope bearer token. The SAD (in the body) is the credential-scope grant. */
|
|
accessToken: string;
|
|
signal?: AbortSignal;
|
|
};
|
|
|
|
/**
|
|
* `signatures/signHash` (§11.9) — submit one or more pre-computed hashes for
|
|
* the TSP to sign with the credential identified by `credentialID`.
|
|
*
|
|
* Authorisation is two-layered:
|
|
* - The service-scope bearer token authenticates the API call itself.
|
|
* - The credential-scope SAD (in the JSON body) authorises the specific
|
|
* hashes — the TSP rejects with `invalid_request` ("Hash is not authorized
|
|
* by the SAD") if any hash in the array wasn't bound at SAD issuance.
|
|
*
|
|
* The returned `signatures` array is position-ordered with `hash` per §11.9.
|
|
* Callers SHALL preserve order when mapping responses back to PDF embed
|
|
* slots (the fifoSigner relies on this).
|
|
*/
|
|
export const cscSignHash = async (opts: CscSignHashOptions): Promise<TCscSignHashResponse> => {
|
|
const { baseUrl, accessToken, signal, credentialID, SAD, hash, hashAlgo, signAlgo, signAlgoParams, clientData } =
|
|
opts;
|
|
|
|
const body: Record<string, unknown> = {
|
|
credentialID,
|
|
SAD,
|
|
hash,
|
|
signAlgo,
|
|
};
|
|
|
|
if (hashAlgo !== undefined) {
|
|
body.hashAlgo = hashAlgo;
|
|
}
|
|
|
|
if (signAlgoParams !== undefined) {
|
|
body.signAlgoParams = signAlgoParams;
|
|
}
|
|
|
|
if (clientData !== undefined) {
|
|
body.clientData = clientData;
|
|
}
|
|
|
|
return await cscJsonPost(
|
|
{
|
|
url: joinCscUrl({ baseUrl, path: 'signatures/signHash' }),
|
|
body,
|
|
accessToken,
|
|
signal,
|
|
},
|
|
ZCscSignHashResponseSchema,
|
|
);
|
|
};
|
|
|
|
type CscTimestampOptions = TCscTimestampRequest & {
|
|
baseUrl: string;
|
|
/**
|
|
* Service-scope bearer token. Per §11.10 the timestamp endpoint may or may
|
|
* not require auth depending on TSP policy; the spec is silent. We send the
|
|
* token unconditionally because all known TSPs gate this endpoint.
|
|
*/
|
|
accessToken: string;
|
|
signal?: AbortSignal;
|
|
};
|
|
|
|
/**
|
|
* `signatures/timestamp` (§11.10) — request an RFC 3161 / RFC 5816 time-stamp
|
|
* token for a pre-computed hash. Driven by {@link CscTspTimestampAuthority}
|
|
* at sign time, when {@link resolveCscSignTimeTsa} selects the TSP source
|
|
* (TSP advertises `signatures/timestamp` in `info.methods`). The bearer is
|
|
* the current recipient's own service-scope token. Seal-time archival
|
|
* timestamps do not go through this endpoint — they use the env-configured
|
|
* RFC 3161 TSA directly.
|
|
*
|
|
* If `nonce` is supplied, the TSP MUST round-trip it in the token — we leave
|
|
* verification to LibPDF / our TSA helper, not this client.
|
|
*/
|
|
export const cscTimestamp = async (opts: CscTimestampOptions): Promise<TCscTimestampResponse> => {
|
|
const { baseUrl, accessToken, signal, hash, hashAlgo, nonce, clientData } = opts;
|
|
|
|
const body: Record<string, unknown> = { hash, hashAlgo };
|
|
|
|
if (nonce !== undefined) {
|
|
body.nonce = nonce;
|
|
}
|
|
|
|
if (clientData !== undefined) {
|
|
body.clientData = clientData;
|
|
}
|
|
|
|
return await cscJsonPost(
|
|
{
|
|
url: joinCscUrl({ baseUrl, path: 'signatures/timestamp' }),
|
|
body,
|
|
accessToken,
|
|
signal,
|
|
},
|
|
ZCscTimestampResponseSchema,
|
|
);
|
|
};
|