@mostajs/pdf-sign 0.6.0 → 0.8.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 CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog — @mostajs/pdf-sign
2
2
 
3
+ ## 0.8.0 — 12/10/2026
4
+
5
+ **La signature SÉPARÉE (XAdES) : la vérifier, comme celle du Journal officiel de l'UE, et en produire.**
6
+
7
+ Le Journal officiel publie des PDF qui ne portent aucune signature : elle est dans un fichier XML à côté, dont le
8
+ manifeste nomme chaque document par son empreinte.
9
+
10
+ ### Ajouté
11
+ - `verifyDetached({ xml, files? })` : vérifie une signature XML séparée (XAdES ou XMLDSig) — valeur de signature
12
+ (RSA, RSA-PSS, ECDSA), références, propriétés signées, certificat annoncé (`SigningCertificate(V2)`), heure
13
+ déclarée, jetons d'horodatage (sur la valeur de signature, et d'archive XAdES 1.4.1, empreinte recalculée),
14
+ données de longue durée, niveau (B-B, B-T, B-LT, B-LTA), et pour chaque fichier fourni s'il est couvert et sous
15
+ quel nom. Le certificat dit s'il SE DÉCLARE qualifié (QCStatements : `qualified`, `qscd`, `qcType`) ; les listes
16
+ de confiance de l'UE ne sont pas consultées.
17
+ - `signDetached({ files, p12?, passphrase?, timestampUrl?… })` → `{ xml }` : UN XML XAdES qui scelle tous les
18
+ fichiers (manifeste, SHA-512), signé RSA ou EC ; horodaté RFC 3161 (XAdES-B-T) si un horodateur est donné, sans
19
+ repli. Mêmes règles que `signPdf` pour le certificat, la phrase et les identifiants de l'horodateur.
20
+ - `detachedVerdict(r)` → `"altered" | "invalid" | "valid"` ; `detachedAvailable()`.
21
+ - Lecture prudente : DOCTYPE et entités refusés, aucun accès réseau, deux éléments au même `Id` refusés
22
+ (emballage de signature), transformations limitées aux canonicalisations et à la signature enveloppée.
23
+ - Essais T-XAD-1 à 6, dont la signature RÉELLE du règlement (UE) 2024/2847 (fixtures/ue) ; le XML produit est
24
+ aussi validé par un vérificateur indépendant (signxml), en RSA et en EC.
25
+
26
+ ### Corrigé (essais)
27
+ - T-DET-3 échouait au hasard : l'octet altéré tombait parfois dans une chaîne hexadécimale, illisible pour pyhanko.
28
+ Il est pris désormais dans le commentaire binaire d'en-tête ; l'essai attend le verdict « altéré ».
29
+
30
+ ## 0.7.0 — 11/10/2026
31
+
32
+ **Vérifier un PDF reçu en mémoire, pour une page publique de vérification.**
33
+
34
+ ### Ajouté
35
+ - `verifyPdfBytes(octets, { openPassword?, trustPem? })` → le résultat de `verifyPdf` plus `file: { bytes, sha256 }`.
36
+ Le PDF n'existe sur disque que le temps de la vérification (dossier privé, fichier 0600, effacé quoi qu'il
37
+ arrive) ; un fichier sans en-tête `%PDF-` est refusé avant tout processus ; le moteur pyhanko est exigé.
38
+ - `signatureVerdict(s)` → `"altered" | "valid" | "trusted" | "invalid"` : l'intégrité d'abord, la confiance ensuite ;
39
+ « présente » (détection Node) n'est jamais « vérifiée ».
40
+ - `fullVerificationAvailable()` : pyhanko est-il là ?
41
+ - Écrit d'abord par mdseal (page /verifier, 07/10/2026), demandé ensuite par qatrax.dev : la logique descend ici.
42
+ Essais T-OCT-1 à 4.
43
+
44
+ ### Corrigé
45
+ - `node-forge` est déclaré en dépendance : `@signpdf/signer-p12` le demande comme dépendance « pair », qu'une
46
+ installation `--legacy-peer-deps` n'installe pas (relevé dans qatrax-cra le 12/10/2026 : « Cannot find module 'node-forge' »).
47
+
3
48
  ## 0.6.0 — 11/10/2026
4
49
 
5
50
  **`timestampPdf` : un horodatage de DOCUMENT ajouté à un PDF déjà signé, sans toucher à sa signature.**
package/README.md CHANGED
@@ -108,10 +108,38 @@ await signPdf({
108
108
  await signPdf({ input: "note.pdf", p12: "cert.p12", passphrase: "•••", openPassword: "•••" });
109
109
  ```
110
110
 
111
+ ### Vérifier un PDF reçu (0.7.0)
112
+
113
+ Pour une page publique où l'on dépose un PDF : `verifyPdfBytes(octets)` rend `{ file: { bytes, sha256 }, signed,
114
+ signatures, dss }` sans laisser de fichier derrière ; `signatureVerdict(s)` dit `altered`, `valid`, `trusted` ou
115
+ `invalid` ; `fullVerificationAvailable()` dit si pyhanko est là (sans lui, ne rien afficher comme vérifié).
116
+
111
117
  > ⚠ Un document verrouillé se vérifie avec sa clé : `verifyPdf({ input, openPassword })`. Sans
112
118
  > elle, ses signatures sont illisibles et la réponse serait « non signé » — sur un document
113
119
  > parfaitement signé.
114
120
 
121
+ ### Signature séparée — XAdES, comme au Journal officiel (0.8.0)
122
+
123
+ Le Journal officiel de l'UE publie ses PDF **sans** signature : elle est dans un XML à côté, dont le manifeste nomme
124
+ chaque document par son empreinte. On la vérifie, et l'on en produit de semblables pour un dossier de fichiers :
125
+
126
+ ```js
127
+ import { verifyDetached, detachedVerdict, signDetached } from "@mostajs/pdf-sign";
128
+
129
+ const r = verifyDetached({ xml, files: [{ name: "reglement.pdf", bytes: pdf }] });
130
+ detachedVerdict(r); // "valid" | "altered" | "invalid"
131
+ r.documents[0].covered; // ce PDF est-il l'un des documents signés ? (r.documents[0].coveredAs : sous quel nom)
132
+ r.signerCertificate; // sujet, émetteur, validité, qualified / qscd / qcType (ce que le certificat DÉCLARE)
133
+ r.timestamps; // jetons RFC 3161 : genTime, horodateur, imprintOk, intact, valid
134
+
135
+ const { xml: sceau } = await signDetached({
136
+ files: [{ name: "certificat.pdf", bytes: a }, { name: "livrable.json", bytes: b }],
137
+ p12: "cachet.p12", passphrase: "•••", timestampUrl: "https://…", // XAdES-B-T ; sans horodateur : B-B
138
+ });
139
+ ```
140
+
141
+ > ⚠ « qualifié » se lit dans le certificat (QCStatements) : les listes de confiance de l'UE ne sont pas consultées.
142
+
115
143
  ## Horodatage — et joindre un horodateur QUALIFIÉ *(0.4.0)*
116
144
 
117
145
  `timestampUrl` fait passer la signature de **PAdES-B** à **PAdES-T** : la date n'est plus déclarée
@@ -0,0 +1,18 @@
1
+ import type { DetachedSignOptions, DetachedSignResult, DetachedVerdict, DetachedVerifyOptions, DetachedVerifyResult } from "./types.js";
2
+ /** Le moteur de la signature séparée est-il là ? (le Python de pyhanko, qui porte aussi lxml) */
3
+ export declare function detachedAvailable(): boolean;
4
+ /** Vérifie une signature XML séparée (XAdES ou XMLDSig) et dit, pour chaque fichier fourni, s'il est couvert. */
5
+ export declare function verifyDetached(opts: DetachedVerifyOptions): DetachedVerifyResult;
6
+ /**
7
+ * Produit UN XML XAdES qui scelle tous les fichiers donnés (manifeste, empreintes SHA-512), signé par le certificat
8
+ * PKCS#12 ; horodaté RFC 3161 (XAdES-B-T) si un horodateur est donné. Le certificat et la phrase de la configuration
9
+ * (`PDF_SIGN_P12`…) servent par défaut ; les identifiants d'horodateur ne partent que vers l'horodateur du `.env`.
10
+ */
11
+ export declare function signDetached(opts: DetachedSignOptions): Promise<DetachedSignResult>;
12
+ /**
13
+ * Le verdict de la signature, en un mot. « altered » : une partie SIGNÉE a changé (propriétés, manifeste, certificat
14
+ * annoncé) ; « invalid » : la valeur de signature ne correspond pas ; « valid » : tout concorde. La couverture des
15
+ * documents se lit à part, fichier par fichier (`documents[].covered`).
16
+ */
17
+ export declare function detachedVerdict(r: Pick<DetachedVerifyResult, "signatureValid" | "references" | "signingCertificateOk">): DetachedVerdict;
18
+ //# sourceMappingURL=detached.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detached.d.ts","sourceRoot":"","sources":["../src/detached.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAgB,mBAAmB,EAAE,kBAAkB,EAAE,eAAe,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAQtJ,iGAAiG;AACjG,wBAAgB,iBAAiB,IAAI,OAAO,CAK3C;AA2BD,iHAAiH;AACjH,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAehF;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkCzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,gBAAgB,GAAG,YAAY,GAAG,sBAAsB,CAAC,GAAG,eAAe,CAKxI"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @mostajs/pdf-sign — LA SIGNATURE SÉPARÉE (0.8.0) : un XML XAdES à côté des documents, comme au Journal officiel.
3
+ *
4
+ * Le Journal officiel de l'UE publie des PDF sans signature ; la signature est un fichier XML dont le manifeste nomme
5
+ * chaque document par son empreinte. `verifyDetached` vérifie une telle signature ; `signDetached` en produit une
6
+ * qui scelle plusieurs fichiers d'un coup (le dossier d'un livrable : certificat PDF, JSON scellé, SBOM…).
7
+ *
8
+ * Moteur : python/xades.py (lxml, cryptography, pyhanko pour l'horodatage). Les octets reçus n'existent sur disque
9
+ * que le temps de l'appel, dans un dossier privé effacé ensuite ; la phrase du certificat et les identifiants de
10
+ * l'horodateur passent par fichier, jamais par `argv`. Aucun repli : un horodateur qui ne répond pas fait échouer.
11
+ *
12
+ * @author Dr Hamid MADANI <drmdh@msn.com>
13
+ * @license AGPL-3.0-or-later
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { detectEngines, PYTHON_DIR } from "./engines.js";
20
+ import { loadPdfSignConfig } from "./config.js";
21
+ import { controlerHorodatage, identifiantsHorodateur } from "./sign.js";
22
+ const XADES_PY = join(PYTHON_DIR, "xades.py");
23
+ const TAILLE_MAX_XML = 10 * 1024 * 1024;
24
+ /** Le moteur de la signature séparée est-il là ? (le Python de pyhanko, qui porte aussi lxml) */
25
+ export function detachedAvailable() {
26
+ const avail = detectEngines(loadPdfSignConfig().python);
27
+ if (!avail.pyhanko)
28
+ return false;
29
+ try {
30
+ execFileSync(avail.python, ["-c", "import lxml, cryptography, asn1crypto"], { stdio: "ignore" });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function python() {
38
+ const avail = detectEngines(loadPdfSignConfig().python);
39
+ if (!avail.pyhanko)
40
+ throw new Error("Signature séparée : moteur Python (pyhanko) indisponible — lancer python/setup-venv.sh (ou PDF_SIGN_PYTHON).");
41
+ return avail.python;
42
+ }
43
+ function lancer(py, mode, cfgPath) {
44
+ try {
45
+ return execFileSync(py, [XADES_PY, mode, cfgPath], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 64 * 1024 * 1024 });
46
+ }
47
+ catch (e) {
48
+ const trace = String(e?.stderr ?? "");
49
+ const net = trace.match(/ERREUR -- ([^\n]*)/);
50
+ if (!net && trace)
51
+ process.stderr.write(trace); // le journal garde la trace entière ; l'appelant, la cause
52
+ throw new Error(net ? net[1] : `Signature séparée : échec du moteur (${trace.trim().split("\n").pop() || String(e?.message ?? e)})`);
53
+ }
54
+ }
55
+ function deposer(dossier, files) {
56
+ return files.map((f, i) => {
57
+ const chemin = join(dossier, `f${i}`);
58
+ writeFileSync(chemin, Buffer.from(f.bytes), { mode: 0o600 });
59
+ return { name: String(f.name), path: chemin };
60
+ });
61
+ }
62
+ /** Vérifie une signature XML séparée (XAdES ou XMLDSig) et dit, pour chaque fichier fourni, s'il est couvert. */
63
+ export function verifyDetached(opts) {
64
+ const xml = Buffer.from(opts.xml);
65
+ if (xml.length > TAILLE_MAX_XML)
66
+ throw new Error("fichier XML trop volumineux (10 Mo au plus)");
67
+ const py = python();
68
+ const dossier = mkdtempSync(join(tmpdir(), "pdf-sign-xades-"));
69
+ try {
70
+ const xmlPath = join(dossier, "signature.xml");
71
+ writeFileSync(xmlPath, xml, { mode: 0o600 });
72
+ const cfg = join(dossier, "config.json");
73
+ writeFileSync(cfg, JSON.stringify({ xml: xmlPath, files: deposer(dossier, opts.files ?? []) }), { mode: 0o600 });
74
+ const out = lancer(py, "verify", cfg);
75
+ return JSON.parse(out.trim().split("\n").pop() ?? "{}");
76
+ }
77
+ finally {
78
+ rmSync(dossier, { recursive: true, force: true });
79
+ }
80
+ }
81
+ /**
82
+ * Produit UN XML XAdES qui scelle tous les fichiers donnés (manifeste, empreintes SHA-512), signé par le certificat
83
+ * PKCS#12 ; horodaté RFC 3161 (XAdES-B-T) si un horodateur est donné. Le certificat et la phrase de la configuration
84
+ * (`PDF_SIGN_P12`…) servent par défaut ; les identifiants d'horodateur ne partent que vers l'horodateur du `.env`.
85
+ */
86
+ export async function signDetached(opts) {
87
+ if (!opts.files?.length)
88
+ throw new Error("Signature séparée : aucun fichier à signer.");
89
+ const noms = opts.files.map((f) => f.name);
90
+ if (new Set(noms).size !== noms.length)
91
+ throw new Error("Signature séparée : deux fichiers portent le même nom (nom en double).");
92
+ const cfg = loadPdfSignConfig();
93
+ const p12Propre = opts.p12 != null && opts.p12 !== cfg.p12;
94
+ const p12 = opts.p12 ?? cfg.p12;
95
+ if (!p12)
96
+ throw new Error("Certificat PKCS#12 manquant (opts.p12 ou PDF_SIGN_P12).");
97
+ const horodateur = { timestampUrl: opts.timestampUrl, ...identifiantsHorodateur(opts, cfg) };
98
+ controlerHorodatage(horodateur);
99
+ const py = python();
100
+ const dossier = mkdtempSync(join(tmpdir(), "pdf-sign-xades-"));
101
+ try {
102
+ const p12Path = typeof p12 === "string" ? p12 : join(dossier, "cert.p12");
103
+ if (typeof p12 !== "string")
104
+ writeFileSync(p12Path, Buffer.from(p12), { mode: 0o600 });
105
+ let passFile = p12Propre ? opts.passphraseFile : (opts.passphraseFile ?? cfg.passphraseFile);
106
+ if (!passFile) {
107
+ passFile = join(dossier, ".pass");
108
+ writeFileSync(passFile, (p12Propre ? opts.passphrase : (opts.passphrase ?? cfg.passphrase)) ?? "", { mode: 0o600 });
109
+ }
110
+ const output = join(dossier, "signature.xml");
111
+ const conf = join(dossier, "config.json");
112
+ writeFileSync(conf, JSON.stringify({
113
+ files: deposer(dossier, opts.files), p12: p12Path, passFile, output,
114
+ timestamp_url: horodateur.timestampUrl || undefined,
115
+ timestamp_headers: horodateur.timestampHeaders,
116
+ timestamp_auth: horodateur.timestampAuth,
117
+ timestamp_timeout: horodateur.timestampTimeout,
118
+ }), { mode: 0o600 });
119
+ lancer(py, "sign", conf);
120
+ return { xml: readFileSync(output), engine: "pyhanko" };
121
+ }
122
+ finally {
123
+ rmSync(dossier, { recursive: true, force: true });
124
+ }
125
+ }
126
+ /**
127
+ * Le verdict de la signature, en un mot. « altered » : une partie SIGNÉE a changé (propriétés, manifeste, certificat
128
+ * annoncé) ; « invalid » : la valeur de signature ne correspond pas ; « valid » : tout concorde. La couverture des
129
+ * documents se lit à part, fichier par fichier (`documents[].covered`).
130
+ */
131
+ export function detachedVerdict(r) {
132
+ const internes = r.references.filter((x) => x.type !== "document");
133
+ if (internes.some((x) => x.ok === false) || r.signingCertificateOk === false)
134
+ return "altered";
135
+ if (r.signatureValid !== true)
136
+ return "invalid";
137
+ return "valid";
138
+ }
139
+ //# sourceMappingURL=detached.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detached.js","sourceRoot":"","sources":["../src/detached.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -9,8 +9,9 @@
9
9
  */
10
10
  export { signPdf } from "./sign.js";
11
11
  export { timestampPdf } from "./timestamp.js";
12
- export { verifyPdf } from "./verify.js";
12
+ export { verifyPdf, verifyPdfBytes, signatureVerdict, fullVerificationAvailable } from "./verify.js";
13
+ export { signDetached, verifyDetached, detachedVerdict, detachedAvailable } from "./detached.js";
13
14
  export { detectEngines, resolvePython, pyhankoAvailable } from "./engines.js";
14
15
  export { loadPdfSignConfig, type PdfSignConfig } from "./config.js";
15
- export type { SignOptions, SignResult, SignMeta, SignField, SignPermissions, SignEngine, VerifyOptions, VerifyResult, SignatureInfo, EngineAvailability, TimestampOptions, HorodateurOptions, } from "./types.js";
16
+ export type { SignOptions, SignResult, SignMeta, SignField, SignPermissions, SignEngine, VerifyOptions, VerifyResult, VerifyBytesResult, SignatureVerdict, SignatureInfo, EngineAvailability, TimestampOptions, HorodateurOptions, DetachedFile, DetachedSignOptions, DetachedSignResult, DetachedVerifyOptions, DetachedVerifyResult, DetachedVerdict, DetachedCertificateInfo, DetachedTimestampInfo, } from "./types.js";
16
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EACV,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EACzE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,GACpG,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EACV,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,UAAU,EACzE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EACxI,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,eAAe,EACnH,uBAAuB,EAAE,qBAAqB,GAC/C,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -9,7 +9,8 @@
9
9
  */
10
10
  export { signPdf } from "./sign.js";
11
11
  export { timestampPdf } from "./timestamp.js";
12
- export { verifyPdf } from "./verify.js";
12
+ export { verifyPdf, verifyPdfBytes, signatureVerdict, fullVerificationAvailable } from "./verify.js";
13
+ export { signDetached, verifyDetached, detachedVerdict, detachedAvailable } from "./detached.js";
13
14
  export { detectEngines, resolvePython, pyhankoAvailable } from "./engines.js";
14
15
  export { loadPdfSignConfig } from "./config.js";
15
16
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAsB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAsB,MAAM,aAAa,CAAC"}
package/dist/types.d.ts CHANGED
@@ -178,9 +178,98 @@ export interface VerifyResult {
178
178
  /** Données de validation à long terme (DSS) présentes dans le document (0.5.0). */
179
179
  dss?: boolean;
180
180
  }
181
+ /** 0.7.0 — `signatureVerdict` : altérée, valide (sans chaîne de confiance), de confiance, ou invalide. */
182
+ export type SignatureVerdict = "altered" | "valid" | "trusted" | "invalid";
183
+ /** 0.7.0 — `verifyPdfBytes` : le résultat de `verifyPdf`, plus la taille et l'empreinte du fichier reçu. */
184
+ export interface VerifyBytesResult extends VerifyResult {
185
+ file: {
186
+ bytes: number;
187
+ sha256: string;
188
+ };
189
+ }
181
190
  export interface EngineAvailability {
182
191
  node: boolean;
183
192
  pyhanko: boolean;
184
193
  python?: string;
185
194
  }
195
+ /** Un fichier fourni à la signature séparée, ou à sa vérification. */
196
+ export interface DetachedFile {
197
+ name: string;
198
+ bytes: Uint8Array;
199
+ }
200
+ export interface DetachedSignOptions extends HorodateurOptions {
201
+ /** Les fichiers que le XML scellera, chacun nommé dans le manifeste (noms distincts). */
202
+ files: DetachedFile[];
203
+ /** PKCS#12 (chemin ou octets) ; défaut : PDF_SIGN_P12. */
204
+ p12?: string | Uint8Array;
205
+ passphrase?: string;
206
+ passphraseFile?: string;
207
+ }
208
+ export interface DetachedSignResult {
209
+ /** Le XML XAdES (XAdES-B-B, ou B-T avec un horodateur). */
210
+ xml: Buffer;
211
+ engine: "pyhanko";
212
+ }
213
+ export interface DetachedVerifyOptions {
214
+ /** Le XML de la signature. */
215
+ xml: Uint8Array;
216
+ /** Les documents à confronter au manifeste (optionnels : la signature se vérifie sans eux). */
217
+ files?: DetachedFile[];
218
+ }
219
+ /** Ce que dit un certificat de signataire XAdES — `qualified` : il SE DÉCLARE qualifié (QCStatements). */
220
+ export interface DetachedCertificateInfo extends CertificateInfo {
221
+ qualified: boolean;
222
+ /** Clé sur dispositif qualifié (QcSSCD). */
223
+ qscd: boolean;
224
+ /** « esign » (personne), « eseal » (cachet d'organisation), « web ». */
225
+ qcType: string[];
226
+ }
227
+ export interface DetachedTimestampInfo extends TimestampInfo {
228
+ /** « signature » (sur la valeur de signature), « archive » (longue durée), « other ». */
229
+ kind: "signature" | "archive" | "other";
230
+ /** Le jeton porte-t-il bien sur ce qu'il prétend couvrir ? null : non recalculé. */
231
+ imprintOk: boolean | null;
232
+ }
233
+ export interface DetachedVerifyResult {
234
+ format: "xades" | "xmldsig";
235
+ /** XAdES-B-B, B-T, B-LT, B-LTA, ou XMLDSig. */
236
+ level: string;
237
+ /** Nombre de ds:Signature dans le fichier (la première est vérifiée). */
238
+ signatures: number;
239
+ signatureValid: boolean;
240
+ signatureAlgorithm: string | null;
241
+ canonicalization: string | null;
242
+ references: {
243
+ uri: string | null;
244
+ type: "manifest" | "signedProperties" | "document" | "other";
245
+ ok: boolean | null;
246
+ }[];
247
+ signedPropertiesOk: boolean | null;
248
+ /** Le certificat qui a signé est-il celui que la signature annonce (SigningCertificate) ? */
249
+ signingCertificateOk: boolean | null;
250
+ signerCertificate: DetachedCertificateInfo | null;
251
+ /** L'heure DÉCLARÉE (xades:SigningTime) — à opposer à celle des jetons. */
252
+ signingTime: string | null;
253
+ timestamps: DetachedTimestampInfo[];
254
+ manifest: {
255
+ uri: string;
256
+ digestAlgorithm: string;
257
+ matchedFile: string | null;
258
+ }[];
259
+ documents: {
260
+ name: string;
261
+ bytes: number;
262
+ sha256: string;
263
+ covered: boolean;
264
+ coveredAs: string | null;
265
+ }[];
266
+ /** Données de validation à long terme embarquées. */
267
+ longTerm: {
268
+ certificates: number;
269
+ ocsp: number;
270
+ crl: number;
271
+ };
272
+ }
273
+ /** « altered » : une partie signée a changé ; « invalid » : la signature ne correspond pas ; « valid ». */
274
+ export type DetachedVerdict = "altered" | "invalid" | "valid";
186
275
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/verify.d.ts CHANGED
@@ -1,3 +1,13 @@
1
- import type { VerifyOptions, VerifyResult } from "./types.js";
1
+ import type { SignatureInfo, SignatureVerdict, VerifyBytesResult, VerifyOptions, VerifyResult } from "./types.js";
2
2
  export declare function verifyPdf(opts: VerifyOptions): VerifyResult;
3
+ /** Le moteur qui VÉRIFIE (pyhanko) est-il là ? Sans lui, on ne sait que constater une présence de signature. */
4
+ export declare function fullVerificationAvailable(): boolean;
5
+ /**
6
+ * Vérifie un PDF reçu en mémoire (dépôt sur une page publique). Le fichier n'existe sur disque que le temps de la
7
+ * vérification, dans un dossier privé (0700, fichier 0600), effacé ensuite quoi qu'il arrive. Moteur pyhanko exigé :
8
+ * une page publique ne doit jamais afficher « signé » pour « vérifié ».
9
+ */
10
+ export declare function verifyPdfBytes(bytes: Uint8Array, opts?: Omit<VerifyOptions, "input" | "engine">): VerifyBytesResult;
11
+ /** Le verdict d'une signature, en un mot : la confiance ne se lit qu'APRÈS l'intégrité. */
12
+ export declare function signatureVerdict(s: Pick<SignatureInfo, "intact" | "valid" | "trusted">): SignatureVerdict;
3
13
  //# sourceMappingURL=verify.d.ts.map
@@ -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,CAkC3D"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAIlH,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAkC3D;AAKD,gHAAgH;AAChH,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,GAAE,IAAI,CAAC,aAAa,EAAE,OAAO,GAAG,QAAQ,CAAM,GAAG,iBAAiB,CAavH;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,gBAAgB,CAIzG"}
package/dist/verify.js CHANGED
@@ -8,7 +8,10 @@
8
8
  * @license AGPL-3.0-or-later
9
9
  */
10
10
  import { execFileSync } from "node:child_process";
11
- import { readFileSync } from "node:fs";
11
+ import { createHash } from "node:crypto";
12
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
12
15
  import { detectEngines, SIGN_PY } from "./engines.js";
13
16
  import { loadPdfSignConfig } from "./config.js";
14
17
  export function verifyPdf(opts) {
@@ -46,4 +49,39 @@ export function verifyPdf(opts) {
46
49
  const signed = s.includes("/ByteRange") && /\/Type\s*\/Sig|adbe\.pkcs7|ETSI\.CAdES/.test(s);
47
50
  return { signed, signatures: signed ? [{ intact: undefined, valid: undefined }] : [], engine: "node" };
48
51
  }
52
+ // ── 0.7.0 (11/10/2026) — VÉRIFIER UN PDF REÇU EN MÉMOIRE ─────────────────────────────────────────────────────────
53
+ // Écrit d'abord par mdseal pour sa page publique /verifier, puis demandé par qatrax.dev : la logique descend ici (R-G).
54
+ /** Le moteur qui VÉRIFIE (pyhanko) est-il là ? Sans lui, on ne sait que constater une présence de signature. */
55
+ export function fullVerificationAvailable() {
56
+ return detectEngines(loadPdfSignConfig().python).pyhanko === true;
57
+ }
58
+ /**
59
+ * Vérifie un PDF reçu en mémoire (dépôt sur une page publique). Le fichier n'existe sur disque que le temps de la
60
+ * vérification, dans un dossier privé (0700, fichier 0600), effacé ensuite quoi qu'il arrive. Moteur pyhanko exigé :
61
+ * une page publique ne doit jamais afficher « signé » pour « vérifié ».
62
+ */
63
+ export function verifyPdfBytes(bytes, opts = {}) {
64
+ const buf = Buffer.from(bytes);
65
+ if (!buf.subarray(0, 5).equals(Buffer.from("%PDF-")))
66
+ throw new Error("Ce fichier n'est pas un PDF (en-tête %PDF- absent).");
67
+ const file = { bytes: buf.length, sha256: createHash("sha256").update(buf).digest("hex") };
68
+ const dossier = mkdtempSync(join(tmpdir(), "pdf-sign-verif-"));
69
+ try {
70
+ const chemin = join(dossier, "document.pdf");
71
+ writeFileSync(chemin, buf, { mode: 0o600 });
72
+ const r = verifyPdf({ ...opts, input: chemin, engine: "pyhanko" });
73
+ return { file, signed: r.signed, signatures: r.signatures, dss: !!r.dss, engine: r.engine };
74
+ }
75
+ finally {
76
+ rmSync(dossier, { recursive: true, force: true });
77
+ }
78
+ }
79
+ /** Le verdict d'une signature, en un mot : la confiance ne se lit qu'APRÈS l'intégrité. */
80
+ export function signatureVerdict(s) {
81
+ if (s.intact === false)
82
+ return "altered";
83
+ if (s.intact === true && s.valid === true)
84
+ return s.trusted ? "trusted" : "valid";
85
+ return "invalid";
86
+ }
49
87
  //# sourceMappingURL=verify.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
package/llms.txt CHANGED
@@ -67,7 +67,19 @@ python/requirements.lock.txt = ensemble EXACT essayé (installé par défaut) ;
67
67
  admise (pyhanko>=0.35.1,<0.38 ; pikepdf>=10.9,<11). Python >= 3.10. En PRODUCTION : venv HORS de
68
68
  node_modules/ (setup-venv.sh <répertoire>). Doc : docs/INSTALLATION-ET-EXPLOITATION.md.
69
69
 
70
+ ## SIGNATURE SÉPARÉE (0.8.0) — XAdES, comme au Journal officiel de l'UE
71
+ verifyDetached({ xml, files? }) → { format, level (XAdES-B-B|B-T|B-LT|B-LTA|XMLDSig), signatureValid, references,
72
+ signedPropertiesOk, signingCertificateOk, signerCertificate{subject,issuer,notBefore,notAfter,qualified,qscd,qcType},
73
+ signingTime, timestamps[{kind:signature|archive|other, genTime, tsa, imprintOk, intact, valid, trusted}],
74
+ manifest[{uri,digestAlgorithm,matchedFile}], documents[{name,bytes,sha256,covered,coveredAs}], longTerm }
75
+ signDetached({ files:[{name,bytes}], p12?, passphrase?, timestampUrl?… }) → { xml } (manifeste SHA-512, RSA ou EC)
76
+ detachedVerdict(r) → altered | invalid | valid ; detachedAvailable() → moteur Python présent ?
77
+ Moteur : python/xades.py (lxml, cryptography, asn1crypto ; pyhanko pour l'horodatage).
78
+
70
79
  ## PIÈGES
80
+ - verifyDetached : DOCTYPE/ENTITY refusés ; deux éléments au même Id refusés ; documents rapprochés PAR EMPREINTE
81
+ (le nom peut différer : coveredAs dit sous quel nom). « qualified » = ce que DÉCLARE le certificat, pas une liste de confiance.
82
+ - signDetached : noms de fichiers distincts exigés ; horodateur injoignable ⇒ ERREUR (aucun XML sans le jeton demandé).
71
83
  - Le cœur Node lève si `permissions` OU `openPassword` est demandé : le chiffrement EXIGE pyhanko.
72
84
  - `openPassword` implique le chiffrement même sans `permissions` — et n'impose alors AUCUNE
73
85
  restriction (qui demande une clé ne demande pas de perdre son imprimante).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/pdf-sign",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
@@ -51,7 +51,8 @@
51
51
  "@signpdf/signpdf": "^3.2.4",
52
52
  "@signpdf/signer-p12": "^3.2.4",
53
53
  "@signpdf/placeholder-plain": "^3.2.4",
54
- "pdf-lib": "^1.17.1"
54
+ "pdf-lib": "^1.17.1",
55
+ "node-forge": "^1.3.3"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@mostajs/mjs-unit": "^0.4.1",
@@ -0,0 +1,601 @@
1
+ #!/usr/bin/env python3
2
+ """@mostajs/pdf-sign 0.8.0 -- LA SIGNATURE SEPAREE (XAdES), comme celle du Journal officiel de l'UE.
3
+
4
+ Author: Dr Hamid MADANI <drmdh@msn.com> -- AGPL-3.0-or-later
5
+
6
+ Le Journal officiel publie des PDF qui ne portent aucune signature : la signature est un fichier XML a cote, dont le
7
+ manifeste (ds:Manifest) nomme chaque document par son empreinte. Ce script VERIFIE une telle signature et en PRODUIT
8
+ de semblables : un seul XML qui scelle plusieurs fichiers (le dossier d'un livrable QATrax, par exemple).
9
+
10
+ xades.py verify <config.json> -> derniere ligne : le rapport JSON
11
+ xades.py sign <config.json> -> ecrit le XML a config["output"]
12
+
13
+ La configuration arrive par FICHIER, jamais par argv : elle porte la phrase du certificat et les identifiants de
14
+ l'horodateur, et une ligne de commande se lit dans la table des processus.
15
+
16
+ Ce que la verification NE FAIT PAS, et dit : elle ne consulte pas les listes de confiance de l'UE ; « qualifie »
17
+ signifie que le certificat SE DECLARE qualifie (QCStatements), pas qu'un registre l'a confirme.
18
+ """
19
+ import asyncio
20
+ import base64
21
+ import hashlib
22
+ import json
23
+ import os
24
+ import re
25
+ import sys
26
+ import uuid
27
+ from datetime import datetime, timezone
28
+ from urllib.parse import quote, unquote
29
+
30
+ from lxml import etree
31
+
32
+ DS = "http://www.w3.org/2000/09/xmldsig#"
33
+ XADES = "http://uri.etsi.org/01903/v1.3.2#"
34
+ XADES141 = "http://uri.etsi.org/01903/v1.4.1#"
35
+ ETSI = "http://uri.etsi.org/01903/"
36
+ NS = {"ds": DS, "xades": XADES, "xades141": XADES141}
37
+
38
+ C14N = {
39
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315": (False, False),
40
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments": (False, True),
41
+ "http://www.w3.org/2001/10/xml-exc-c14n#": (True, False),
42
+ "http://www.w3.org/2001/10/xml-exc-c14n#WithComments": (True, True),
43
+ # C14N 1.1 ne differe de 1.0 que pour xml:id / xml:base herites : traite comme 1.0.
44
+ "http://www.w3.org/2006/12/xml-c14n11": (False, False),
45
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments": (False, True),
46
+ }
47
+ ENVELOPPEE = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
48
+ DIGESTS = {
49
+ "http://www.w3.org/2000/09/xmldsig#sha1": "sha1",
50
+ "http://www.w3.org/2001/04/xmldsig-more#sha224": "sha224",
51
+ "http://www.w3.org/2001/04/xmlenc#sha256": "sha256",
52
+ "http://www.w3.org/2001/04/xmldsig-more#sha384": "sha384",
53
+ "http://www.w3.org/2001/04/xmlenc#sha512": "sha512",
54
+ }
55
+ DIGEST_URI = {v: k for k, v in DIGESTS.items()}
56
+ SIGNATURES = {}
57
+ for h in ("sha1", "sha224", "sha256", "sha384", "sha512"):
58
+ base = "http://www.w3.org/2000/09/xmldsig#" if h == "sha1" else "http://www.w3.org/2001/04/xmldsig-more#"
59
+ SIGNATURES[base + "rsa-" + h] = ("rsa", h)
60
+ SIGNATURES["http://www.w3.org/2001/04/xmldsig-more#ecdsa-" + h] = ("ecdsa", h)
61
+ for h in ("sha256", "sha384", "sha512"):
62
+ SIGNATURES["http://www.w3.org/2007/05/xmldsig-more#" + h + "-rsa-MGF1"] = ("pss", h)
63
+ TYPE_MANIFEST = "http://www.w3.org/2000/09/xmldsig#Manifest"
64
+ TYPE_PROPRIETES = "http://uri.etsi.org/01903#SignedProperties"
65
+
66
+ # OID ETSI des QCStatements (EN 319 412-5), encodes DER : le certificat SE DECLARE qualifie / sur dispositif qualifie.
67
+ QC_CONFORME = bytes.fromhex("060604008e460101")
68
+ QC_SSCD = bytes.fromhex("060604008e460104")
69
+ QC_TYPES = {bytes.fromhex("060704008e46010601"): "esign", bytes.fromhex("060704008e46010602"): "eseal",
70
+ bytes.fromhex("060704008e46010603"): "web"}
71
+
72
+ TAILLE_MAX_XML = 10 * 1024 * 1024
73
+
74
+
75
+ class Refus(Exception):
76
+ """Une cause a montrer telle quelle : jamais de chemin, jamais de secret."""
77
+
78
+
79
+ def erreur(msg):
80
+ raise SystemExit("ERREUR -- " + msg)
81
+
82
+
83
+ def _iso(d):
84
+ if d is None:
85
+ return None
86
+ if d.tzinfo is not None:
87
+ d = d.astimezone(timezone.utc)
88
+ return d.strftime("%Y-%m-%dT%H:%M:%SZ")
89
+
90
+
91
+ def _h(nom, octets):
92
+ return hashlib.new(nom, octets).digest()
93
+
94
+
95
+ def _b64(o):
96
+ return base64.b64encode(o).decode("ascii")
97
+
98
+
99
+ def _texte(el):
100
+ return re.sub(r"\s+", "", el.text or "") if el is not None else ""
101
+
102
+
103
+ def _local(el):
104
+ return etree.QName(el).localname if isinstance(el.tag, str) else ""
105
+
106
+
107
+ def _etsi(el):
108
+ return isinstance(el.tag, str) and (etree.QName(el).namespace or "").startswith(ETSI)
109
+
110
+
111
+ # ── Lecture prudente ────────────────────────────────────────────────────────────────────────────────────────────
112
+ def lire_xml(octets):
113
+ if len(octets) > TAILLE_MAX_XML:
114
+ raise Refus("fichier XML trop volumineux (10 Mo au plus)")
115
+ if re.search(rb"<!DOCTYPE", octets[:4096], re.I) or b"<!ENTITY" in octets:
116
+ raise Refus("XML refusé : une déclaration DOCTYPE ou ENTITY n'a rien à faire dans une signature")
117
+ parseur = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False, dtd_validation=False,
118
+ huge_tree=False, remove_blank_text=False)
119
+ try:
120
+ racine = etree.fromstring(octets, parseur)
121
+ except etree.XMLSyntaxError:
122
+ raise Refus("ce fichier n'est pas un document XML lisible")
123
+ return racine
124
+
125
+
126
+ def table_ids(racine):
127
+ ids = {}
128
+ for el in racine.iter():
129
+ if not isinstance(el.tag, str):
130
+ continue
131
+ for a in ("Id", "ID", "id"):
132
+ v = el.get(a)
133
+ if v is None:
134
+ continue
135
+ if v in ids and ids[v] is not el:
136
+ # Deux elements au meme Id : la porte classique de l'« emballage » de signature (XSW).
137
+ raise Refus(f"XML refusé : l'Id « {v[:60]} » désigne deux éléments")
138
+ ids[v] = el
139
+ return ids
140
+
141
+
142
+ def c14n(el, algo, prefixes=None):
143
+ if algo not in C14N:
144
+ raise Refus(f"canonicalisation non prise en charge : {algo}")
145
+ exclusif, commentaires = C14N[algo]
146
+ return etree.tostring(el, method="c14n", exclusive=exclusif, with_comments=commentaires,
147
+ inclusive_ns_prefixes=prefixes if exclusif else None)
148
+
149
+
150
+ def prefixes_inclus(tr):
151
+ for e in tr:
152
+ if _local(e) == "InclusiveNamespaces":
153
+ return (e.get("PrefixList") or "").split() or None
154
+ return None
155
+
156
+
157
+ # ── Les references ──────────────────────────────────────────────────────────────────────────────────────────────
158
+ def traiter_reference(ref, racine, ids):
159
+ """Les octets que la reference designe, apres ses transformations ; None pour un document exterieur."""
160
+ uri = ref.get("URI")
161
+ transfos = ref.find("ds:Transforms", NS)
162
+ liste = list(transfos) if transfos is not None else []
163
+ if uri is None or uri == "" or uri == "#xpointer(/)":
164
+ noeud = racine
165
+ elif uri.startswith("#"):
166
+ noeud = ids.get(uri[1:])
167
+ if noeud is None:
168
+ raise Refus(f"référence vers un Id absent : {uri[:80]}")
169
+ else:
170
+ if liste:
171
+ raise Refus("transformation sur un document extérieur non prise en charge")
172
+ return None
173
+ algo, prefixes, enveloppee = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", None, False
174
+ for t in liste:
175
+ a = t.get("Algorithm")
176
+ if a == ENVELOPPEE:
177
+ enveloppee = True
178
+ elif a in C14N:
179
+ algo, prefixes = a, prefixes_inclus(t)
180
+ else:
181
+ raise Refus(f"transformation non prise en charge : {a}")
182
+ if enveloppee:
183
+ if noeud is not racine:
184
+ raise Refus("signature enveloppée hors de la racine : non prise en charge")
185
+ copie = etree.fromstring(etree.tostring(racine))
186
+ for s in copie.iter("{%s}Signature" % DS):
187
+ s.getparent().remove(s)
188
+ noeud = copie
189
+ return c14n(noeud, algo, prefixes)
190
+
191
+
192
+ def digest_de(ref):
193
+ dm = ref.find("ds:DigestMethod", NS)
194
+ a = dm.get("Algorithm") if dm is not None else None
195
+ if a not in DIGESTS:
196
+ raise Refus(f"algorithme d'empreinte non pris en charge : {a}")
197
+ return DIGESTS[a], base64.b64decode(_texte(ref.find("ds:DigestValue", NS)))
198
+
199
+
200
+ def nom_uri(uri):
201
+ return unquote(uri or "")
202
+
203
+
204
+ # ── Certificats ─────────────────────────────────────────────────────────────────────────────────────────────────
205
+ def infos_certificat(der):
206
+ from asn1crypto import x509 as ax
207
+ from sign_with_perms import _cert_info
208
+ c = ax.Certificate.load(der)
209
+ info = _cert_info(c) or {}
210
+ qualifie, sscd, types = False, False, []
211
+ for ext in c["tbs_certificate"]["extensions"] or []:
212
+ if ext["extn_id"].dotted == "1.3.6.1.5.5.7.1.3":
213
+ brut = ext["extn_value"].contents
214
+ qualifie = QC_CONFORME in brut
215
+ sscd = QC_SSCD in brut
216
+ types = [n for o, n in QC_TYPES.items() if o in brut]
217
+ info.update({"qualified": qualifie, "qscd": sscd, "qcType": types})
218
+ if info.get("notBefore"):
219
+ info["notBefore"] = _iso(c["tbs_certificate"]["validity"]["not_before"].native)
220
+ info["notAfter"] = _iso(c["tbs_certificate"]["validity"]["not_after"].native)
221
+ return info
222
+
223
+
224
+ def verifier_valeur(der, algo_uri, signee, valeur):
225
+ from cryptography import x509
226
+ from cryptography.exceptions import InvalidSignature
227
+ from cryptography.hazmat.primitives import hashes
228
+ from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
229
+ from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
230
+ if algo_uri not in SIGNATURES:
231
+ raise Refus(f"algorithme de signature non pris en charge : {algo_uri}")
232
+ famille, h = SIGNATURES[algo_uri]
233
+ hachage = getattr(hashes, h.upper())()
234
+ cle = x509.load_der_x509_certificate(der).public_key()
235
+ try:
236
+ if famille == "rsa" and isinstance(cle, rsa.RSAPublicKey):
237
+ cle.verify(valeur, signee, padding.PKCS1v15(), hachage)
238
+ elif famille == "pss" and isinstance(cle, rsa.RSAPublicKey):
239
+ cle.verify(valeur, signee, padding.PSS(mgf=padding.MGF1(hachage), salt_length=hachage.digest_size), hachage)
240
+ elif famille == "ecdsa" and isinstance(cle, ec.EllipticCurvePublicKey):
241
+ n = len(valeur) // 2
242
+ der_sig = encode_dss_signature(int.from_bytes(valeur[:n], "big"), int.from_bytes(valeur[n:], "big"))
243
+ cle.verify(der_sig, signee, ec.ECDSA(hachage))
244
+ else:
245
+ return False
246
+ return True
247
+ except InvalidSignature:
248
+ return False
249
+
250
+
251
+ # ── Horodatages ─────────────────────────────────────────────────────────────────────────────────────────────────
252
+ def lire_jeton(b64, attendu):
253
+ """Le jeton RFC 3161 : ce qu'il dit, et s'il porte sur `attendu` (fonction algo -> empreinte, ou None)."""
254
+ from asn1crypto import cms
255
+ from pyhanko.sign.validation.generic_cms import validate_tst_signed_data
256
+ from pyhanko_certvalidator import ValidationContext
257
+ from sign_with_perms import _jeton
258
+ tok = cms.ContentInfo.load(base64.b64decode(b64))
259
+ info = _jeton(tok) or {}
260
+ sd = tok["content"]
261
+ tst = sd["encap_content_info"]["content"].parsed
262
+ algo = tst["message_imprint"]["hash_algorithm"]["algorithm"].native
263
+ porte = tst["message_imprint"]["hashed_message"].native
264
+ imprint_ok = None
265
+ if attendu is not None:
266
+ try:
267
+ imprint_ok = attendu(algo) == porte
268
+ except Exception:
269
+ imprint_ok = None
270
+ statut = {"intact": None, "valid": None, "trusted": None}
271
+ try:
272
+ st = asyncio.run(validate_tst_signed_data(sd, ValidationContext(allow_fetching=False), lambda a: porte))
273
+ get = (lambda k: st.get(k)) if isinstance(st, dict) else (lambda k: getattr(st, k, None))
274
+ statut = {"intact": bool(get("intact")), "valid": bool(get("valid")), "trusted": bool(get("trusted"))}
275
+ except Exception:
276
+ pass
277
+ info["genTime"] = _iso(tst["gen_time"].native)
278
+ info.update(statut)
279
+ info["imprintOk"] = imprint_ok
280
+ return info
281
+
282
+
283
+ def entree_archive(sig, ids, racine, algo_ts, ts_el, usp):
284
+ """L'entree d'un ArchiveTimeStamp (XAdES 1.4.1, 8.2.1) : references, SignedInfo, SignatureValue, KeyInfo,
285
+ proprietes non signees qui le precedent, ds:Object hors QualifyingProperties."""
286
+ morceaux = []
287
+ si = sig.find("ds:SignedInfo", NS)
288
+ for ref in si.findall("ds:Reference", NS):
289
+ o = traiter_reference(ref, racine, ids)
290
+ if o is None:
291
+ return None # document exterieur : il faudrait ses octets ; on ne recalcule pas
292
+ morceaux.append(o)
293
+ for nom in ("SignedInfo", "SignatureValue", "KeyInfo"):
294
+ e = sig.find("ds:" + nom, NS)
295
+ if e is not None:
296
+ morceaux.append(c14n(e, algo_ts))
297
+ for e in usp:
298
+ if e is ts_el:
299
+ break
300
+ if isinstance(e.tag, str):
301
+ morceaux.append(c14n(e, algo_ts))
302
+ for o in sig.findall("ds:Object", NS):
303
+ if not any(_local(x) == "QualifyingProperties" for x in o):
304
+ morceaux.append(c14n(o, algo_ts))
305
+ return b"".join(morceaux)
306
+
307
+
308
+ # ── Verification ────────────────────────────────────────────────────────────────────────────────────────────────
309
+ def verifier(xml_octets, fichiers):
310
+ racine = lire_xml(xml_octets)
311
+ signatures = [racine] if racine.tag == "{%s}Signature" % DS else list(racine.iter("{%s}Signature" % DS))
312
+ if not signatures:
313
+ raise Refus("aucune signature XML (ds:Signature) dans ce fichier")
314
+ ids = table_ids(racine)
315
+ sig = signatures[0]
316
+ si = sig.find("ds:SignedInfo", NS)
317
+ if si is None:
318
+ raise Refus("signature sans SignedInfo")
319
+ cm = si.find("ds:CanonicalizationMethod", NS)
320
+ algo_c14n = cm.get("Algorithm") if cm is not None else None
321
+ sm = si.find("ds:SignatureMethod", NS)
322
+ algo_sig = sm.get("Algorithm") if sm is not None else None
323
+ valeur_el = sig.find("ds:SignatureValue", NS)
324
+ valeur = base64.b64decode(_texte(valeur_el))
325
+ empreintes = [(f["name"], f["bytes"]) for f in fichiers]
326
+
327
+ # Les certificats : KeyInfo, puis ceux des proprietes non signees ; le signataire est celui que nomme
328
+ # SigningCertificate(V2), s'il y en a un.
329
+ candidats = [base64.b64decode(_texte(e)) for e in sig.iterfind(".//ds:KeyInfo//ds:X509Certificate", NS)]
330
+ candidats += [base64.b64decode(_texte(e)) for e in sig.iter() if _local(e) == "EncapsulatedX509Certificate" and _etsi(e)]
331
+ signataire, signing_cert_ok = (candidats[0] if candidats else None), None
332
+ certs_annonces = [e for e in sig.iter() if _local(e) == "CertDigest" and _etsi(e)
333
+ and _local(e.getparent().getparent()) in ("SigningCertificate", "SigningCertificateV2")]
334
+ if certs_annonces:
335
+ signing_cert_ok = False
336
+ for cd in certs_annonces:
337
+ algo, attendu = digest_de(cd)
338
+ for c in candidats:
339
+ if _h(algo, c) == attendu:
340
+ signataire, signing_cert_ok = c, True
341
+ break
342
+ if signing_cert_ok:
343
+ break
344
+ if signataire is None:
345
+ raise Refus("aucun certificat de signataire dans la signature")
346
+
347
+ references, manifeste, couverts = [], [], {}
348
+ signed_props_ok = None
349
+ for ref in si.findall("ds:Reference", NS):
350
+ uri, typ = ref.get("URI"), ref.get("Type") or ""
351
+ algo, attendu = digest_de(ref)
352
+ cible = ids.get(uri[1:]) if uri and uri.startswith("#") else None
353
+ genre = ("manifest" if typ == TYPE_MANIFEST or (cible is not None and _local(cible) == "Manifest")
354
+ else "signedProperties" if typ == TYPE_PROPRIETES or (cible is not None and _local(cible) == "SignedProperties")
355
+ else "document" if uri and not uri.startswith("#") else "other")
356
+ entree = {"uri": nom_uri(uri) if genre == "document" else uri, "type": genre, "ok": None}
357
+ o = traiter_reference(ref, racine, ids)
358
+ if o is not None:
359
+ entree["ok"] = _h(algo, o) == attendu
360
+ else:
361
+ for nom, octets in empreintes:
362
+ if _h(algo, octets) == attendu:
363
+ entree["ok"] = True
364
+ couverts.setdefault(nom, nom_uri(uri))
365
+ if entree["ok"] is None and any(n == nom_uri(uri) or os.path.basename(n) == os.path.basename(nom_uri(uri)) for n, _ in empreintes):
366
+ entree["ok"] = False # le fichier de ce nom est la, mais ce n'est pas celui qui a ete signe
367
+ if genre == "signedProperties":
368
+ signed_props_ok = entree["ok"]
369
+ if genre == "manifest" and cible is not None:
370
+ for mr in cible.findall("ds:Reference", NS):
371
+ malgo, mattendu = digest_de(mr)
372
+ trouve = None
373
+ for nom, octets in empreintes:
374
+ if _h(malgo, octets) == mattendu:
375
+ trouve = nom
376
+ couverts.setdefault(nom, nom_uri(mr.get("URI")))
377
+ manifeste.append({"uri": nom_uri(mr.get("URI")), "digestAlgorithm": malgo, "matchedFile": trouve})
378
+ references.append(entree)
379
+
380
+ signature_ok = verifier_valeur(signataire, algo_sig, c14n(si, algo_c14n, prefixes_inclus(cm)), valeur)
381
+
382
+ # Proprietes signees et non signees (XAdES).
383
+ st = next((e for e in sig.iter() if _local(e) == "SigningTime" and _etsi(e)), None)
384
+ usp = next((e for e in sig.iter() if _local(e) == "UnsignedSignatureProperties" and _etsi(e)), None)
385
+ horodatages = []
386
+ nb_certs = nb_ocsp = nb_crl = 0
387
+ if usp is not None:
388
+ for e in usp:
389
+ if not isinstance(e.tag, str):
390
+ continue
391
+ n = _local(e)
392
+ nb_certs += sum(1 for x in e.iter() if _local(x) == "EncapsulatedX509Certificate") if n == "CertificateValues" else 0
393
+ nb_ocsp += sum(1 for x in e.iter() if _local(x) == "EncapsulatedOCSPValue") if n == "RevocationValues" else 0
394
+ nb_crl += sum(1 for x in e.iter() if _local(x) == "EncapsulatedCRLValue") if n == "RevocationValues" else 0
395
+ if n not in ("SignatureTimeStamp", "ArchiveTimeStamp", "SigAndRefsTimeStamp", "RefsOnlyTimeStamp"):
396
+ continue
397
+ tcm = e.find("ds:CanonicalizationMethod", NS)
398
+ algo_ts = tcm.get("Algorithm") if tcm is not None else "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
399
+ if n == "SignatureTimeStamp":
400
+ entree = c14n(valeur_el, algo_ts)
401
+ genre = "signature"
402
+ elif n == "ArchiveTimeStamp":
403
+ try:
404
+ entree = entree_archive(sig, ids, racine, algo_ts, e, usp)
405
+ except Refus:
406
+ entree = None
407
+ genre = "archive"
408
+ else:
409
+ entree, genre = None, "other"
410
+ for enc in e:
411
+ if _local(enc) == "EncapsulatedTimeStamp":
412
+ attendu = (lambda a, x=entree: _h(a, x)) if entree is not None else None
413
+ j = lire_jeton(_texte(enc), attendu)
414
+ j["kind"] = genre
415
+ if genre == "archive" and j["imprintOk"] is False:
416
+ # Les variantes de calcul de l'entree d'archive divergent selon les versions de XAdES : un
417
+ # desaccord ici ne prouve pas une alteration. On ne conclut pas.
418
+ j["imprintOk"] = None
419
+ horodatages.append(j)
420
+
421
+ xades = signed_props_ok is not None
422
+ if not xades:
423
+ niveau = "XMLDSig"
424
+ elif any(t["kind"] == "archive" for t in horodatages):
425
+ niveau = "XAdES-B-LTA"
426
+ elif nb_certs and (nb_ocsp or nb_crl):
427
+ niveau = "XAdES-B-LT"
428
+ elif any(t["kind"] == "signature" for t in horodatages):
429
+ niveau = "XAdES-B-T"
430
+ else:
431
+ niveau = "XAdES-B-B"
432
+
433
+ documents = []
434
+ for nom, octets in empreintes:
435
+ documents.append({"name": nom, "bytes": len(octets), "sha256": hashlib.sha256(octets).hexdigest(),
436
+ "covered": nom in couverts, "coveredAs": couverts.get(nom)})
437
+ return {
438
+ "format": "xades" if xades else "xmldsig",
439
+ "level": niveau,
440
+ "signatures": len(signatures),
441
+ "signatureValid": signature_ok,
442
+ "signatureAlgorithm": algo_sig,
443
+ "canonicalization": algo_c14n,
444
+ "references": references,
445
+ "signedPropertiesOk": signed_props_ok,
446
+ "signingCertificateOk": signing_cert_ok,
447
+ "signerCertificate": infos_certificat(signataire),
448
+ "signingTime": _texte(st) or None,
449
+ "timestamps": horodatages,
450
+ "manifest": manifeste,
451
+ "documents": documents,
452
+ "longTerm": {"certificates": nb_certs, "ocsp": nb_ocsp, "crl": nb_crl},
453
+ }
454
+
455
+
456
+ # ── Signature ───────────────────────────────────────────────────────────────────────────────────────────────────
457
+ def E(ns, nom, parent=None, texte=None, **attrs):
458
+ el = etree.SubElement(parent, "{%s}%s" % (ns, nom), nsmap=None) if parent is not None else etree.Element("{%s}%s" % (ns, nom), nsmap={"ds": DS})
459
+ for k, v in attrs.items():
460
+ el.set(k, v)
461
+ if texte is not None:
462
+ el.text = texte
463
+ return el
464
+
465
+
466
+ def signer(cfg):
467
+ from cryptography.hazmat.primitives import hashes, serialization
468
+ from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
469
+ from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
470
+ from cryptography.hazmat.primitives.serialization import pkcs12
471
+ from asn1crypto import cms, x509 as ax
472
+
473
+ fichiers = cfg["files"]
474
+ if not fichiers:
475
+ erreur("aucun fichier à signer")
476
+ noms = [f["name"] for f in fichiers]
477
+ if len(set(noms)) != len(noms):
478
+ erreur("deux fichiers portent le même nom : un manifeste les confondrait (nom en double)")
479
+ with open(cfg["p12"], "rb") as fp:
480
+ donnees = fp.read()
481
+ with open(cfg["passFile"], "r", encoding="utf-8") as fp:
482
+ phrase = fp.read()
483
+ try:
484
+ cle, cert, autres = pkcs12.load_key_and_certificates(donnees, phrase.encode("utf-8") if phrase else None)
485
+ except Exception:
486
+ erreur("certificat PKCS#12 illisible, ou phrase secrète incorrecte")
487
+ if cle is None or cert is None:
488
+ erreur("certificat PKCS#12 sans clé privée ou sans certificat")
489
+ if isinstance(cle, rsa.RSAPrivateKey):
490
+ algo_sig = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
491
+ signe = lambda o: cle.sign(o, padding.PKCS1v15(), hashes.SHA256())
492
+ elif isinstance(cle, ec.EllipticCurvePrivateKey):
493
+ h = {256: "sha256", 384: "sha384", 521: "sha512"}.get(cle.curve.key_size, "sha256")
494
+ algo_sig = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-" + h
495
+ taille = (cle.curve.key_size + 7) // 8
496
+ def signe(o):
497
+ r, s = decode_dss_signature(cle.sign(o, ec.ECDSA(getattr(hashes, h.upper())())))
498
+ return r.to_bytes(taille, "big") + s.to_bytes(taille, "big")
499
+ else:
500
+ erreur("type de clé non pris en charge (RSA ou EC attendus)")
501
+
502
+ EXC = "http://www.w3.org/2001/10/xml-exc-c14n#"
503
+ der = cert.public_bytes(serialization.Encoding.DER)
504
+ sid = "id-" + uuid.uuid4().hex
505
+ id_manifeste, id_props = "manifest-" + sid, "xades-" + sid
506
+ racine = E(DS, "Signature", Id=sid)
507
+ si = E(DS, "SignedInfo", racine)
508
+ E(DS, "CanonicalizationMethod", si, Algorithm=EXC)
509
+ E(DS, "SignatureMethod", si, Algorithm=algo_sig)
510
+ refs = []
511
+ for uri, typ, rid in ((id_manifeste, TYPE_MANIFEST, "r-" + sid + "-1"), (id_props, TYPE_PROPRIETES, None)):
512
+ r = E(DS, "Reference", si, **({"Id": rid} if rid else {}), Type=typ, URI="#" + uri)
513
+ tr = E(DS, "Transforms", r)
514
+ E(DS, "Transform", tr, Algorithm=EXC)
515
+ E(DS, "DigestMethod", r, Algorithm=DIGEST_URI["sha256"])
516
+ refs.append((r, E(DS, "DigestValue", r), uri))
517
+ valeur_el = E(DS, "SignatureValue", racine, Id="value-" + sid)
518
+ ki = E(DS, "KeyInfo", racine)
519
+ xd = E(DS, "X509Data", ki)
520
+ E(DS, "X509Certificate", xd, _b64(der))
521
+ for a in autres or []:
522
+ E(DS, "X509Certificate", xd, _b64(a.public_bytes(serialization.Encoding.DER)))
523
+
524
+ obj = E(DS, "Object", racine)
525
+ man = E(DS, "Manifest", obj, Id=id_manifeste)
526
+ for f in fichiers:
527
+ with open(f["path"], "rb") as fp:
528
+ octets = fp.read()
529
+ mr = E(DS, "Reference", man, URI=quote(f["name"], safe="-._~"))
530
+ E(DS, "DigestMethod", mr, Algorithm=DIGEST_URI["sha512"])
531
+ E(DS, "DigestValue", mr, _b64(_h("sha512", octets)))
532
+
533
+ obj2 = E(DS, "Object", racine)
534
+ qp = etree.SubElement(obj2, "{%s}QualifyingProperties" % XADES, nsmap={"xades": XADES})
535
+ qp.set("Target", "#" + sid)
536
+ sp = etree.SubElement(qp, "{%s}SignedProperties" % XADES)
537
+ sp.set("Id", id_props)
538
+ ssp = etree.SubElement(sp, "{%s}SignedSignatureProperties" % XADES)
539
+ etree.SubElement(ssp, "{%s}SigningTime" % XADES).text = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
540
+ sc = etree.SubElement(ssp, "{%s}SigningCertificateV2" % XADES)
541
+ ce = etree.SubElement(sc, "{%s}Cert" % XADES)
542
+ cd = etree.SubElement(ce, "{%s}CertDigest" % XADES)
543
+ E(DS, "DigestMethod", cd, Algorithm=DIGEST_URI["sha512"])
544
+ E(DS, "DigestValue", cd, _b64(_h("sha512", der)))
545
+ ac = ax.Certificate.load(der)
546
+ issuer_serial = cms.IssuerSerial({"issuer": [ax.GeneralName({"directory_name": ac.issuer})], "serial": ac.serial_number})
547
+ etree.SubElement(ce, "{%s}IssuerSerialV2" % XADES).text = _b64(issuer_serial.dump())
548
+ sdop = etree.SubElement(sp, "{%s}SignedDataObjectProperties" % XADES)
549
+ dof = etree.SubElement(sdop, "{%s}DataObjectFormat" % XADES)
550
+ dof.set("ObjectReference", "#r-" + sid + "-1")
551
+ etree.SubElement(dof, "{%s}MimeType" % XADES).text = "text/xml"
552
+
553
+ # Les empreintes se calculent sur l'arbre FINAL (les espaces de noms en portee comptent), puis on signe.
554
+ for r, dv, uri in refs:
555
+ cible = man if uri == id_manifeste else sp
556
+ dv.text = _b64(_h("sha256", c14n(cible, EXC)))
557
+ valeur_el.text = _b64(signe(c14n(si, EXC)))
558
+
559
+ url = cfg.get("timestamp_url")
560
+ if url:
561
+ from pyhanko.sign import timestamps
562
+ from sign_with_perms import _cause_horodatage
563
+ auth_cfg = cfg.get("timestamp_auth")
564
+ auth = (auth_cfg["user"], auth_cfg.get("password", "")) if auth_cfg else None
565
+ horodateur = timestamps.HTTPTimeStamper(url=url, headers=cfg.get("timestamp_headers") or None, auth=auth,
566
+ timeout=cfg.get("timestamp_timeout") or 10)
567
+ try:
568
+ jeton = asyncio.run(horodateur.async_timestamp(_h("sha256", c14n(valeur_el, EXC)), "sha256"))
569
+ except Exception as e:
570
+ erreur(f"horodatage impossible ({url}) : {_cause_horodatage(e)}. Aucune signature n'a été produite : "
571
+ "une date non attestée vaut moins que pas de date.")
572
+ up = etree.SubElement(qp, "{%s}UnsignedProperties" % XADES)
573
+ usp = etree.SubElement(up, "{%s}UnsignedSignatureProperties" % XADES)
574
+ sts = etree.SubElement(usp, "{%s}SignatureTimeStamp" % XADES)
575
+ sts.set("Id", "ts-" + sid)
576
+ E(DS, "CanonicalizationMethod", sts, Algorithm=EXC)
577
+ etree.SubElement(sts, "{%s}EncapsulatedTimeStamp" % XADES).text = _b64(jeton.dump())
578
+
579
+ with open(cfg["output"], "wb") as fp:
580
+ fp.write(etree.tostring(racine, xml_declaration=True, encoding="UTF-8", standalone=False))
581
+ print("OK -- signature séparée écrite", flush=True)
582
+
583
+
584
+ if __name__ == "__main__":
585
+ if len(sys.argv) != 3 or sys.argv[1] not in ("verify", "sign"):
586
+ erreur("usage : xades.py verify|sign <config.json>")
587
+ with open(sys.argv[2], "r", encoding="utf-8") as fp:
588
+ conf = json.load(fp)
589
+ if sys.argv[1] == "sign":
590
+ signer(conf)
591
+ else:
592
+ try:
593
+ with open(conf["xml"], "rb") as fp:
594
+ xml = fp.read()
595
+ fs = []
596
+ for f in conf.get("files", []):
597
+ with open(f["path"], "rb") as fp:
598
+ fs.append({"name": f["name"], "bytes": fp.read()})
599
+ print(json.dumps(verifier(xml, fs), default=str, ensure_ascii=False))
600
+ except Refus as r:
601
+ erreur(str(r))