
Research
/Security News
OpenAPI React Query Codegen Compromised in Mini Shai-Hulud npm Supply Chain Attack
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
@e-sig/core
Advanced tools
Self-contained PKCS#7 PDF signing — no SaaS, no metering, no per-doc fees. Render→sign→verify engine + pluggable persistence interfaces + end-to-end signDocument() orchestrator.
@e-sig/core — Portable In-Platform E-SignatureSelf-contained PKCS#7 PDF signing — no SaaS, no metering, no per-doc fees. Battle-tested in production at opendelphi.org.
This directory is the portable core of the Opendelphi e-signature pipeline. Drop it (plus a tiny adapter you write) into any TypeScript / Node.js project to add real cryptographic signing of PDFs.
Given an HTML document and a person who wants to sign it, this library:
puppeteer-core; scripting disabled by default).padesStrict: true for strict PAdES B-B (also drops the PAdES-forbidden signing-time attribute).verifyPdfSignature() checks this cryptographically (recomputes the digest over the signed ByteRange and RSA-verifies the signature).Trust vs. validity. The signature is cryptographically valid, but the cert is self-issued — stock Adobe Reader shows "validity unknown" until the cert is trusted (org trust-store import, or plug in an AATL/CA signer). This verifies the signature math and integrity, not third-party trust. See the compliance notes below.
That's the whole thing. It's ~600 lines of TypeScript with zero runtime dependencies on Supabase, Next.js, or any SaaS.
By design — these are wrapper concerns:
CertStore adapter interface.AuditLogStore adapter interface.The library gives you crypto + rendering. You bring persistence + UI + auth.
| File | Purpose | Project-agnostic? |
|---|---|---|
pem-signer.ts | Custom @signpdf Signer driven by raw PEM key+cert (bypasses node-forge's broken P12 round-trip — see Background below) | ✅ |
cert-issuer.ts | Generate self-signed RSA-2048 X.509; AES-256-GCM-wrap private keys for at-rest storage | ✅ |
render-pdf.ts | HTML → PDF via puppeteer-core; auto-detects Lambda vs local Chrome | ✅ |
sign-pdf.ts | Combine placeholder injection + PKCS#7 sign (ETSI.CAdES) | ✅ |
verify-pdf.ts | Structural verifier (parses /ByteRange + PKCS#7 blob, returns diagnostics) | ✅ |
pq-seal.ts | Hybrid Ed25519 + ML-DSA-65 (FIPS 204) post-quantum seal — keygen, sign, verify | ✅ |
pq-embed.ts | Embed/extract the seal in a PDF as an append-only incremental update | ✅ |
pq-verify.ts | verifyPqSeal + verifyDocument (classical and post-quantum verdict) | ✅ |
pq-lifecycle.ts | ensureActivePqKeys / rotatePqKeys over a bring-your-own PqKeyStore | ✅ |
signature-block.ts | HTML helper to render N signature blocks for multi-party flows | ✅ |
types.ts | Shared TS types (Signer, SigningCertPem, …) | ✅ |
index.ts | Public re-export barrel | ✅ |
npm install \
@signpdf/signpdf \
@signpdf/utils \
@signpdf/placeholder-plain \
node-forge \
puppeteer-core \
@sparticuz/chromium # only on Lambda; skip for local-only
Plus @types/node-forge if you're using TypeScript.
If you're on Next.js, you MUST externalize the binary-adjacent packages in next.config.ts:
const nextConfig = {
serverExternalPackages: [
"@sparticuz/chromium",
"puppeteer-core",
"node-forge",
"@signpdf/signpdf",
"@signpdf/utils",
"@signpdf/placeholder-plain",
],
// The chromium binary tarball is a static asset; tell file-tracing to
// include it in your e-sig route's bundle.
outputFileTracingIncludes: {
"/api/your-esig-route": [
"./node_modules/@sparticuz/chromium/bin/**",
],
},
};
import {
generateSelfSignedCert,
renderHtmlToPdf,
signPdf,
verifyPdfStructure,
} from "@e-sig/core";
// 1. Issue a one-off cert (in real life, persist + reuse).
const cert = generateSelfSignedCert({ subjectName: "Acme Corp" });
// 2. Render HTML → unsigned PDF.
const unsigned = await renderHtmlToPdf({
html: `<h1>Service Agreement</h1><p>Signed by Jane Doe at ${new Date().toISOString()}.</p>`,
});
// 3. Sign it.
const { signedPdf } = await signPdf({
pdf: unsigned,
keyPem: cert.keyPem,
certPem: cert.certPem,
reason: "Service Agreement acceptance",
location: "https://acme.example",
contactInfo: "jane@example.com",
name: "Jane Doe",
});
// 4. Verify the result cryptographically (also exported as verifyPdfSignature).
// ok === true only when structure + document digest + RSA signature all pass;
// a single flipped byte under the signature makes ok=false / digestValid=false.
const verify = verifyPdfStructure(signedPdf);
console.log(verify.ok, verify.digestValid, verify.signatureValid, verify.signerCommonName);
// → true, true, true, "E-sig (Acme Corp)"
// 5. Persist + serve. Up to you.
require("fs").writeFileSync("./signed.pdf", signedPdf);
That's it. Open signed.pdf in Preview — signature panel shows valid (self-signed).
Pass a tsa transport to signPdf to embed an RFC 3161 TimeStampToken,
upgrading the signature from CAdES-B to CAdES-T. The token is added as the
id-aa-timeStampToken unsigned attribute (OID 1.2.840.113549.1.9.16.2.14)
computed over the SignerInfo signatureValue (RFC 3161 §2.4.1).
The package performs no network egress — you inject the POST so the package stays dependency-free. The TSA only ever receives a SHA-256 hash, never the document or any PHI:
import type { TsaTransport } from "@e-sig/core";
const tsa: TsaTransport = {
required: false, // false = degrade to CAdES-B on TSA failure; true = throw
fetch: async (reqDerBytes) => {
const res = await fetch("http://timestamp.digicert.com", {
method: "POST",
headers: { "Content-Type": "application/timestamp-query" },
body: reqDerBytes,
});
return new Uint8Array(await res.arrayBuffer());
},
};
const { signedPdf, timestamped, tsaError } = await signPdf({
pdf, keyPem, certPem,
reason: "DUA acceptance", location: "opendelphi.org",
contactInfo: "legal@acme.org", name: "Acme Research Institute",
tsa,
});
const v = verifyPdfStructure(signedPdf);
// v.timestamped, v.timestampTime (ISO), v.tsaCommonName
// v.ok is false if the §2.4.2 binding check fails (imprint != sha256(sigValue))
Notes:
tsa is supplied and signatureLength is omitted, the
/Contents placeholder budget defaults to 30720 (vs 8192 without a TSA)
to fit the TimeStampToken plus the TSA certificate chain. An overflow is
rejected, never silently truncated.required: false (default), a TSA error produces a
valid CAdES-B signature and sets tsaError; with required: true the error
is rethrown.messageImprint must equal sha256(SignerInfo.signature), else ok:false.See CONSUMING.md for the full consumer guide.
Harvest-now-decrypt-later is a real threat to long-lived signed documents: a
signature that is only RSA/ECDSA today is forgeable the day a cryptographically
relevant quantum computer exists. @e-sig/core can add a hybrid post-quantum
seal — Ed25519 + ML-DSA-65 (the FIPS 204 module-lattice signature, née
Dilithium) — to any signed PDF, without giving up compatibility.
How it stays compatible: the seal does not replace the PKCS#7/PAdES RSA signature (no mainstream PDF reader validates ML-DSA in PAdES yet). The seal is embedded first, as an append-only incremental update, and the classical RSA signature is applied on top — so it cryptographically covers the seal. Adobe Acrobat still shows a valid signature; your verifier additionally confirms the quantum-resistant layer. This is the hybrid migration path NIST/CNSA 2.0 recommend over a hard cutover.
import {
generateSelfSignedCert,
generatePqKeyBundle,
loadPqSigningKeys,
signPdf,
verifyDocument,
} from "@e-sig/core";
const cert = generateSelfSignedCert({ subjectName: "Acme Corp" });
// One hybrid key bundle per signer/tenant (persist wrapPqKeyBundle(...) at rest;
// or use ensureActivePqKeys with a PqKeyStore — see below).
const pqKeys = loadPqSigningKeys(generatePqKeyBundle().bundle);
const { signedPdf } = await signPdf({
pdf: unsigned,
keyPem: cert.keyPem,
certPem: cert.certPem,
reason: "Service Agreement acceptance",
location: "",
contactInfo: "jane@example.com",
name: "Jane Doe",
pqSeal: { keys: pqKeys }, // ← adds the post-quantum seal
});
// One call, two verdicts:
const v = verifyDocument(signedPdf);
console.log("classical (PAdES/RSA):", v.classical.ok); // → true (Acrobat-grade)
console.log("post-quantum (ML-DSA-65):", v.postQuantum.ok); // → true
console.log("PQ signer fingerprint:", v.postQuantum.mldsa65Fpr);
// v.ok === true only when BOTH layers verify AND the seal lies inside the
// RSA-signed region. Tampering with one byte of the document fails BOTH.
Managed keys. ensureActivePqKeys({ store, tenantId, passphrase }) mints +
wraps a bundle on first use and reuses it thereafter (implement the small
PqKeyStore interface against your DB, same pattern as CertStore);
rotatePqKeys(...) rolls to a fresh key while old documents keep verifying
against the public key embedded in each seal. signDocument({ pq: { keys } })
threads it through the end-to-end orchestrator and records the PQ key id +
ML-DSA fingerprint in the audit row.
Trust model (v1). The seal carries the raw ML-DSA-65 public key + its SHA-256 fingerprint; a relying party pins/publishes the expected fingerprint (TOFU). Assert it in-band:
verifyDocument(signedPdf, {
expectedMldsa65Fpr: "<the signer's published fingerprint>", // fails if it differs
requirePq: true, // fails if no seal present (no silent downgrade)
});
X.509 identity (RFC 9881). For an enterprise-shaped identity, issue a
self-signed ML-DSA-65 X.509 certificate (OID 2.16.840.1.101.3.4.3.18) —
its SubjectPublicKeyInfo and signature are ML-DSA-65, so it parses and verifies
in OpenSSL 3.5+ (validated against 3.6). Bind it to a seal at verify time:
const cert = issueMlDsaCertificate({ keys, subjectName: "Acme Inc" }); // publish cert.certPem
verifyDocument(signedPdf, { signerCert: cert.certPem }); // fails unless the cert is valid AND owns the seal's key
verifyMlDsaCertificate() checks the self-signature, algorithm, and validity
window; certMatchesPqSeal() ties it to a seal by public-key fingerprint. Both
seal signatures are required — if either Ed25519 or ML-DSA-65 fails (or the
seal's fingerprint/keyId don't match its keys), the seal fails.
For real usage you need to:
The library provides adapter interfaces (CertStore, AuditLogStore — see ../adapters/types.ts). Implement them against your DB.
A reference Supabase implementation lives at ../adapters/supabase.ts (~150 lines). It works against any schema with these two tables — copy the migration from supabase/migrations/00106_esig_self_contained.sql for the canonical shape, or write your own.
interface CertStore {
findActive(tenantId: string): Promise<StoredCert | null>;
insert(input: { tenantId; generated; keyPemEncrypted; rotatedFromId? }): Promise<StoredCert>;
deactivate(id: string): Promise<void>;
findExpiring(withinDays: number): Promise<StoredCert[]>;
}
interface AuditLogStore {
insert(entry: AuditLogEntry): Promise<AuditLogRow>;
}
Then use the convenience helper ensureActiveCert from ../adapters/supabase.ts as a template:
const result = await ensureActiveCert({
store: new YourCertStore(...),
tenantId: "acme-corp",
subjectName: "Acme Corp",
passphrase: process.env.ESIG_CERT_PASSPHRASE!,
});
// result.certPem + result.keyPem ready to feed into signPdf()
The Opendelphi production wire-up uses this library for HIPAA-bound Data Use Agreements and is mapped against:
See .planning/phases/19-esig-primitives-spike/19-04-ESIGN-GAPS.md for the full mapping.
Not legal advice. Talk to your lawyer about whether this satisfies the regulatory framework for your specific use case.
Per-document metering (~$0.20/sig) made the unit economics painful at scale. And every signed PDF flowed through a third-party processor — making HIPAA + GDPR compliance harder than it had to be.
This library is what you reach for when "no SaaS, no metering, no fees" is a hard requirement.
pdf-lib?pdf-lib is the most popular Node PDF library, but it hasn't shipped a release since 2021. Documenso uses @libpdf/core instead — same conclusion here. (Neither is actually used by this core — we drive puppeteer for rendering and @signpdf + node-forge for signing, both actively maintained.)
We tried. node-forge.pkcs12.toPkcs12Asn1 produces P12 bundles whose MAC neither node-forge nor openssl can verify. Looks like a long-standing BMPString-password-derivation bug. We bypass it entirely — the PemSigner takes raw PEM and drives forge.pkcs7 directly.
node-forge.pkcs12.toPkcs12Asn1 — see above.forge.pki.certificateFromPem mis-counts bytes for non-ASCII (em-dash in OU breaks PEM round-trip with "Too few bytes to parse DER").@signpdf/signpdf v3 ESM default import is opaque — use the named export: import { SignPdf } from "@signpdf/signpdf" and new SignPdf().sign(...).@sparticuz/chromium is Lambda-only — locally, use system Chrome via the executablePath override.forge.pem.decode extracts blocks correctly, but re-encoding the cert block from a multi-block buffer doesn't survive certificateFromPem round-trip. Store them as separate files / DB columns.End-to-end on Vercel Lambda (cold start), tested against opendelphi.org in production:
Subsequent signs reuse the cached cert → ~1–1.5 s warm.
Same as the parent project. The core/ directory is intentionally self-contained so it can be vendored under your own license.
.planning/phases/19-esig-primitives-spike/19-02-PATTERNS-OBSERVED.md for the lessons borrowed).FAQs
Self-contained PKCS#7 PDF signing — no SaaS, no metering, no per-doc fees. Render→sign→verify engine + pluggable persistence interfaces + end-to-end signDocument() orchestrator.
The npm package @e-sig/core receives a total of 13 weekly downloads. As such, @e-sig/core popularity was classified as not popular.
We found that @e-sig/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.