@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 +45 -0
- package/README.md +28 -0
- package/dist/detached.d.ts +18 -0
- package/dist/detached.d.ts.map +1 -0
- package/dist/detached.js +139 -0
- package/dist/detached.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +11 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +39 -1
- package/dist/verify.js.map +1 -1
- package/llms.txt +12 -0
- package/package.json +3 -2
- package/python/xades.py +601 -0
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"}
|
package/dist/detached.js
ADDED
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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
|
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;
|
|
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.
|
|
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",
|
package/python/xades.py
ADDED
|
@@ -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))
|