@mostajs/pdf-sign 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog — @mostajs/pdf-sign
2
2
 
3
+ ## 0.4.1 — 29/09/2026
4
+
5
+ **pyhanko 0.37 taisait le diagnostic « identifiants refusés » — et rien ne fixait les versions.**
6
+
7
+ ### Corrigé
8
+
9
+ pyhanko 0.37 a remplacé son client HTTP (`requests` → `aiohttp`). Son API n'a pas changé, mais le
10
+ code d'erreur HTTP a changé de place : il est porté par l'exception (`.status`) et non plus par la
11
+ réponse passée en argument (`.status_code`). Sous 0.37, la 0.4.0 présentait donc un 401 comme
12
+ « Error while contacting timestamp service » — ce qui envoie chercher un problème réseau. Les deux
13
+ emplacements sont désormais lus. La suite complète passe sous pyhanko **0.35.1 et 0.37.0**.
14
+
15
+ ### Corrigé — la VÉRIFICATION, qu'aucun essai ne gardait
16
+
17
+ Relevé en vérifiant que l'adaptation à 0.37 n'avait rien cassé d'autre : `verifyPdf` n'était
18
+ essayé qu'avec le moteur Node. Le chemin pyhanko cachait deux défauts, **identiques sous 0.35 et
19
+ 0.37** — donc antérieurs à la montée de version :
20
+
21
+ 1. **Repli silencieux.** Tout échec de pyhanko (document verrouillé, plantage, API changée)
22
+ retombait sur la détection Node, qui rend `intact` et `valid` **indéfinis** — et un affichage
23
+ qui lit « pas false » y voit « valide ». Désormais : si pyhanko est présent, son échec est une
24
+ **erreur** qui nomme la cause ; `engine:"pyhanko"` sans pyhanko est une erreur. Le repli Node ne
25
+ vaut plus que si pyhanko **manque**.
26
+ 2. **Mauvais mot de passe pris pour le bon.** `decrypt()` ne lève pas : il rend un statut `FAILED`.
27
+ La boucle attendait une exception, et tenait le document pour ouvert au premier essai. Le
28
+ statut est désormais lu ; un document verrouillé vérifié sans son mot de passe donne
29
+ « document verrouillé : son mot de passe d'ouverture est requis ».
30
+
31
+ `test-scripts/test-verification-pyhanko.ts` — 7 essais (T-VER-1 à 7) : intact / altéré /
32
+ horodaté / verrouillé sans et avec mot de passe / fichier illisible / cachet avec image. Suite
33
+ complète : **30 essais, 0 sauté, sous pyhanko 0.35.1 ET 0.37.0**.
34
+
35
+ ⚠️ `valid` seul ne suffit pas : sur un document altéré, pyhanko rend `valid: true` (la signature
36
+ est mathématiquement bonne) et `intact: false` (elle ne couvre plus ce document). Lire les deux.
37
+
38
+ ### Relecture de l'adaptation 0.37
39
+
40
+ La lecture de `.status` est restreinte aux exceptions d'`aiohttp` : ailleurs, un attribut du même
41
+ nom pourrait ne pas être un code HTTP. Le diagnostic ne peut, par construction, que **changer le
42
+ message** d'un échec — jamais transformer un échec en succès. Contre-épreuve : sans cette lecture,
43
+ T-TSA-4 et T-TSA-8 échouent sous 0.37.
44
+
45
+ ### Ajouté — les versions Python sont FIXÉES
46
+
47
+ - `python/requirements.lock.txt` : l'ensemble exact vérifié (24 paquets). **Installé par défaut**.
48
+ - `python/requirements.txt` devient une **plage bornée** : `pyhanko>=0.35.1,<0.38`,
49
+ `pikepdf>=10.9,<11` (auparavant `>=0.25` / `>=8`, sans borne haute : deux installations à un
50
+ mois d'écart n'avaient pas les mêmes versions — c'est ainsi que 0.37 est passé inaperçu).
51
+ - `setup-venv.sh` : accepte un répertoire (pour poser le venv **hors de node_modules/**), vérifie
52
+ Python ≥ 3.10 et `python3-venv` **avant** d'installer, `--plage` pour une montée de version.
53
+ - `docs/INSTALLATION-ET-EXPLOITATION.md` (livré dans le paquet) : exigences, rôle de chaque
54
+ bibliothèque, installation en production, vérification, montée de version, sens de chaque
55
+ message d'erreur, secrets.
56
+
3
57
  ## 0.4.0 — 28/09/2026
4
58
 
5
59
  **Joindre un horodateur qui authentifie — c'est-à-dire un horodateur QUALIFIÉ.**
package/README.md CHANGED
@@ -24,6 +24,10 @@ Le module reste **générique** : il ne connaît aucun métier. La sémantique (
24
24
 
25
25
  ## Installation
26
26
 
27
+ > **Exploitation et maintenance** — exigences (Python ≥ 3.10), rôle de chaque bibliothèque Python,
28
+ > installation en production **hors de `node_modules/`**, sens de chaque message d'erreur, montée
29
+ > de version : **[docs/INSTALLATION-ET-EXPLOITATION.md](docs/INSTALLATION-ET-EXPLOITATION.md)**.
30
+
27
31
  ```bash
28
32
  pnpm add @mostajs/pdf-sign
29
33
  # Moteur pyhanko (optionnel : PAdES + permissions) :
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAuB3D"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI9D,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAkC3D"}
package/dist/verify.js CHANGED
@@ -16,19 +16,30 @@ export function verifyPdf(opts) {
16
16
  const avail = detectEngines(cfg.python);
17
17
  const trust = opts.trustPem ?? cfg.trustPem;
18
18
  const want = opts.engine ?? cfg.engine;
19
+ // ⚠️ AUCUN REPLI quand pyhanko EST là. Jusqu'en 0.4.0, un échec de pyhanko — document verrouillé
20
+ // sans son mot de passe, plantage, version de pyhanko qui change son API — retombait EN SILENCE
21
+ // sur la détection Node, qui ne sait dire que « une signature est présente » (intact et valid
22
+ // indéfinis). Un affichage qui lisait « pas false » y voyait « valide ». Une vérification qui
23
+ // n'a pas eu lieu doit se DIRE, pas se déguiser. Le repli Node ne vaut que si pyhanko MANQUE.
24
+ if (want === "pyhanko" && !avail.pyhanko) {
25
+ throw new Error("Vérification complète demandée, mais le moteur pyhanko est indisponible — lancer python/setup-venv.sh (ou PDF_SIGN_PYTHON).");
26
+ }
19
27
  if (avail.pyhanko && want !== "node") {
28
+ const args = [SIGN_PY, "verify", opts.input, trust ?? ""];
29
+ if (opts.openPassword)
30
+ args.push(opts.openPassword);
31
+ let out;
20
32
  try {
21
- const args = [SIGN_PY, "verify", opts.input, trust ?? ""];
22
- if (opts.openPassword)
23
- args.push(opts.openPassword);
24
- const out = execFileSync(avail.python, args, { encoding: "utf-8" });
25
- const line = out.trim().split("\n").pop() ?? "{}";
26
- const parsed = JSON.parse(line);
27
- return { signed: !!parsed.signed, signatures: parsed.signatures ?? [], engine: "pyhanko" };
33
+ out = execFileSync(avail.python, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
28
34
  }
29
- catch {
30
- /* repli sur la détection Node */
35
+ catch (e) {
36
+ const trace = String(e?.stderr ?? "");
37
+ const net = trace.match(/ERREUR -- ([^\n]*)/);
38
+ throw new Error(net ? net[1] : `Vérification impossible (pyhanko) : ${trace.trim().split("\n").pop() || String(e?.message ?? e)}`);
31
39
  }
40
+ const line = out.trim().split("\n").pop() ?? "{}";
41
+ const parsed = JSON.parse(line);
42
+ return { signed: !!parsed.signed, signatures: parsed.signatures ?? [], engine: "pyhanko" };
32
43
  }
33
44
  // Détection Node basique : présence d'un /ByteRange + dictionnaire de signature.
34
45
  const s = readFileSync(opts.input).toString("latin1");
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,UAAU,SAAS,CAAC,IAAmB;IAC3C,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC;IAEvC,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;YAC1D,IAAI,IAAI,CAAC,YAAY;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACpD,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,MAAgB,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;YAC9E,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;YAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAChC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC7F,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;IACH,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,wCAAwC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACzG,CAAC"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,271 @@
1
+ # @mostajs/pdf-sign — installation, exploitation, maintenance
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
+ **Version du module** : 0.4.1 · **Rédigé le** : 2026-09-28
5
+ **Pour qui** : la personne qui reprend ce module — ou un produit qui l'emploie (mdseal, qatrax) —
6
+ sans avoir assisté à sa construction. Rien ici ne suppose de savoir ce qui s'est dit avant.
7
+
8
+ > **Si vous n'avez que deux minutes** : ce module signe des PDF. La signature simple se fait en
9
+ > Node ; **tout le reste** — protection, mot de passe, horodatage, vérification complète — passe
10
+ > par un programme **Python** (pyhanko), installé dans un environnement isolé (*venv*). Neuf
11
+ > pannes sur dix viennent de ce venv : absent, effacé, ou d'une version non essayée. Le §6 dit
12
+ > comment le vérifier en une commande, le §8 ce que veut dire chaque message d'erreur.
13
+
14
+ ---
15
+
16
+ ## 1. Comment le module fonctionne
17
+
18
+ ```
19
+ application (mdseal, qatrax…)
20
+ │ signPdf({ … })
21
+ ▼
22
+ @mostajs/pdf-sign (Node, TypeScript)
23
+ │
24
+ ├── moteur NODE ─── @signpdf ─────────► signature PKCS#7 simple, invisible
25
+ │ (rien d'autre : ni protection, ni date)
26
+ │
27
+ └── moteur PYHANKO
28
+ │ 1. écrit config.json dans un dossier temporaire (droits 0700)
29
+ │ 2. lance : <python du venv> python/sign_with_perms.py sign … config.json
30
+ ▼
31
+ pikepdf ── chiffrement AES-256, permissions (ni impression, ni copie)
32
+ pyhanko ── signature PAdES visible
33
+ └─ horodatage RFC 3161 ──► HTTPS ──► horodateur (DigiCert, Evidency…)
34
+ ```
35
+
36
+ **Ce qui exige le moteur pyhanko** — sans lui, ces demandes sont **refusées**, jamais ignorées :
37
+
38
+ | demande | option | pourquoi Node ne suffit pas |
39
+ |---|---|---|
40
+ | permissions (ni impression, ni copie) | `permissions` | chiffrement du PDF : pikepdf |
41
+ | mot de passe à l'ouverture | `openPassword` | idem |
42
+ | horodatage (date attestée par un tiers) | `timestampUrl` | client RFC 3161 : pyhanko |
43
+ | cachet visible avec image | `field.stampImage` | apparence PAdES : pyhanko |
44
+ | vérification complète (chaîne, intégrité) | `verifyPdf` | Node ne sait que détecter une signature |
45
+
46
+ `engine: "auto"` (défaut) prend pyhanko s'il est disponible, Node sinon. **C'est la raison pour
47
+ laquelle un venv absent ne se voit pas toujours** : une signature simple continue de marcher.
48
+
49
+ ## 2. Exigences techniques
50
+
51
+ | élément | exigé | essayé | à savoir |
52
+ |---|---|---|---|
53
+ | système | Linux x86_64 ou aarch64 | Ubuntu 23.10 | pikepdf est livré compilé (*wheel*) pour ces deux architectures ; ailleurs, il faut compiler qpdf |
54
+ | **Node.js** | ≥ 18 | 20 et plus | `fetch` et `AbortSignal.timeout` sont employés par les essais |
55
+ | **Python** | **≥ 3.10** | 3.11.6 | exigé par pyhanko **et** pikepdf. Plus ancien : échec au milieu de `pip install`, sans nommer la cause (le script d'installation le vérifie avant) |
56
+ | `python3-venv` | oui (Debian/Ubuntu) | 3.11.4 | livré à part sur Debian/Ubuntu. Absent : `python3 -m venv` échoue sur « ensurepip » → `sudo apt install python3-venv` |
57
+ | `pip` | fourni par le venv | — | mis à jour par le script |
58
+ | accès réseau sortant | HTTPS 443 vers l'horodateur | — | **seulement** si l'horodatage est employé. DigiCert écoute en HTTP 80 |
59
+ | `openssl` | non (essais seulement) | 3.0.10 | sert aux essais à fabriquer un certificat jetable |
60
+ | `qpdf` | **non** | — | embarqué dans pikepdf |
61
+ | certificat de signature | fichier PKCS#12 (`.p12`) + sa phrase secrète | — | voir §9 |
62
+
63
+ ## 3. Les bibliothèques Python, une par une
64
+
65
+ Versions : celles du **verrou** `python/requirements.lock.txt`, vérifiées par la suite d'essais.
66
+
67
+ | bibliothèque | version | licence | rôle ici | si elle manque ou change |
68
+ |---|---|---|---|---|
69
+ | **pyHanko** | 0.37.0 | MIT | signature PAdES, horodatage RFC 3161, vérification | le moteur pyhanko est déclaré indisponible |
70
+ | pyhanko-certvalidator | 0.32.1 | MIT | valide la chaîne de certificats (vérification) | tiré par pyHanko |
71
+ | **pikepdf** | 10.15.0 | MPL-2.0 | chiffre le PDF, pose les permissions ; contient qpdf | idem |
72
+ | cryptography | 50.0.1 | Apache-2.0 / BSD | ouvre le `.p12`, calcule la signature | « Phrase secrète invalide » si la phrase est fausse |
73
+ | asn1crypto | 1.5.1 | MIT | structures CMS et TSP (le jeton d'horodatage) | tiré par pyHanko |
74
+ | **aiohttp** | 3.14.3 | Apache-2.0 | **client HTTP de l'horodatage** depuis pyHanko 0.37 | voir le piège du §8, « pyhanko 0.37 » |
75
+ | certifi | 2026.7.22 | MPL-2.0 | autorités racines pour joindre un horodateur **en HTTPS** | un HTTPS vers l'horodateur échoue en « Error while contacting » |
76
+ | Pillow | 12.3.0 | MIT-CMU | image de fond du cachet visible | tiré par pikepdf |
77
+ | lxml | 6.1.3 | BSD | lecture XML (structures PDF annexes) | tiré par pyHanko |
78
+ | tzlocal | 5.4.4 | MIT | fuseau local de l'heure affichée dans le cachet | produit l'avertissement bénin « /etc/timezone is deprecated » |
79
+ | oscrypto, uritools, yarl, multidict, frozenlist, attrs, … | cf. verrou | MIT / Apache | dépendances de second rang | ne s'installent jamais seules |
80
+
81
+ **Côté Node**, pour mémoire : `@signpdf/*` (moteur Node), `pdf-lib`, `@mostajs/config` (lecture du
82
+ `.env`).
83
+
84
+ ### Pourquoi deux fichiers de dépendances
85
+
86
+ | fichier | contenu | qui l'emploie |
87
+ |---|---|---|
88
+ | `python/requirements.lock.txt` | **l'ensemble exact** essayé (24 paquets, `==`) | `setup-venv.sh`, **par défaut** — donc la production |
89
+ | `python/requirements.txt` | la **plage admise** : `pyhanko>=0.35.1,<0.38`, `pikepdf>=10.9,<11` | `setup-venv.sh --plage`, pour une montée de version (§7) |
90
+
91
+ Sans verrou, deux installations à un mois d'écart n'ont pas les mêmes versions. **C'est arrivé** :
92
+ le poste de développement portait pyhanko 0.35.1 quand la dernière version publiée était 0.37.0,
93
+ qui avait changé de client HTTP (§8).
94
+
95
+ ## 4. Installer
96
+
97
+ ### 4.1 Poste de développement
98
+
99
+ ```bash
100
+ cd mosta-pdf-sign
101
+ npm install
102
+ bash python/setup-venv.sh # crée python/.venv depuis le VERROU
103
+ npx mosta-pdf-sign engines # doit afficher "pyhanko": true
104
+ ```
105
+
106
+ Le module trouve seul `python/.venv` : aucune variable à poser.
107
+
108
+ ### 4.2 Production — ⚠️ le venv HORS de `node_modules/`
109
+
110
+ Dans une application, le module vit sous `node_modules/@mostajs/pdf-sign/`. **Un venv créé là est
111
+ effacé par le `npm install` du déploiement suivant** — et rien ne le signale : les signatures
112
+ simples continuent, seules les demandes de protection ou d'horodatage se mettent à échouer, à la
113
+ première demande d'un client.
114
+
115
+ ```bash
116
+ # depuis l'application, après npm install :
117
+ bash node_modules/@mostajs/pdf-sign/python/setup-venv.sh /home/hmd/prod/.venv-pdfsign
118
+ # puis, dans le .env de l'application :
119
+ PDF_SIGN_PYTHON=/home/hmd/prod/.venv-pdfsign/bin/python
120
+ # et redémarrer en relisant l'environnement :
121
+ pm2 restart <application> --update-env
122
+ ```
123
+
124
+ **mdseal** a son propre script, `scripts/setup-pyhanko.sh`, qui fait la même chose et pose le venv
125
+ dans `/home/hmd/prod/.venv-pdfsign` (voir le README de mdseal). Un seul venv peut servir plusieurs
126
+ applications du même serveur : elles emploient la même version du module.
127
+
128
+ ## 5. Configurer
129
+
130
+ Tout se règle par le `.env` (lu par `@mostajs/config`, cascade `MOSTA_ENV`). Rien en dur.
131
+
132
+ | clé | rôle | défaut |
133
+ |---|---|---|
134
+ | `PDF_SIGN_PYTHON` | chemin **absolu** du python du venv | voir ci-dessous |
135
+ | `PDF_SIGN_ENGINE` | `auto` · `node` · `pyhanko` | `auto` |
136
+ | `PDF_SIGN_P12` | certificat de signature | — (obligatoire) |
137
+ | `PDF_SIGN_PASSPHRASE_FILE` | fichier de la phrase secrète — **préférer à** `PDF_SIGN_PASSPHRASE` | — |
138
+ | `PDF_SIGN_REASON` / `_LOCATION` / `_NAME` / `_CONTACT` | métadonnées du cachet | — |
139
+ | `PDF_SIGN_FIELD_PAGE` / `_FIELD_BOX` / `_STAMP_IMAGE` | position et image du cachet | page 0, bas-droite |
140
+ | `PDF_SIGN_RESTRICT` | `true` → permissions restrictives | `false` |
141
+ | `PDF_SIGN_TRUST_PEM` | certificat de confiance pour la vérification | — |
142
+ | `PDF_SIGN_TSA` | adresse de l'horodateur — **apparie** les identifiants suivants | — |
143
+ | `PDF_SIGN_TSA_API_KEY` / `_API_KEY_HEADER` | clé d'API de l'horodateur, nom de l'en-tête | — / `X-API-KEY` |
144
+ | `PDF_SIGN_TSA_USER` / `_PASSWORD` | HTTP Basic auprès de l'horodateur | — |
145
+ | `PDF_SIGN_TSA_TIMEOUT` | délai d'attente, secondes | 10 |
146
+
147
+ ⚠️ **Le choix du python, et son piège.** L'ordre est : `PDF_SIGN_PYTHON` **s'il existe** → sinon
148
+ `python/.venv` du module → sinon `python3` du système. Un `PDF_SIGN_PYTHON` **mal orthographié**
149
+ ne provoque donc **aucune erreur** : le module retombe sur le suivant. C'est pourquoi le §6
150
+ demande de lire le champ `python` de `engines`, pas seulement `pyhanko: true`.
151
+
152
+ ⚠️ **Les identifiants de l'horodateur ne partent que vers `PDF_SIGN_TSA`.** Si l'application
153
+ passe une autre adresse, la clé du `.env` n'est pas jointe — c'est voulu (la joindre, ce serait
154
+ la donner à un tiers). Symptôme : « identifiants refusés (HTTP 401) » alors que la clé est bonne.
155
+
156
+ ## 6. Vérifier que tout marche
157
+
158
+ ```bash
159
+ # 1. le moteur, et SURTOUT le chemin du python réellement employé
160
+ PDF_SIGN_PYTHON=/home/hmd/prod/.venv-pdfsign/bin/python npx mosta-pdf-sign engines
161
+ # → { "node": true, "pyhanko": true, "python": "/home/hmd/prod/.venv-pdfsign/bin/python" }
162
+
163
+ # 2. les versions installées
164
+ /home/hmd/prod/.venv-pdfsign/bin/pip freeze | grep -iE 'pyhanko|pikepdf|aiohttp'
165
+
166
+ # 3. la suite d'essais complète CONTRE CE venv (depuis le dépôt du module)
167
+ PDF_SIGN_PYTHON=/home/hmd/prod/.venv-pdfsign/bin/python bash test-scripts/run-tests.sh
168
+ # → ✓ ALL TEST SUITES PASSED, et AUCUNE ligne « ⊘ SAUTÉ »
169
+ ```
170
+
171
+ Une ligne `⊘ SAUTÉ` n'est **pas** un succès : elle dit qu'un essai n'a pas pu s'exécuter (pyhanko,
172
+ openssl ou réseau absent) et en donne la raison. Les essais d'horodatage joignent DigiCert : sans
173
+ réseau, trois d'entre eux sautent.
174
+
175
+ ## 7. Monter de version (pyhanko, pikepdf)
176
+
177
+ **Jamais directement en production.** Une version non essayée n'y entre pas.
178
+
179
+ ```bash
180
+ S=$(mktemp -d)
181
+ bash python/setup-venv.sh "$S/venv" --plage # dernières versions DANS la plage
182
+ PDF_SIGN_PYTHON="$S/venv/bin/python" bash test-scripts/run-tests.sh
183
+ ```
184
+
185
+ - Tout passe, aucun saut → régénérer le verrou (`"$S/venv/bin/pip" freeze`, en gardant l'en-tête
186
+ de `requirements.lock.txt`), noter la date et les versions dans l'en-tête et le CHANGELOG,
187
+ publier une version du module, puis réinstaller le venv de production depuis le verrou.
188
+ - Une version **hors** de la plage (pyhanko 0.38…) → élargir la borne dans `requirements.txt`
189
+ **seulement après** que la suite passe dessus, et relire son journal des changements : 0.37 a
190
+ changé de client HTTP sans le dire dans son API.
191
+
192
+ ## 8. Pannes connues — ce que veut dire chaque message
193
+
194
+ | message | cause | que faire |
195
+ |---|---|---|
196
+ | `Moteur pyhanko indisponible — lancer python/setup-venv.sh` | `engine:"pyhanko"` demandé, venv absent ou cassé | §6 étape 1 ; réinstaller (§4) |
197
+ | `Horodatage demandé, mais le moteur retenu (node) ne sait pas horodater` | pas de pyhanko, et un `timestampUrl` | installer le venv. (Avant 0.4.0, le document sortait **sans date, sans erreur**.) |
198
+ | `Le cœur Node ne gère pas les permissions/PAdES` | protection demandée sans pyhanko | idem |
199
+ | protections refusées **juste après un déploiement** | venv créé dans `node_modules/`, effacé par `npm install` | §4.2 : venv hors du dépôt |
200
+ | `Phrase secrète invalide pour ce certificat PKCS#12` | mauvaise phrase, ou phrase d'un autre certificat | vérifier le couple `PDF_SIGN_P12` / `PDF_SIGN_PASSPHRASE_FILE` |
201
+ | `Certificat PKCS#12 illisible` | le fichier n'est pas un `.p12` | le régénérer ou le recopier en binaire |
202
+ | `Horodatage impossible (…) : identifiants refusés par l'horodateur (HTTP 401)` | clé absente, fausse, expirée — **ou** adresse différente de `PDF_SIGN_TSA` (§5) | vérifier la clé et l'égalité exacte des adresses |
203
+ | `… débit de l'horodateur dépassé (HTTP 429)` | trop d'appels par seconde (Evidency : 5/s) | réessayer ; étaler les signatures |
204
+ | `… Error while contacting timestamp service` | réseau, DNS, pare-feu, délai dépassé | `curl -I <adresse>` depuis le serveur ; `PDF_SIGN_TSA_TIMEOUT` |
205
+ | `Horodatage refusé : des identifiants ne partent pas en clair` | adresse `http://` avec une clé | passer l'adresse en `https://` |
206
+ | `document verrouillé : son mot de passe d'ouverture est requis pour vérifier` | `verifyPdf` sur un document à mot de passe, sans `openPassword` (ou avec un faux) | fournir le mot de passe d'ouverture |
207
+ | `Vérification impossible (pyhanko) : …` | pyhanko n'a pas pu lire le document | le fichier n'est pas un PDF signé lisible ; **avant 0.4.1, ce cas répondait « signé » par la détection Node** |
208
+ | `modify_form ne peut pas etre refuse` | `permissions.modifyForm:false` | impossible en PDF signé : retirer l'option |
209
+ | `/etc/timezone is deprecated …` | avertissement de tzlocal | **bénin**, aucune action |
210
+ | `AttributeError: … __version__` | un script lit `pyhanko.__version__`, qui n'existe pas | lire `importlib.metadata.version("pyhanko")` |
211
+ | `✗ Python >= 3.10 requis` / `ensurepip` | Python trop ancien / `python3-venv` absent | installer Python ≥ 3.10 / `sudo apt install python3-venv` |
212
+
213
+ ### Le piège « pyhanko 0.37 », en détail
214
+
215
+ pyhanko 0.37 a remplacé son client HTTP (`requests` → `aiohttp`). Son API n'a pas changé — les
216
+ signatures et l'horodatage marchent — mais **le code d'erreur HTTP a changé de place** dans
217
+ l'exception. Le module 0.4.0 lisait l'ancien emplacement : sous 0.37, un refus de clé redevenait
218
+ « Error while contacting timestamp service », qui envoie chercher un problème réseau. Corrigé en
219
+ 0.4.1 (les deux emplacements sont lus) et gardé par l'essai T-TSA-4, qui passe sous 0.35.1 **et**
220
+ 0.37.0. C'est l'exemple même de ce que le §7 prévient.
221
+
222
+ ### Lire un résultat de vérification
223
+
224
+ | champ | sens | à retenir |
225
+ |---|---|---|
226
+ | `intact` | le document n'a pas changé depuis la signature | **false = document altéré** |
227
+ | `valid` | la signature est mathématiquement correcte | reste **true** sur un document altéré : ne jamais le lire seul |
228
+ | `trusted` | le certificat est reconnu (via `trustPem`) | false pour un certificat auto-signé non fourni |
229
+ | `engine` | `pyhanko` = vérifié ; `node` = **seulement détecté**, rien n'a été validé | un résultat `node` ne dit jamais « valide » |
230
+
231
+ ## 9. Secrets
232
+
233
+ | secret | où | comment il circule |
234
+ |---|---|---|
235
+ | certificat `.p12` | hors dépôt (`/home/hmd/prod/secrets/`, `Entreprise/.secrets/`), droits 600 | chemin dans `PDF_SIGN_P12` |
236
+ | sa phrase secrète | fichier à part, droits 600 | `PDF_SIGN_PASSPHRASE_FILE` → lue par Python depuis le fichier |
237
+ | clé d'API / mot de passe de l'horodateur | `.env` de l'application, jamais commité | Node → `config.json` temporaire (dossier 0700, effacé après usage) → Python |
238
+
239
+ **Aucun secret ne passe par la ligne de commande** : elle se lit par n'importe quel utilisateur
240
+ de la machine (`ps aux`). Le CLI `mosta-pdf-sign` n'accepte d'ailleurs les identifiants de
241
+ l'horodateur que par le `.env`. Python n'imprime jamais ni la clé ni les en-têtes — seulement
242
+ « (authentifie) ».
243
+
244
+ ## 10. État de la production — À COMPLÉTER
245
+
246
+ La lecture du serveur n'a pas pu être faite en rédigeant ce document. À lancer sur `amia`, et à
247
+ reporter ici :
248
+
249
+ ```bash
250
+ ssh amia '/home/hmd/prod/.venv-pdfsign/bin/python --version;
251
+ /home/hmd/prod/.venv-pdfsign/bin/pip freeze | grep -iE "pyhanko|pikepdf|aiohttp|requests";
252
+ python3 --version; grep -E "^(PRETTY_NAME)" /etc/os-release'
253
+ ```
254
+
255
+ | élément | valeur en production | relevé le |
256
+ |---|---|---|
257
+ | Python du venv | *à relever* | |
258
+ | pyhanko / pikepdf | *à relever* | |
259
+ | système | *à relever* | |
260
+
261
+ Si les versions diffèrent du verrou : réinstaller depuis le verrou (§4.2), puis §6.
262
+
263
+ ## 11. Chronologie des leçons
264
+
265
+ | date | ce qui s'est passé | ce qui a changé |
266
+ |---|---|---|
267
+ | avant 0.4 | risque identifié dans mdseal : un venv sous `node_modules/` serait effacé par le déploiement | venv hors du dépôt, §4.2 (`mdseal/scripts/setup-pyhanko.sh`) |
268
+ | 26/09/2026 | des essais affichaient `✓` sans rien exécuter | `saute()` : un saut se voit |
269
+ | 28/09/2026 | Node signait sans date quand on lui demandait une date | refus explicite (0.4.0) |
270
+ | 28/09/2026 | pyhanko 0.37 : un 401 redevenait une « erreur de contact » | double lecture du code (0.4.1), verrou des versions |
271
+ | 29/09/2026 | la vérification pyhanko n'était gardée par aucun essai ; son échec retombait sur « signé » | erreur explicite, 7 essais T-VER (0.4.1) |
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @mostajs/pdf-sign — fiche LLM
2
2
  > Signature numérique de PDF, moteur HYBRIDE : cœur Node (PKCS#7 détaché via @signpdf, zéro Python) + bascule pyhanko (PAdES visible + permissions AES-256 + MOT DE PASSE d'ouverture) quand un venv est disponible. Config 100 % `.env`, CLI + API. Portage générique du pipeline pyhanko+pikepdf éprouvé. IMPLÉMENTÉ 0.1.0.
3
3
 
4
- - Version: 0.4.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
4
+ - Version: 0.4.1 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
5
5
  - Chemin: mostajs/mosta-pdf-sign-stack/mosta-pdf-sign · UI: ../mosta-pdf-sign-ui
6
6
 
7
7
  ## RÔLE
@@ -55,7 +55,9 @@ mosta-pdf-sign engines
55
55
 
56
56
  ## MOTEUR pyhanko (optionnel)
57
57
  bash python/setup-venv.sh → .venv local → exporter PDF_SIGN_PYTHON=…/.venv/bin/python
58
- requiert pyhanko>=0.25, pikepdf>=8.
58
+ python/requirements.lock.txt = ensemble EXACT essayé (installé par défaut) ; requirements.txt = plage
59
+ admise (pyhanko>=0.35.1,<0.38 ; pikepdf>=10.9,<11). Python >= 3.10. En PRODUCTION : venv HORS de
60
+ node_modules/ (setup-venv.sh <répertoire>). Doc : docs/INSTALLATION-ET-EXPLOITATION.md.
59
61
 
60
62
  ## PIÈGES
61
63
  - Le cœur Node lève si `permissions` OU `openPassword` est demandé : le chiffrement EXIGE pyhanko.
@@ -73,3 +75,6 @@ requiert pyhanko>=0.25, pikepdf>=8.
73
75
  - Les identifiants du .env ne partent QUE si timestampUrl === PDF_SIGN_TSA. Autre adresse ⇒ aucun
74
76
  identifiant du .env n'est joint (l'appelant fournit les siens).
75
77
  - HTTP 401/403 de l'horodateur ⇒ « identifiants refusés (HTTP 401) », 429 ⇒ « débit dépassé ».
78
+ - verifyPdf (0.4.1) : pyhanko présent ⇒ son échec est une ERREUR (plus de repli Node muet) ;
79
+ engine:'pyhanko' sans pyhanko ⇒ ERREUR. engine 'node' dans le résultat = DÉTECTÉ, pas validé.
80
+ - intact=false ⇒ document altéré ; valid reste true dans ce cas : lire intact ET valid.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/pdf-sign",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "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.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
@@ -24,6 +24,7 @@
24
24
  "python/*.sh",
25
25
  "examples/*.mjs",
26
26
  "README.md",
27
+ "docs/INSTALLATION-ET-EXPLOITATION.md",
27
28
  "llms.txt",
28
29
  "CHANGELOG.md"
29
30
  ],
@@ -0,0 +1,32 @@
1
+ # @mostajs/pdf-sign — VERROU : l'ensemble EXACT des paquets Python vérifié par la suite d'essais.
2
+ # Auteur : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
3
+ #
4
+ # Vérifié le 2026-09-28 : suite complète (23 essais, 0 sauté) sous Python 3.11.6, Ubuntu 23.10.
5
+ # C'est CE fichier que `setup-venv.sh` installe par défaut. `requirements.txt` ne donne que la
6
+ # plage admise ; ce fichier dit ce qui a été ESSAYÉ.
7
+ #
8
+ # Le régénérer = une montée de version : voir docs/INSTALLATION-ET-EXPLOITATION.md §7.
9
+ aiohappyeyeballs==2.7.1
10
+ aiohttp==3.14.3
11
+ aiosignal==1.4.0
12
+ asn1crypto==1.5.1
13
+ attrs==26.1.0
14
+ certifi==2026.7.22
15
+ cffi==2.1.1
16
+ cryptography==50.0.1
17
+ frozenlist==1.8.0
18
+ idna==3.20
19
+ lxml==6.1.3
20
+ multidict==6.9.1
21
+ oscrypto==1.3.0
22
+ packaging==26.3
23
+ pikepdf==10.15.0
24
+ pillow==12.3.0
25
+ propcache==0.5.4
26
+ pycparser==3.0
27
+ pyHanko==0.37.0
28
+ pyhanko-certvalidator==0.32.1
29
+ typing_extensions==4.16.0
30
+ tzlocal==5.4.4
31
+ uritools==6.1.3
32
+ yarl==1.25.1
@@ -1,3 +1,14 @@
1
- # @mostajs/pdf-sign — dépendances du moteur pyhanko (PAdES + permissions).
2
- pyhanko>=0.25
3
- pikepdf>=8
1
+ # @mostajs/pdf-sign — dépendances du moteur pyhanko (PAdES + permissions + horodatage).
2
+ # Auteur : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
3
+ #
4
+ # PLAGE ADMISE, pas versions exactes — l'ensemble exact vérifié est dans requirements.lock.txt,
5
+ # et c'est lui que setup-venv.sh installe.
6
+ #
7
+ # Pourquoi des bornes HAUTES : pyhanko 0.37 a changé de client HTTP (requests → aiohttp) sans
8
+ # changer son API publique ; seul l'endroit où se lit le code d'erreur HTTP a bougé, et le
9
+ # diagnostic « identifiants refusés » s'est tu. Une version non essayée n'entre pas en production.
10
+ # Élargir une borne = passer la suite d'essais sur la nouvelle version (docs/…EXPLOITATION.md §7).
11
+ #
12
+ # Pourquoi 0.35 au minimum : versions essayées 0.35.1 et 0.37.0. Python >= 3.10 exigé par les deux.
13
+ pyhanko>=0.35.1,<0.38
14
+ pikepdf>=10.9,<11
@@ -1,9 +1,40 @@
1
1
  #!/usr/bin/env bash
2
- # @mostajs/pdf-sign — crée le venv Python du moteur pyhanko (optionnel : PAdES + permissions).
2
+ # @mostajs/pdf-sign — crée le venv Python du moteur pyhanko (PAdES + permissions + horodatage).
3
3
  # @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
4
+ #
5
+ # Usage : bash python/setup-venv.sh [répertoire-du-venv] [--plage]
6
+ # répertoire défaut : python/.venv à côté de ce script
7
+ # ⚠️ EN PRODUCTION, PASSEZ UN RÉPERTOIRE HORS DE node_modules/ : un `npm install`
8
+ # efface node_modules/, donc le venv, sans qu'aucune erreur ne le signale.
9
+ # --plage installe requirements.txt (dernières versions de la plage admise) au lieu du
10
+ # verrou requirements.lock.txt. Réservé à une MONTÉE DE VERSION, suivie des essais.
11
+ #
12
+ # Documentation complète : docs/INSTALLATION-ET-EXPLOITATION.md
4
13
  set -euo pipefail
5
14
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
- python3 -m venv "$HERE/.venv"
7
- "$HERE/.venv/bin/pip" install --upgrade pip >/dev/null
8
- "$HERE/.venv/bin/pip" install -r "$HERE/requirements.txt"
9
- echo "OK — venv pyhanko prêt : $HERE/.venv (exporter PDF_SIGN_PYTHON=$HERE/.venv/bin/python)"
15
+ VENV="$HERE/.venv"
16
+ FICHIER="$HERE/requirements.lock.txt"
17
+ for a in "$@"; do
18
+ case "$a" in
19
+ --plage) FICHIER="$HERE/requirements.txt" ;;
20
+ *) VENV="$a" ;;
21
+ esac
22
+ done
23
+
24
+ # Python >= 3.10 : exigé par pyhanko ET pikepdf. Un Python plus ancien échouerait au milieu de
25
+ # `pip install` avec un message qui ne nomme pas la cause.
26
+ python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' || {
27
+ echo "✗ Python >= 3.10 requis — trouvé : $(python3 --version 2>&1)" >&2; exit 1; }
28
+ # Debian/Ubuntu livrent `venv` à part : sans lui, `python3 -m venv` échoue sur « ensurepip ».
29
+ python3 -c 'import ensurepip' 2>/dev/null || {
30
+ echo "✗ module venv/ensurepip absent — sudo apt install python3-venv" >&2; exit 1; }
31
+
32
+ echo "· venv : $VENV"
33
+ echo "· paquets : $(basename "$FICHIER")"
34
+ python3 -m venv "$VENV"
35
+ "$VENV/bin/pip" install --upgrade pip >/dev/null
36
+ "$VENV/bin/pip" install -r "$FICHIER"
37
+ # Versions lues par les MÉTADONNÉES : pyhanko n'expose pas `__version__` (AttributeError).
38
+ "$VENV/bin/python" -c 'import importlib.metadata as m, pyhanko, pikepdf
39
+ print("· pyhanko", m.version("pyhanko"), "· pikepdf", m.version("pikepdf"), "— import OK")'
40
+ echo "✓ venv prêt. Déclarer : PDF_SIGN_PYTHON=$(cd "$VENV" && pwd)/bin/python"
@@ -164,13 +164,24 @@ def _cause_horodatage(e):
164
164
  vus, x = set(), e
165
165
  while x is not None and id(x) not in vus:
166
166
  vus.add(id(x))
167
- for a in getattr(x, "args", ()):
168
- code = getattr(a, "status_code", None)
167
+ # Deux clients HTTP selon la version de pyhanko -- et le code n'est pas au meme endroit :
168
+ # <= 0.36 : `requests`, la reponse est passee en argument -> args[i].status_code
169
+ # >= 0.37 : `aiohttp`, l'exception porte le code elle-meme -> ClientResponseError.status
170
+ # Constate le 28/09/2026 : sous 0.37, un 401 redevenait « Error while contacting
171
+ # timestamp service », sans que rien d'autre ne change.
172
+ codes = [getattr(a, "status_code", None) for a in getattr(x, "args", ())]
173
+ # `.status` n'est lu que sur une exception d'aiohttp : ailleurs, un attribut du meme nom
174
+ # pourrait ne pas etre un code HTTP, et le message accuserait l'horodateur a tort.
175
+ if type(x).__module__.split(".")[0] == "aiohttp":
176
+ codes.append(getattr(x, "status", None))
177
+ for code in codes:
178
+ if not isinstance(code, int):
179
+ continue
169
180
  if code in (401, 403):
170
181
  return f"identifiants refusés par l'horodateur (HTTP {code}) -- clé d'API ou mot de passe absent ou invalide"
171
182
  if code == 429:
172
183
  return "débit de l'horodateur dépassé (HTTP 429) -- réessayer plus tard"
173
- if code is not None and code != 200:
184
+ if code != 200:
174
185
  return f"l'horodateur a répondu HTTP {code}"
175
186
  x = x.__cause__ or x.__context__
176
187
  return str(e)
@@ -226,12 +237,26 @@ def do_verify(pdf_path, cert_pem, open_password=None):
226
237
  if getattr(r, "encrypted", False):
227
238
  # Le mot de passe d'ouverture d'abord s'il est donne, la chaine vide ensuite : un
228
239
  # document non verrouille s'authentifie avec elle, un document verrouille l'exige.
240
+ # ⚠️ `decrypt` ne LÈVE PAS sur un mauvais mot de passe : il rend un AuthResult au statut
241
+ # FAILED. Jusqu'en 0.4.0, la boucle attendait une exception et tenait donc le document
242
+ # pour ouvert des le premier essai, quel qu'il soit — l'echec surgissait plus loin, sans
243
+ # nom. On lit le STATUT. (Constate le 29/09/2026 par T-VER-4, sous pyhanko 0.35 et 0.37.)
244
+ from pyhanko.pdf_utils.crypt import AuthStatus
245
+ ouvert = False
229
246
  for essai in ([open_password] if open_password else []) + [""]:
230
247
  try:
231
- r.decrypt(essai)
232
- break
248
+ ouvert = r.decrypt(essai).status != AuthStatus.FAILED
233
249
  except Exception:
234
- continue
250
+ ouvert = False
251
+ if ouvert:
252
+ break
253
+ # Verrouille et aucun mot de passe ne l'ouvre : on le DIT. Continuer faisait planter
254
+ # l'enumeration plus bas, et l'appelant retombait (<= 0.4.0) sur une detection muette.
255
+ if not ouvert:
256
+ raise SystemExit(
257
+ "ERREUR -- document verrouillé : son mot de passe d'ouverture est requis pour "
258
+ "vérifier ses signatures%s." % (" (celui fourni ne l'ouvre pas)" if open_password else "")
259
+ )
235
260
  sigs = r.embedded_signatures
236
261
  if not sigs:
237
262
  print(json.dumps({"signed": False, "signatures": []}))