fix: default unset signing transport to local (#3309)

`/api/health` and `/api/certificate-status` reported the cert as
available when `NEXT_PRIVATE_SIGNING_TRANSPORT` was unset, even though
sealing defaults to the local P12 and fails if it is missing,
unreadable, or expired.
This commit is contained in:
Catalin Pit
2026-09-04 08:17:42 +10:00
committed by GitHub
parent 3ec877a68b
commit cbb1cf7bef
6 changed files with 42 additions and 20 deletions
@@ -1,8 +1,8 @@
import { getCertificateStatus } from '@documenso/lib/server-only/cert/cert-status';
export const loader = () => {
export const loader = async () => {
try {
const certStatus = getCertificateStatus();
const certStatus = await getCertificateStatus();
return Response.json({
isAvailable: certStatus.isAvailable,
+1 -1
View File
@@ -22,7 +22,7 @@ export const loader = async () => {
}
try {
const certStatus = getCertificateStatus();
const certStatus = await getCertificateStatus();
if (certStatus.isAvailable) {
checks.certificate = { status: 'ok' };
+2
View File
@@ -93,6 +93,8 @@ export const NEXT_PRIVATE_USE_PLAYWRIGHT_PDF = () => env('NEXT_PRIVATE_USE_PLAYW
export const NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY = () => env('NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY');
export const NEXT_PRIVATE_SIGNING_TRANSPORT = () => env('NEXT_PRIVATE_SIGNING_TRANSPORT') || 'local';
/**
* Whether this Documenso instance is running in CSC (Cloud Signature Consortium) mode.
*
+23 -13
View File
@@ -1,26 +1,36 @@
import * as fs from 'node:fs';
import { X509Certificate } from 'node:crypto';
import { env } from '@documenso/lib/utils/env';
import { createLocalSigner } from '@documenso/signing/transports/local';
export const getCertificateStatus = () => {
if (env('NEXT_PRIVATE_SIGNING_TRANSPORT') !== 'local') {
import { NEXT_PRIVATE_SIGNING_TRANSPORT } from '../../constants/app';
/**
* Whether the local P12 opens with the configured passphrase and is in date.
* Skips AIA so this stays offline. gcloud-hsm and csc always report available.
*/
export const getCertificateStatus = async () => {
const transport = NEXT_PRIVATE_SIGNING_TRANSPORT();
// Cannot inspect a remote HSM or CSC provider from this process.
if (transport === 'gcloud-hsm' || transport === 'csc') {
return { isAvailable: true };
}
if (env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS')) {
return { isAvailable: true };
// Anything else (typo, leftover `http`) would throw at seal time.
if (transport !== 'local') {
return { isAvailable: false };
}
const defaultPath = env('NODE_ENV') === 'production' ? '/opt/documenso/cert.p12' : './example/cert.p12';
const filePath = env('NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH') || defaultPath;
try {
fs.accessSync(filePath, fs.constants.F_OK | fs.constants.R_OK);
const signer = await createLocalSigner({ buildChain: false });
const stats = fs.statSync(filePath);
const certificate = new X509Certificate(Buffer.from(signer.certificate));
return { isAvailable: stats.size > 0 };
const now = new Date();
const isWithinValidityPeriod = new Date(certificate.validFrom) <= now && now <= new Date(certificate.validTo);
return { isAvailable: isWithinValidityPeriod };
} catch {
return { isAvailable: false };
}
+2 -2
View File
@@ -1,9 +1,9 @@
import {
NEXT_PRIVATE_SIGNING_TRANSPORT,
NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER,
NEXT_PUBLIC_SIGNING_CONTACT_INFO,
NEXT_PUBLIC_WEBAPP_URL,
} from '@documenso/lib/constants/app';
import { env } from '@documenso/lib/utils/env';
import type { PDF, Signer } from '@libpdf/core';
import { match } from 'ts-pattern';
@@ -22,7 +22,7 @@ const getSigner = async () => {
return signer;
}
const transport = env('NEXT_PRIVATE_SIGNING_TRANSPORT') || 'local';
const transport = NEXT_PRIVATE_SIGNING_TRANSPORT();
// eslint-disable-next-line require-atomic-updates
signer = await match(transport)
+12 -2
View File
@@ -22,10 +22,20 @@ const loadP12 = (): Uint8Array => {
throw new Error('No certificate found for local signing');
};
export const createLocalSigner = async () => {
export type CreateLocalSignerOptions = {
/**
* Fetch missing intermediates via AIA. Leave on for sealing.
* Turn off for health checks so they do not hit the network.
*
* @default true
*/
buildChain?: boolean;
};
export const createLocalSigner = async ({ buildChain = true }: CreateLocalSignerOptions = {}) => {
const p12 = loadP12();
return await P12Signer.create(p12, env('NEXT_PRIVATE_SIGNING_PASSPHRASE') || '', {
buildChain: true,
buildChain,
});
};