@mostajs/pdf-sign 0.4.0 → 0.5.0
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 +74 -0
- package/README.md +4 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +20 -9
- package/dist/verify.js.map +1 -1
- package/docs/INSTALLATION-ET-EXPLOITATION.md +271 -0
- package/llms.txt +11 -2
- package/package.json +2 -1
- package/python/requirements.lock.txt +32 -0
- package/python/requirements.txt +14 -3
- package/python/setup-venv.sh +36 -5
- package/python/sign_with_perms.py +168 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog — @mostajs/pdf-sign
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 07/10/2026
|
|
4
|
+
|
|
5
|
+
**`verifyPdf` dit QUAND et QUI, plus seulement « valide ».**
|
|
6
|
+
|
|
7
|
+
### Ajouté (additif : les champs existants ne changent pas)
|
|
8
|
+
- Par signature (moteur pyhanko) : `type` (signature ou horodatage de document), `level` (PAdES-B-B,
|
|
9
|
+
B-T, B-LT, horodatage de document), `claimedSigningTime` (la date DÉCLARÉE par le signataire),
|
|
10
|
+
`timestamp` (date ATTESTÉE `genTime`, politique `policy` (OID), numéro, autorité `tsa`, chaîne
|
|
11
|
+
embarquée, intact/valid/trusted du jeton), `signerCertificate` (titulaire, émetteur, numéro,
|
|
12
|
+
validité, `selfSigned`), `name` (nom affiché par les lecteurs), `reason`, `location`, `subFilter`,
|
|
13
|
+
`coverage`, `digestAlgorithm`. Au niveau du document : `dss`.
|
|
14
|
+
- Raison : la date déclarée (antidatable) et la date attestée par un horodateur étaient
|
|
15
|
+
indiscernables ; aucun écran de vérification ne pouvait les opposer.
|
|
16
|
+
|
|
17
|
+
### Corrigé
|
|
18
|
+
- Une signature que pyhanko REFUSE de valider (CMS abîmé, certificat de signature qui ne
|
|
19
|
+
correspond plus) est désormais rendue **invalide** (`intact: false, valid: false, error`) au lieu
|
|
20
|
+
de faire échouer toute la vérification. Jusqu'en 0.4.1, un document altéré de cette façon ne
|
|
21
|
+
rendait plus rien. T-DET-1..4.
|
|
22
|
+
|
|
23
|
+
## 0.4.1 — 29/09/2026
|
|
24
|
+
|
|
25
|
+
**pyhanko 0.37 taisait le diagnostic « identifiants refusés » — et rien ne fixait les versions.**
|
|
26
|
+
|
|
27
|
+
### Corrigé
|
|
28
|
+
|
|
29
|
+
pyhanko 0.37 a remplacé son client HTTP (`requests` → `aiohttp`). Son API n'a pas changé, mais le
|
|
30
|
+
code d'erreur HTTP a changé de place : il est porté par l'exception (`.status`) et non plus par la
|
|
31
|
+
réponse passée en argument (`.status_code`). Sous 0.37, la 0.4.0 présentait donc un 401 comme
|
|
32
|
+
« Error while contacting timestamp service » — ce qui envoie chercher un problème réseau. Les deux
|
|
33
|
+
emplacements sont désormais lus. La suite complète passe sous pyhanko **0.35.1 et 0.37.0**.
|
|
34
|
+
|
|
35
|
+
### Corrigé — la VÉRIFICATION, qu'aucun essai ne gardait
|
|
36
|
+
|
|
37
|
+
Relevé en vérifiant que l'adaptation à 0.37 n'avait rien cassé d'autre : `verifyPdf` n'était
|
|
38
|
+
essayé qu'avec le moteur Node. Le chemin pyhanko cachait deux défauts, **identiques sous 0.35 et
|
|
39
|
+
0.37** — donc antérieurs à la montée de version :
|
|
40
|
+
|
|
41
|
+
1. **Repli silencieux.** Tout échec de pyhanko (document verrouillé, plantage, API changée)
|
|
42
|
+
retombait sur la détection Node, qui rend `intact` et `valid` **indéfinis** — et un affichage
|
|
43
|
+
qui lit « pas false » y voit « valide ». Désormais : si pyhanko est présent, son échec est une
|
|
44
|
+
**erreur** qui nomme la cause ; `engine:"pyhanko"` sans pyhanko est une erreur. Le repli Node ne
|
|
45
|
+
vaut plus que si pyhanko **manque**.
|
|
46
|
+
2. **Mauvais mot de passe pris pour le bon.** `decrypt()` ne lève pas : il rend un statut `FAILED`.
|
|
47
|
+
La boucle attendait une exception, et tenait le document pour ouvert au premier essai. Le
|
|
48
|
+
statut est désormais lu ; un document verrouillé vérifié sans son mot de passe donne
|
|
49
|
+
« document verrouillé : son mot de passe d'ouverture est requis ».
|
|
50
|
+
|
|
51
|
+
`test-scripts/test-verification-pyhanko.ts` — 7 essais (T-VER-1 à 7) : intact / altéré /
|
|
52
|
+
horodaté / verrouillé sans et avec mot de passe / fichier illisible / cachet avec image. Suite
|
|
53
|
+
complète : **30 essais, 0 sauté, sous pyhanko 0.35.1 ET 0.37.0**.
|
|
54
|
+
|
|
55
|
+
⚠️ `valid` seul ne suffit pas : sur un document altéré, pyhanko rend `valid: true` (la signature
|
|
56
|
+
est mathématiquement bonne) et `intact: false` (elle ne couvre plus ce document). Lire les deux.
|
|
57
|
+
|
|
58
|
+
### Relecture de l'adaptation 0.37
|
|
59
|
+
|
|
60
|
+
La lecture de `.status` est restreinte aux exceptions d'`aiohttp` : ailleurs, un attribut du même
|
|
61
|
+
nom pourrait ne pas être un code HTTP. Le diagnostic ne peut, par construction, que **changer le
|
|
62
|
+
message** d'un échec — jamais transformer un échec en succès. Contre-épreuve : sans cette lecture,
|
|
63
|
+
T-TSA-4 et T-TSA-8 échouent sous 0.37.
|
|
64
|
+
|
|
65
|
+
### Ajouté — les versions Python sont FIXÉES
|
|
66
|
+
|
|
67
|
+
- `python/requirements.lock.txt` : l'ensemble exact vérifié (24 paquets). **Installé par défaut**.
|
|
68
|
+
- `python/requirements.txt` devient une **plage bornée** : `pyhanko>=0.35.1,<0.38`,
|
|
69
|
+
`pikepdf>=10.9,<11` (auparavant `>=0.25` / `>=8`, sans borne haute : deux installations à un
|
|
70
|
+
mois d'écart n'avaient pas les mêmes versions — c'est ainsi que 0.37 est passé inaperçu).
|
|
71
|
+
- `setup-venv.sh` : accepte un répertoire (pour poser le venv **hors de node_modules/**), vérifie
|
|
72
|
+
Python ≥ 3.10 et `python3-venv` **avant** d'installer, `--plage` pour une montée de version.
|
|
73
|
+
- `docs/INSTALLATION-ET-EXPLOITATION.md` (livré dans le paquet) : exigences, rôle de chaque
|
|
74
|
+
bibliothèque, installation en production, vérification, montée de version, sens de chaque
|
|
75
|
+
message d'erreur, secrets.
|
|
76
|
+
|
|
3
77
|
## 0.4.0 — 28/09/2026
|
|
4
78
|
|
|
5
79
|
**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) :
|
package/dist/types.d.ts
CHANGED
|
@@ -107,17 +107,60 @@ export interface VerifyOptions {
|
|
|
107
107
|
trustPem?: string;
|
|
108
108
|
engine?: SignEngine;
|
|
109
109
|
}
|
|
110
|
+
/** Ce qu'un vérificateur doit voir d'un certificat (0.5.0) — jamais sa clé. */
|
|
111
|
+
export interface CertificateInfo {
|
|
112
|
+
subject: string;
|
|
113
|
+
issuer?: string;
|
|
114
|
+
serial?: string;
|
|
115
|
+
notBefore?: string | null;
|
|
116
|
+
notAfter?: string | null;
|
|
117
|
+
/** Émetteur = titulaire : personne d'autre ne s'en porte garant. */
|
|
118
|
+
selfSigned?: boolean;
|
|
119
|
+
}
|
|
120
|
+
/** Le jeton d'horodatage RFC 3161 attaché à une signature, ou l'horodatage de document (0.5.0). */
|
|
121
|
+
export interface TimestampInfo {
|
|
122
|
+
/** La date ATTESTÉE par l'horodateur (ISO 8601) — à opposer à `claimedSigningTime`. */
|
|
123
|
+
genTime: string | null;
|
|
124
|
+
/** Politique d'horodatage (OID), celle que publie le prestataire. */
|
|
125
|
+
policy: string;
|
|
126
|
+
serial?: string;
|
|
127
|
+
hashAlgorithm?: string;
|
|
128
|
+
tsa: CertificateInfo | null;
|
|
129
|
+
/** Sujets des certificats embarqués dans le jeton. */
|
|
130
|
+
chain: string[];
|
|
131
|
+
intact: boolean | null;
|
|
132
|
+
valid: boolean | null;
|
|
133
|
+
trusted: boolean | null;
|
|
134
|
+
}
|
|
110
135
|
export interface SignatureInfo {
|
|
111
136
|
field?: string;
|
|
112
137
|
intact?: boolean;
|
|
113
138
|
valid?: boolean;
|
|
114
139
|
trusted?: boolean;
|
|
115
140
|
signer?: string;
|
|
141
|
+
type?: "signature" | "horodatage-document";
|
|
142
|
+
/** Nom AFFICHÉ par les lecteurs (« signé par … ») : déclaratif, à ne pas confondre avec le certificat. */
|
|
143
|
+
name?: string | null;
|
|
144
|
+
reason?: string | null;
|
|
145
|
+
location?: string | null;
|
|
146
|
+
subFilter?: string | null;
|
|
147
|
+
/** L'heure DÉCLARÉE par le signataire : sans horodatage, elle est antidatable. */
|
|
148
|
+
claimedSigningTime?: string | null;
|
|
149
|
+
coverage?: string | null;
|
|
150
|
+
digestAlgorithm?: string | null;
|
|
151
|
+
signerCertificate?: CertificateInfo | null;
|
|
152
|
+
timestamp?: TimestampInfo | null;
|
|
153
|
+
/** Cause, quand le moteur refuse de valider la signature (rendue invalide, jamais en panne). */
|
|
154
|
+
error?: string;
|
|
155
|
+
/** PAdES-B-B (aucune date attestée), B-T (jeton), B-LT (+ DSS), ou horodatage de document (B-LTA). */
|
|
156
|
+
level?: string;
|
|
116
157
|
}
|
|
117
158
|
export interface VerifyResult {
|
|
118
159
|
signed: boolean;
|
|
119
160
|
signatures: SignatureInfo[];
|
|
120
161
|
engine: "pyhanko" | "node";
|
|
162
|
+
/** Données de validation à long terme (DSS) présentes dans le document (0.5.0). */
|
|
163
|
+
dss?: boolean;
|
|
121
164
|
}
|
|
122
165
|
export interface EngineAvailability {
|
|
123
166
|
node: boolean;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,qCAAqC;IACrC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,qGAAqG;IACrG,WAAW,CAAC,EAAE,eAAe,GAAG,KAAK,CAAC;IAEtC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,6GAA6G;IAC7G,aAAa,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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", dss: !!parsed.dss };
|
|
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");
|
package/dist/verify.js.map
CHANGED
|
@@ -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,
|
|
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
|
+
- Version: 0.5.0 · 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
|
|
@@ -33,6 +33,10 @@ La signature PAdES + permissions AES-256 garantit intégrité, authenticité et
|
|
|
33
33
|
· opts.timestampHeaders / opts.timestampAuth / opts.timestampTimeout (0.4.0) : joindre un
|
|
34
34
|
horodateur QUALIFIÉ, qui authentifie (clé d'API en en-tête, ou HTTP Basic). Exige pyhanko.
|
|
35
35
|
- verifyPdf(opts): VerifyResult — validation pyhanko complète si dispo, sinon détection Node (/ByteRange)
|
|
36
|
+
0.5.0 : + par signature type, level (PAdES-B-B/B-T/B-LT), claimedSigningTime (DÉCLARÉE), timestamp
|
|
37
|
+
{genTime (ATTESTÉE), policy OID, tsa, chain, intact/valid/trusted}, signerCertificate {subject, issuer,
|
|
38
|
+
notBefore, notAfter, selfSigned}, name/reason/location, coverage ; document : dss. Signature que
|
|
39
|
+
pyhanko refuse = invalide avec `error`, jamais une exception.
|
|
36
40
|
- detectEngines(python?): EngineAvailability — { node:true, pyhanko:bool, python }
|
|
37
41
|
- resolvePython / pyhankoAvailable
|
|
38
42
|
- loadPdfSignConfig(): PdfSignConfig — lecture `.env` via @mostajs/config
|
|
@@ -55,7 +59,9 @@ mosta-pdf-sign engines
|
|
|
55
59
|
|
|
56
60
|
## MOTEUR pyhanko (optionnel)
|
|
57
61
|
bash python/setup-venv.sh → .venv local → exporter PDF_SIGN_PYTHON=…/.venv/bin/python
|
|
58
|
-
|
|
62
|
+
python/requirements.lock.txt = ensemble EXACT essayé (installé par défaut) ; requirements.txt = plage
|
|
63
|
+
admise (pyhanko>=0.35.1,<0.38 ; pikepdf>=10.9,<11). Python >= 3.10. En PRODUCTION : venv HORS de
|
|
64
|
+
node_modules/ (setup-venv.sh <répertoire>). Doc : docs/INSTALLATION-ET-EXPLOITATION.md.
|
|
59
65
|
|
|
60
66
|
## PIÈGES
|
|
61
67
|
- Le cœur Node lève si `permissions` OU `openPassword` est demandé : le chiffrement EXIGE pyhanko.
|
|
@@ -73,3 +79,6 @@ requiert pyhanko>=0.25, pikepdf>=8.
|
|
|
73
79
|
- Les identifiants du .env ne partent QUE si timestampUrl === PDF_SIGN_TSA. Autre adresse ⇒ aucun
|
|
74
80
|
identifiant du .env n'est joint (l'appelant fournit les siens).
|
|
75
81
|
- HTTP 401/403 de l'horodateur ⇒ « identifiants refusés (HTTP 401) », 429 ⇒ « débit dépassé ».
|
|
82
|
+
- verifyPdf (0.4.1) : pyhanko présent ⇒ son échec est une ERREUR (plus de repli Node muet) ;
|
|
83
|
+
engine:'pyhanko' sans pyhanko ⇒ ERREUR. engine 'node' dans le résultat = DÉTECTÉ, pas validé.
|
|
84
|
+
- 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.
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
package/python/requirements.txt
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
-
# @mostajs/pdf-sign — dépendances du moteur pyhanko (PAdES + permissions).
|
|
2
|
-
|
|
3
|
-
|
|
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
|
package/python/setup-venv.sh
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# @mostajs/pdf-sign — crée le venv Python du moteur pyhanko (
|
|
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
|
-
|
|
7
|
-
"$HERE
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
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)
|
|
@@ -201,6 +212,112 @@ def _cert_name(cert):
|
|
|
201
212
|
return str(cert) if cert else ""
|
|
202
213
|
|
|
203
214
|
|
|
215
|
+
def _iso(d):
|
|
216
|
+
try:
|
|
217
|
+
return d.isoformat() if d is not None else None
|
|
218
|
+
except Exception:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _cert_info(c):
|
|
223
|
+
"""Ce qu'un vérificateur doit voir d'un certificat — jamais sa clé."""
|
|
224
|
+
if c is None:
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
v = c["tbs_certificate"]["validity"]
|
|
228
|
+
return {
|
|
229
|
+
"subject": c.subject.human_friendly,
|
|
230
|
+
"issuer": c.issuer.human_friendly,
|
|
231
|
+
"serial": format(c.serial_number, "x"),
|
|
232
|
+
"notBefore": _iso(v["not_before"].native),
|
|
233
|
+
"notAfter": _iso(v["not_after"].native),
|
|
234
|
+
"selfSigned": c.subject == c.issuer,
|
|
235
|
+
}
|
|
236
|
+
except Exception:
|
|
237
|
+
return {"subject": _cert_name(c)}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _jeton(token):
|
|
241
|
+
"""Le jeton RFC 3161 : date, politique, autorité, chaîne embarquée."""
|
|
242
|
+
try:
|
|
243
|
+
sd = token["content"]
|
|
244
|
+
tst = sd["encap_content_info"]["content"].parsed
|
|
245
|
+
certs = [c.chosen for c in (sd["certificates"] or [])]
|
|
246
|
+
tsa = certs[0] if certs else None
|
|
247
|
+
sid = sd["signer_infos"][0]["sid"]
|
|
248
|
+
for c in certs: # le certificat qui a SIGNÉ le jeton, pas le premier venu
|
|
249
|
+
if sid.name == "issuer_and_serial_number" and c.serial_number == sid.chosen["serial_number"].native:
|
|
250
|
+
tsa = c
|
|
251
|
+
return {
|
|
252
|
+
"genTime": _iso(tst["gen_time"].native),
|
|
253
|
+
"policy": tst["policy"].dotted,
|
|
254
|
+
"serial": format(tst["serial_number"].native, "x"),
|
|
255
|
+
"hashAlgorithm": tst["message_imprint"]["hash_algorithm"]["algorithm"].native,
|
|
256
|
+
"tsa": _cert_info(tsa),
|
|
257
|
+
"chain": [c.subject.human_friendly for c in certs],
|
|
258
|
+
}
|
|
259
|
+
except Exception:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _details(s, status, est_horodatage, dss):
|
|
264
|
+
d = {"type": "horodatage-document" if est_horodatage else "signature"}
|
|
265
|
+
so = s.sig_object
|
|
266
|
+
for cle, nom in (("/Name", "name"), ("/Reason", "reason"), ("/Location", "location"), ("/SubFilter", "subFilter")):
|
|
267
|
+
try:
|
|
268
|
+
v = so.get(cle)
|
|
269
|
+
d[nom] = str(v).lstrip("/") if v is not None else None
|
|
270
|
+
except Exception:
|
|
271
|
+
d[nom] = None
|
|
272
|
+
try:
|
|
273
|
+
d["claimedSigningTime"] = _iso(s.self_reported_timestamp) # DÉCLARÉE par le signataire
|
|
274
|
+
except Exception:
|
|
275
|
+
d["claimedSigningTime"] = None
|
|
276
|
+
try:
|
|
277
|
+
d["coverage"] = status.coverage.name
|
|
278
|
+
except Exception:
|
|
279
|
+
d["coverage"] = None
|
|
280
|
+
try:
|
|
281
|
+
d["digestAlgorithm"] = status.md_algorithm
|
|
282
|
+
except Exception:
|
|
283
|
+
d["digestAlgorithm"] = None
|
|
284
|
+
try:
|
|
285
|
+
cert = getattr(status, "signing_cert", None) if status is not None else s.signer_cert
|
|
286
|
+
except Exception:
|
|
287
|
+
cert = None
|
|
288
|
+
d["signerCertificate"] = _cert_info(cert)
|
|
289
|
+
jeton = None
|
|
290
|
+
if est_horodatage:
|
|
291
|
+
try:
|
|
292
|
+
jeton = _jeton(s.signed_data_obj if hasattr(s, "signed_data_obj") else None) or None
|
|
293
|
+
except Exception:
|
|
294
|
+
jeton = None
|
|
295
|
+
if jeton is None:
|
|
296
|
+
try:
|
|
297
|
+
from asn1crypto import cms
|
|
298
|
+
jeton = _jeton(cms.ContentInfo({"content_type": "signed_data", "content": s.signed_data}))
|
|
299
|
+
except Exception:
|
|
300
|
+
jeton = None
|
|
301
|
+
else:
|
|
302
|
+
try:
|
|
303
|
+
attrs = s.signer_info["unsigned_attrs"] or []
|
|
304
|
+
toks = [a["values"][0] for a in attrs if a["type"].native == "signature_time_stamp_token"]
|
|
305
|
+
jeton = _jeton(toks[0]) if toks else None
|
|
306
|
+
except Exception:
|
|
307
|
+
jeton = None
|
|
308
|
+
if jeton is not None:
|
|
309
|
+
tv = getattr(status, "timestamp_validity", None) if not est_horodatage else status
|
|
310
|
+
jeton["intact"] = bool(getattr(tv, "intact", False)) if tv is not None else None
|
|
311
|
+
jeton["valid"] = bool(getattr(tv, "valid", False)) if tv is not None else None
|
|
312
|
+
jeton["trusted"] = bool(getattr(tv, "trusted", False)) if tv is not None else None
|
|
313
|
+
d["timestamp"] = jeton
|
|
314
|
+
if est_horodatage:
|
|
315
|
+
d["level"] = "horodatage de document (PAdES-B-LTA)"
|
|
316
|
+
else:
|
|
317
|
+
d["level"] = "PAdES-B-LT" if (jeton and dss) else ("PAdES-B-T" if jeton else "PAdES-B-B")
|
|
318
|
+
return d
|
|
319
|
+
|
|
320
|
+
|
|
204
321
|
def do_verify(pdf_path, cert_pem, open_password=None):
|
|
205
322
|
import logging
|
|
206
323
|
# Silence les logs internes (tracebacks de chaîne de confiance) : on renvoie le verdict en JSON.
|
|
@@ -226,27 +343,63 @@ def do_verify(pdf_path, cert_pem, open_password=None):
|
|
|
226
343
|
if getattr(r, "encrypted", False):
|
|
227
344
|
# Le mot de passe d'ouverture d'abord s'il est donne, la chaine vide ensuite : un
|
|
228
345
|
# document non verrouille s'authentifie avec elle, un document verrouille l'exige.
|
|
346
|
+
# ⚠️ `decrypt` ne LÈVE PAS sur un mauvais mot de passe : il rend un AuthResult au statut
|
|
347
|
+
# FAILED. Jusqu'en 0.4.0, la boucle attendait une exception et tenait donc le document
|
|
348
|
+
# pour ouvert des le premier essai, quel qu'il soit — l'echec surgissait plus loin, sans
|
|
349
|
+
# nom. On lit le STATUT. (Constate le 29/09/2026 par T-VER-4, sous pyhanko 0.35 et 0.37.)
|
|
350
|
+
from pyhanko.pdf_utils.crypt import AuthStatus
|
|
351
|
+
ouvert = False
|
|
229
352
|
for essai in ([open_password] if open_password else []) + [""]:
|
|
230
353
|
try:
|
|
231
|
-
r.decrypt(essai)
|
|
232
|
-
break
|
|
354
|
+
ouvert = r.decrypt(essai).status != AuthStatus.FAILED
|
|
233
355
|
except Exception:
|
|
234
|
-
|
|
356
|
+
ouvert = False
|
|
357
|
+
if ouvert:
|
|
358
|
+
break
|
|
359
|
+
# Verrouille et aucun mot de passe ne l'ouvre : on le DIT. Continuer faisait planter
|
|
360
|
+
# l'enumeration plus bas, et l'appelant retombait (<= 0.4.0) sur une detection muette.
|
|
361
|
+
if not ouvert:
|
|
362
|
+
raise SystemExit(
|
|
363
|
+
"ERREUR -- document verrouillé : son mot de passe d'ouverture est requis pour "
|
|
364
|
+
"vérifier ses signatures%s." % (" (celui fourni ne l'ouvre pas)" if open_password else "")
|
|
365
|
+
)
|
|
235
366
|
sigs = r.embedded_signatures
|
|
236
367
|
if not sigs:
|
|
237
368
|
print(json.dumps({"signed": False, "signatures": []}))
|
|
238
369
|
return
|
|
239
370
|
out = []
|
|
371
|
+
dss = "/DSS" in r.root
|
|
240
372
|
for s in sigs:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
373
|
+
est_horodatage = str(s.sig_object.get("/Type", "")) == "/DocTimeStamp"
|
|
374
|
+
# 0.5.0 : une signature que pyhanko REFUSE de valider (structure CMS abîmée, certificat
|
|
375
|
+
# qui ne correspond plus…) est un verdict — invalide —, pas une panne de la vérification.
|
|
376
|
+
# Jusqu'en 0.4.1, l'exception remontait et l'appelant ne lisait plus RIEN du document.
|
|
377
|
+
try:
|
|
378
|
+
if est_horodatage:
|
|
379
|
+
from pyhanko.sign.validation import validate_pdf_timestamp
|
|
380
|
+
status = validate_pdf_timestamp(s, vc)
|
|
381
|
+
else:
|
|
382
|
+
status = validate_pdf_signature(s, vc)
|
|
383
|
+
base = {
|
|
384
|
+
"field": s.field_name,
|
|
385
|
+
"intact": bool(status.intact),
|
|
386
|
+
"valid": bool(status.valid),
|
|
387
|
+
"trusted": bool(getattr(status, "trusted", False)),
|
|
388
|
+
"signer": _cert_name(getattr(status, "signing_cert", None)),
|
|
389
|
+
}
|
|
390
|
+
except Exception as e:
|
|
391
|
+
status = None
|
|
392
|
+
try:
|
|
393
|
+
signataire = s.signer_cert
|
|
394
|
+
except Exception:
|
|
395
|
+
signataire = None
|
|
396
|
+
base = {"field": s.field_name, "intact": False, "valid": False, "trusted": False,
|
|
397
|
+
"signer": _cert_name(signataire), "error": str(e).split("\n")[0][:300]}
|
|
398
|
+
# 0.5.0 — le DÉTAIL, additif : chaque champ est lu à part, et un champ illisible vaut
|
|
399
|
+
# null plutôt que de faire échouer la vérification entière.
|
|
400
|
+
base.update(_details(s, status, est_horodatage, dss))
|
|
401
|
+
out.append(base)
|
|
402
|
+
print(json.dumps({"signed": True, "signatures": out, "dss": dss}, default=str))
|
|
250
403
|
|
|
251
404
|
|
|
252
405
|
if __name__ == "__main__":
|