diff --git a/apps/remix/app/routes/api+/certificate-status.ts b/apps/remix/app/routes/api+/certificate-status.ts index eb8ae6cfd..5e4ea8be4 100644 --- a/apps/remix/app/routes/api+/certificate-status.ts +++ b/apps/remix/app/routes/api+/certificate-status.ts @@ -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, diff --git a/apps/remix/app/routes/api+/health.ts b/apps/remix/app/routes/api+/health.ts index c43ac1478..1cf8ccba9 100644 --- a/apps/remix/app/routes/api+/health.ts +++ b/apps/remix/app/routes/api+/health.ts @@ -22,7 +22,7 @@ export const loader = async () => { } try { - const certStatus = getCertificateStatus(); + const certStatus = await getCertificateStatus(); if (certStatus.isAvailable) { checks.certificate = { status: 'ok' }; diff --git a/packages/lib/constants/app.ts b/packages/lib/constants/app.ts index 9740fae22..17ded9d66 100644 --- a/packages/lib/constants/app.ts +++ b/packages/lib/constants/app.ts @@ -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. * diff --git a/packages/lib/server-only/cert/cert-status.ts b/packages/lib/server-only/cert/cert-status.ts index 737989eea..a3c0fd222 100644 --- a/packages/lib/server-only/cert/cert-status.ts +++ b/packages/lib/server-only/cert/cert-status.ts @@ -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 }; } diff --git a/packages/signing/index.ts b/packages/signing/index.ts index 45af28441..d5f6c2f9b 100644 --- a/packages/signing/index.ts +++ b/packages/signing/index.ts @@ -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) diff --git a/packages/signing/transports/local.ts b/packages/signing/transports/local.ts index a6e1698a2..1635e613b 100644 --- a/packages/signing/transports/local.ts +++ b/packages/signing/transports/local.ts @@ -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, }); };