@mostajs/pdf-sign
v0.8.0
Published
Signature numérique de PDF pour @mostajs — moteur HYBRIDE : cœur Node (PKCS#7 via @signpdf) + bascule pyhanko (PAdES + permissions AES-256) si disponible. Config .env, CLI, API. Réutilise le pipeline éprouvé pyhanko+pikepdf.
Maintainers
Readme
@mostajs/pdf-sign
Auteur : Dr Hamid MADANI [email protected] Licence : AGPL-3.0-or-later
Signature numérique de PDF pour l'écosystème @mostajs/*. Moteur hybride :
- cœur Node — signature PKCS#7 détachée via
@signpdf, cachet invisible, aucune dépendance Python ; - bascule pyhanko — signature PAdES visible + permissions restrictives (chiffrement AES-256, R=6) quand un venv Python est disponible.
Configuration 100 % .env (via @mostajs/config), API + CLI. Portage générique et paramétré du pipeline pyhanko + pikepdf éprouvé.
Pourquoi — valeur probante (SIH & documents officiels)
La signature électronique de PDF apporte la preuve médico-légale et la valeur probante exigées par l'interopérabilité et la sécurité d'un Système d'Information Hospitalier :
| Domaine | Documents | |---|---| | Clinique | CRH, CRO, comptes rendus d'imagerie / biologie signés par le praticien | | Administratif | certificats de séjour, ordonnances, dossiers de sortie | | Financier / CNAS-CASNOS | factures, bordereaux de remboursement, prises en charge |
Le module reste générique : il ne connaît aucun métier. La sémantique (quel document, quel signataire) est portée par l'appelant.
Installation
Exploitation et maintenance — exigences (Python ≥ 3.10), rôle de chaque bibliothèque Python, installation en production hors de
node_modules/, sens de chaque message d'erreur, montée de version : docs/INSTALLATION-ET-EXPLOITATION.md.
pnpm add @mostajs/pdf-sign
# Moteur pyhanko (optionnel : PAdES + permissions) :
bash node_modules/@mostajs/pdf-sign/python/setup-venv.sh
export PDF_SIGN_PYTHON="$(pwd)/node_modules/@mostajs/pdf-sign/python/.venv/bin/python"API
import { signPdf, verifyPdf, detectEngines } from "@mostajs/pdf-sign";
// Signature (auto : pyhanko si dispo, sinon Node)
await signPdf({
input: "crh.pdf",
output: "crh.signe.pdf",
p12: "praticien.p12",
passphrase: process.env.P12_PASS,
meta: { reason: "Compte rendu d'hospitalisation", name: "Dr H. Madani", location: "Alger, DZ" },
});
// Signature restreinte (PAdES + chiffrement AES-256 — exige pyhanko)
await signPdf({
input: "facture-cnas.pdf",
output: "facture-cnas.signe.pdf",
p12: "etablissement.p12",
passphrase: process.env.P12_PASS,
permissions: { extract: false, modifyOther: false, printHighres: true },
field: { page: 1, box: [320, 40, 575, 140], stampImage: "logo-chu.png" }, // cachet visible image + texte
engine: "pyhanko",
});
// Vérification
const r = verifyPdf({ input: "crh.signe.pdf", trustPem: "ac-racine.pem" });
console.log(r.signed, r.signatures);
// État des moteurs
console.log(detectEngines());CLI
mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret \
--reason "Ordonnance" --name "Dr H. Madani" --location "Alger, DZ"
mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret --restrict --engine pyhanko
mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret --open-password ouvre-moi
mosta-pdf-sign verify out.pdf --trust ac-racine.pem [--password ouvre-moi]
mosta-pdf-sign enginesPermissions, mot de passe — ce qui protège vraiment
Deux choses très différentes, et les confondre coûte cher :
| | permissions | openPassword |
|---|---|---|
| Effet | le document demande qu'on n'imprime ni ne copie | le document ne s'ouvre pas sans le mot de passe |
| Qui l'applique | le lecteur, s'il veut bien | le chiffrement — personne ne le contourne |
| Le document s'ouvre | librement | sur mot de passe seulement |
| Moteur | pyhanko | pyhanko |
Les permissions seules sont déclaratives : le mot de passe utilisateur reste vide, le document
s'ouvre, et Acrobat comme tous les lecteurs sérieux respectent les interdits — mais un outil qui
choisit de les ignorer le peut. C'est le format PDF, pas une faiblesse de ce module. Pour un verrou
que rien ne contourne, il faut openPassword.
// Ni impression ni copie, ET fermé à clé.
await signPdf({
input: "crh.pdf", output: "crh.signe.pdf", p12: "cert.p12", passphrase: "•••",
permissions: { extract: false, printLowres: false, printHighres: false },
openPassword: "ouvre-moi",
engine: "pyhanko",
});
// Un mot de passe SEUL : on ferme l'ouverture, on ne restreint rien d'autre.
await signPdf({ input: "note.pdf", p12: "cert.p12", passphrase: "•••", openPassword: "•••" });Vérifier un PDF reçu (0.7.0)
Pour une page publique où l'on dépose un PDF : verifyPdfBytes(octets) rend { file: { bytes, sha256 }, signed,
signatures, dss } sans laisser de fichier derrière ; signatureVerdict(s) dit altered, valid, trusted ou
invalid ; fullVerificationAvailable() dit si pyhanko est là (sans lui, ne rien afficher comme vérifié).
⚠ Un document verrouillé se vérifie avec sa clé :
verifyPdf({ input, openPassword }). Sans elle, ses signatures sont illisibles et la réponse serait « non signé » — sur un document parfaitement signé.
Signature séparée — XAdES, comme au Journal officiel (0.8.0)
Le Journal officiel de l'UE publie ses PDF sans signature : elle est dans un XML à côté, dont le manifeste nomme chaque document par son empreinte. On la vérifie, et l'on en produit de semblables pour un dossier de fichiers :
import { verifyDetached, detachedVerdict, signDetached } from "@mostajs/pdf-sign";
const r = verifyDetached({ xml, files: [{ name: "reglement.pdf", bytes: pdf }] });
detachedVerdict(r); // "valid" | "altered" | "invalid"
r.documents[0].covered; // ce PDF est-il l'un des documents signés ? (r.documents[0].coveredAs : sous quel nom)
r.signerCertificate; // sujet, émetteur, validité, qualified / qscd / qcType (ce que le certificat DÉCLARE)
r.timestamps; // jetons RFC 3161 : genTime, horodateur, imprintOk, intact, valid
const { xml: sceau } = await signDetached({
files: [{ name: "certificat.pdf", bytes: a }, { name: "livrable.json", bytes: b }],
p12: "cachet.p12", passphrase: "•••", timestampUrl: "https://…", // XAdES-B-T ; sans horodateur : B-B
});⚠ « qualifié » se lit dans le certificat (QCStatements) : les listes de confiance de l'UE ne sont pas consultées.
Horodatage — et joindre un horodateur QUALIFIÉ (0.4.0)
timestampUrl fait passer la signature de PAdES-B à PAdES-T : la date n'est plus déclarée
par le signataire, elle est attestée par un tiers. Si l'horodateur ne répond pas, la signature
échoue — jamais de repli sur une signature sans date.
Un horodateur qualifié (eIDAS art. 42, celui dont la date renverse la charge de la preuve) est un service payant : il authentifie son client. Depuis 0.4.0 :
await signPdf({
input: "in.pdf", output: "out.pdf", p12: "cachet.p12", passphraseFile: "cachet.pass",
timestampUrl: "https://api.evidency.io/v3/projects/<ID>/timestamp",
timestampHeaders: { "X-API-KEY": process.env.CLE }, // ou timestampAuth: { user, password }
timestampTimeout: 10,
});Ou, sans toucher au code, par le .env : PDF_SIGN_TSA, PDF_SIGN_TSA_API_KEY (voir plus bas).
Quatre garde-fous, chacun couvert par un essai (test-scripts/test-horodatage-authentifie.ts) :
| | ce qui est refusé | pourquoi |
|---|---|---|
| 1 | le moteur Node avec un timestampUrl | il ne sait pas horodater ; jusqu'en 0.3.2 il signait sans date, sans rien dire |
| 2 | des identifiants vers une adresse http:// non locale | une clé envoyée en clair est une clé publiée |
| 3 | des identifiants sans adresse | une configuration à moitié posée ne doit pas passer pour complète |
| 4 | la clé du .env vers un autre horodateur que PDF_SIGN_TSA | joindre sa clé à une requête vers un tiers, c'est la lui donner |
Un refus d'identifiants est nommé : « identifiants refusés par l'horodateur (HTTP 401) ». pyhanko, seul, l'aurait présenté comme une « réponse malformée ».
Les identifiants partent au moteur par le fichier de configuration temporaire (dossier en 0700,
effacé après usage), jamais par la ligne de commande — et le CLI ne les accepte qu'en .env.
Horodatage d'un document déjà signé (0.6.0)
timestampPdf ajoute un jeton RFC 3161 (PAdES « DocTimeStamp ») à un PDF, sans toucher aux signatures qu'il
porte déjà : mise à jour incrémentale, octets d'origine gardés. Usage type : poser un horodatage qualifié sur un
document scellé par un cachet de service.
import { timestampPdf } from "@mostajs/pdf-sign";
const { bytes } = await timestampPdf({
input: pdfScelle, // octets ou chemin
timestampUrl: "https://api.evidency.io/v3/projects/<ID>/timestamp",
timestampHeaders: { "X-API-KEY": process.env.CLE },
});Mêmes garde-fous que la signature (tableau ci-dessus), aucun repli : si l'horodateur ne répond pas ou refuse la clé,
rien n'est rendu. Exige pyhanko. Essais : test-scripts/test-horodatage-document.ts.
Configuration (.env)
| Clé | Rôle |
|---|---|
| PDF_SIGN_P12 / PDF_SIGN_PASSPHRASE / PDF_SIGN_PASSPHRASE_FILE | certificat PKCS#12 et secret |
| PDF_SIGN_ENGINE | auto (défaut) · node · pyhanko |
| PDF_SIGN_PYTHON | binaire Python du venv pyhanko |
| PDF_SIGN_REASON / _LOCATION / _NAME / _CONTACT | métadonnées du cachet |
| PDF_SIGN_FIELD_PAGE / PDF_SIGN_FIELD_BOX | page (0-based) et boîte x1,y1,x2,y2 (points, origine bas-gauche) |
| PDF_SIGN_STAMP_IMAGE | image de fond du cachet visible (logo/tampon derrière le texte ; moteur pyhanko) |
| PDF_SIGN_RESTRICT | true → permissions restrictives (pyhanko) |
| PDF_SIGN_TRUST_PEM | certificat de confiance pour la vérification |
| PDF_SIGN_TSA | adresse de l'horodateur de la configuration — sert à apparier les identifiants ci-dessous (0.4.0) |
| PDF_SIGN_TSA_API_KEY / PDF_SIGN_TSA_API_KEY_HEADER | clé d'API de l'horodateur, et le nom de son en-tête (défaut X-API-KEY) (0.4.0) |
| PDF_SIGN_TSA_USER / PDF_SIGN_TSA_PASSWORD | identifiants HTTP Basic de l'horodateur (0.4.0) |
| PDF_SIGN_TSA_TIMEOUT | délai d'attente de l'horodateur, en secondes (défaut 10) (0.4.0) |
Moteurs
| Moteur | PKCS#7 | PAdES visible | Permissions AES-256 | Dépendance |
|---|:---:|:---:|:---:|---|
| node | ✅ | ❌ | ❌ | aucune (JS pur) |
| pyhanko | ✅ | ✅ | ✅ | venv pyhanko>=0.25, pikepdf>=8 |
Le cœur Node lève une erreur si des permissions sont demandées : le PAdES et le chiffrement exigent pyhanko.
Voir aussi
@mostajs/pdf-sign-ui— couche de présentation (formulaire de signature, vérification, headless + composants).
