@mostajs/pdf-sign 0.1.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 +18 -0
- package/README.md +101 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +78 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/engines.d.ts +11 -0
- package/dist/engines.d.ts.map +1 -0
- package/dist/engines.js +39 -0
- package/dist/engines.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/node-signer.d.ts +4 -0
- package/dist/node-signer.d.ts.map +1 -0
- package/dist/node-signer.js +35 -0
- package/dist/node-signer.js.map +1 -0
- package/dist/pyhanko-signer.d.ts +4 -0
- package/dist/pyhanko-signer.d.ts.map +1 -0
- package/dist/pyhanko-signer.js +59 -0
- package/dist/pyhanko-signer.js.map +1 -0
- package/dist/sign.d.ts +13 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +35 -0
- package/dist/sign.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +36 -0
- package/dist/verify.js.map +1 -0
- package/examples/sign-basic.mjs +28 -0
- package/examples/sign-restricted.mjs +33 -0
- package/examples/sign-stamped.mjs +28 -0
- package/llms.txt +57 -0
- package/package.json +60 -0
- package/python/requirements.txt +3 -0
- package/python/setup-venv.sh +9 -0
- package/python/sign_with_perms.py +173 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog — @mostajs/pdf-sign
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
|
|
5
|
+
Le format suit [Keep a Changelog](https://keepachangelog.com/fr/) ; versionnage [SemVer](https://semver.org/lang/fr/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-07-01
|
|
8
|
+
|
|
9
|
+
### Ajouté
|
|
10
|
+
|
|
11
|
+
- Moteur **hybride** de signature PDF : cœur Node (PKCS#7 détaché via `@signpdf`) + bascule pyhanko (PAdES visible + permissions AES-256).
|
|
12
|
+
- API : `signPdf`, `verifyPdf`, `detectEngines`, `resolvePython`, `pyhankoAvailable`, `loadPdfSignConfig`.
|
|
13
|
+
- CLI `mosta-pdf-sign` : `sign`, `verify`, `engines`.
|
|
14
|
+
- Configuration 100 % `.env` via `@mostajs/config` (préfixe `PDF_SIGN_*`).
|
|
15
|
+
- Script Python embarqué `python/sign_with_perms.py` (portage générique et paramétré du pipeline `pyhanko + pikepdf`), + `setup-venv.sh` et `requirements.txt`.
|
|
16
|
+
- Cachet visible « image de fond + texte » (logo/tampon) via `field.stampImage` / `PDF_SIGN_STAMP_IMAGE` (moteur pyhanko, `stamp.TextStampStyle`).
|
|
17
|
+
- Vérification : validation cryptographique pyhanko si disponible (authentification automatique du mot de passe utilisateur vide sur PDF chiffré, nom du signataire lisible), sinon détection Node (`/ByteRange`).
|
|
18
|
+
- Tests `@mostajs/mjs-unit` et exemples `examples/*.mjs`.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @mostajs/pdf-sign
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Licence** : AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
Signature numérique de PDF pour l'écosystème `@mostajs/*`. Moteur **hybride** :
|
|
7
|
+
|
|
8
|
+
- **cœur Node** — signature PKCS#7 détachée via [`@signpdf`](https://github.com/vbuch/node-signpdf), cachet invisible, **aucune dépendance Python** ;
|
|
9
|
+
- **bascule pyhanko** — signature **PAdES** visible + **permissions restrictives** (chiffrement AES-256, R=6) quand un venv Python est disponible.
|
|
10
|
+
|
|
11
|
+
Configuration **100 % `.env`** (via `@mostajs/config`), API + CLI. Portage générique et paramétré du pipeline `pyhanko + pikepdf` éprouvé.
|
|
12
|
+
|
|
13
|
+
## Pourquoi — valeur probante (SIH & documents officiels)
|
|
14
|
+
|
|
15
|
+
La signature électronique de PDF apporte la **preuve médico-légale** et la **valeur probante** exigées par l'interopérabilité et la sécurité d'un Système d'Information Hospitalier :
|
|
16
|
+
|
|
17
|
+
| Domaine | Documents |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Clinique | CRH, CRO, comptes rendus d'imagerie / biologie signés par le praticien |
|
|
20
|
+
| Administratif | certificats de séjour, ordonnances, dossiers de sortie |
|
|
21
|
+
| Financier / CNAS-CASNOS | factures, bordereaux de remboursement, prises en charge |
|
|
22
|
+
|
|
23
|
+
Le module reste **générique** : il ne connaît aucun métier. La sémantique (quel document, quel signataire) est portée par l'appelant.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @mostajs/pdf-sign
|
|
29
|
+
# Moteur pyhanko (optionnel : PAdES + permissions) :
|
|
30
|
+
bash node_modules/@mostajs/pdf-sign/python/setup-venv.sh
|
|
31
|
+
export PDF_SIGN_PYTHON="$(pwd)/node_modules/@mostajs/pdf-sign/python/.venv/bin/python"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { signPdf, verifyPdf, detectEngines } from "@mostajs/pdf-sign";
|
|
38
|
+
|
|
39
|
+
// Signature (auto : pyhanko si dispo, sinon Node)
|
|
40
|
+
await signPdf({
|
|
41
|
+
input: "crh.pdf",
|
|
42
|
+
output: "crh.signe.pdf",
|
|
43
|
+
p12: "praticien.p12",
|
|
44
|
+
passphrase: process.env.P12_PASS,
|
|
45
|
+
meta: { reason: "Compte rendu d'hospitalisation", name: "Dr H. Madani", location: "Alger, DZ" },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Signature restreinte (PAdES + chiffrement AES-256 — exige pyhanko)
|
|
49
|
+
await signPdf({
|
|
50
|
+
input: "facture-cnas.pdf",
|
|
51
|
+
output: "facture-cnas.signe.pdf",
|
|
52
|
+
p12: "etablissement.p12",
|
|
53
|
+
passphrase: process.env.P12_PASS,
|
|
54
|
+
permissions: { extract: false, modifyOther: false, printHighres: true },
|
|
55
|
+
field: { page: 1, box: [320, 40, 575, 140], stampImage: "logo-chu.png" }, // cachet visible image + texte
|
|
56
|
+
engine: "pyhanko",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Vérification
|
|
60
|
+
const r = verifyPdf({ input: "crh.signe.pdf", trustPem: "ac-racine.pem" });
|
|
61
|
+
console.log(r.signed, r.signatures);
|
|
62
|
+
|
|
63
|
+
// État des moteurs
|
|
64
|
+
console.log(detectEngines());
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret \
|
|
71
|
+
--reason "Ordonnance" --name "Dr H. Madani" --location "Alger, DZ"
|
|
72
|
+
mosta-pdf-sign sign in.pdf out.pdf --p12 cert.p12 --pass secret --restrict --engine pyhanko
|
|
73
|
+
mosta-pdf-sign verify out.pdf --trust ac-racine.pem
|
|
74
|
+
mosta-pdf-sign engines
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration (`.env`)
|
|
78
|
+
|
|
79
|
+
| Clé | Rôle |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `PDF_SIGN_P12` / `PDF_SIGN_PASSPHRASE` / `PDF_SIGN_PASSPHRASE_FILE` | certificat PKCS#12 et secret |
|
|
82
|
+
| `PDF_SIGN_ENGINE` | `auto` (défaut) · `node` · `pyhanko` |
|
|
83
|
+
| `PDF_SIGN_PYTHON` | binaire Python du venv pyhanko |
|
|
84
|
+
| `PDF_SIGN_REASON` / `_LOCATION` / `_NAME` / `_CONTACT` | métadonnées du cachet |
|
|
85
|
+
| `PDF_SIGN_FIELD_PAGE` / `PDF_SIGN_FIELD_BOX` | page (0-based) et boîte `x1,y1,x2,y2` (points, origine bas-gauche) |
|
|
86
|
+
| `PDF_SIGN_STAMP_IMAGE` | image de fond du cachet visible (logo/tampon derrière le texte ; moteur pyhanko) |
|
|
87
|
+
| `PDF_SIGN_RESTRICT` | `true` → permissions restrictives (pyhanko) |
|
|
88
|
+
| `PDF_SIGN_TRUST_PEM` | certificat de confiance pour la vérification |
|
|
89
|
+
|
|
90
|
+
## Moteurs
|
|
91
|
+
|
|
92
|
+
| Moteur | PKCS#7 | PAdES visible | Permissions AES-256 | Dépendance |
|
|
93
|
+
|---|:---:|:---:|:---:|---|
|
|
94
|
+
| `node` | ✅ | ❌ | ❌ | aucune (JS pur) |
|
|
95
|
+
| `pyhanko` | ✅ | ✅ | ✅ | venv `pyhanko>=0.25`, `pikepdf>=8` |
|
|
96
|
+
|
|
97
|
+
Le cœur Node **lève une erreur** si des permissions sont demandées : le PAdES et le chiffrement exigent pyhanko.
|
|
98
|
+
|
|
99
|
+
## Voir aussi
|
|
100
|
+
|
|
101
|
+
- `@mostajs/pdf-sign-ui` — couche de présentation (formulaire de signature, vérification, headless + composants).
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @mostajs/pdf-sign — CLI. `mosta-pdf-sign sign|verify|engines`.
|
|
4
|
+
*
|
|
5
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
6
|
+
* @license AGPL-3.0-or-later
|
|
7
|
+
*/
|
|
8
|
+
import { signPdf } from "./sign.js";
|
|
9
|
+
import { verifyPdf } from "./verify.js";
|
|
10
|
+
import { detectEngines } from "./engines.js";
|
|
11
|
+
function opt(argv, name) {
|
|
12
|
+
const i = argv.indexOf(`--${name}`);
|
|
13
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
14
|
+
}
|
|
15
|
+
function flag(argv, name) {
|
|
16
|
+
return argv.includes(`--${name}`);
|
|
17
|
+
}
|
|
18
|
+
const HELP = `mosta-pdf-sign — signature numérique de PDF (moteur hybride Node + pyhanko)
|
|
19
|
+
|
|
20
|
+
mosta-pdf-sign sign <in.pdf> [out.pdf] [options]
|
|
21
|
+
--p12 <fichier> certificat PKCS#12 (ou PDF_SIGN_P12)
|
|
22
|
+
--pass <str> | --pass-file <fichier>
|
|
23
|
+
--engine auto|node|pyhanko (défaut auto)
|
|
24
|
+
--restrict permissions restrictives (PAdES pyhanko requis)
|
|
25
|
+
--reason --name --location --contact
|
|
26
|
+
mosta-pdf-sign verify <pdf> [--trust cert.pem] [--engine ...]
|
|
27
|
+
mosta-pdf-sign engines état des moteurs disponibles
|
|
28
|
+
`;
|
|
29
|
+
async function main() {
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
const cmd = argv[0];
|
|
32
|
+
const rest = argv.slice(1).filter((a) => !a.startsWith("--"));
|
|
33
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
34
|
+
process.stdout.write(HELP);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (cmd === "engines") {
|
|
38
|
+
console.log(JSON.stringify(detectEngines(), null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (cmd === "sign") {
|
|
42
|
+
const input = rest[0];
|
|
43
|
+
const output = rest[1];
|
|
44
|
+
if (!input) {
|
|
45
|
+
process.stderr.write("Entrée PDF manquante.\n" + HELP);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const res = await signPdf({
|
|
49
|
+
input,
|
|
50
|
+
output,
|
|
51
|
+
p12: opt(argv, "p12"),
|
|
52
|
+
passphrase: opt(argv, "pass"),
|
|
53
|
+
passphraseFile: opt(argv, "pass-file"),
|
|
54
|
+
engine: opt(argv, "engine"),
|
|
55
|
+
permissions: flag(argv, "restrict") ? {} : undefined,
|
|
56
|
+
meta: {
|
|
57
|
+
reason: opt(argv, "reason"), name: opt(argv, "name"),
|
|
58
|
+
location: opt(argv, "location"), contact: opt(argv, "contact"),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
console.log(`OK — signé (${res.engine}) : ${res.output ?? "<buffer>"}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (cmd === "verify") {
|
|
65
|
+
const input = rest[0];
|
|
66
|
+
if (!input) {
|
|
67
|
+
process.stderr.write("PDF à vérifier manquant.\n");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const res = verifyPdf({ input, trustPem: opt(argv, "trust"), engine: opt(argv, "engine") });
|
|
71
|
+
console.log(JSON.stringify(res, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
process.stderr.write(`Commande inconnue : ${cmd}\n` + HELP);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
main().catch((e) => { process.stderr.write(String(e?.message ?? e) + "\n"); process.exit(1); });
|
|
78
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SignEngine, SignField, SignMeta } from "./types.js";
|
|
2
|
+
export interface PdfSignConfig {
|
|
3
|
+
p12?: string;
|
|
4
|
+
passphrase?: string;
|
|
5
|
+
passphraseFile?: string;
|
|
6
|
+
engine: SignEngine;
|
|
7
|
+
python?: string;
|
|
8
|
+
meta: SignMeta;
|
|
9
|
+
field: SignField;
|
|
10
|
+
restrict: boolean;
|
|
11
|
+
trustPem?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadPdfSignConfig(): PdfSignConfig;
|
|
14
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAOD,wBAAgB,iBAAiB,IAAI,aAAa,CAwBjD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — configuration via @mostajs/config (.env). Rien en dur.
|
|
3
|
+
*
|
|
4
|
+
* Clés :
|
|
5
|
+
* PDF_SIGN_P12 / PDF_SIGN_PASSPHRASE / PDF_SIGN_PASSPHRASE_FILE
|
|
6
|
+
* PDF_SIGN_ENGINE (auto|node|pyhanko) · PDF_SIGN_PYTHON (bin du venv pyhanko)
|
|
7
|
+
* PDF_SIGN_REASON / PDF_SIGN_LOCATION / PDF_SIGN_NAME / PDF_SIGN_CONTACT
|
|
8
|
+
* PDF_SIGN_FIELD_PAGE / PDF_SIGN_FIELD_BOX ("x1,y1,x2,y2") · PDF_SIGN_STAMP_IMAGE (logo du cachet) · PDF_SIGN_RESTRICT (bool)
|
|
9
|
+
* PDF_SIGN_TRUST_PEM (validation)
|
|
10
|
+
*
|
|
11
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
12
|
+
* @license AGPL-3.0-or-later
|
|
13
|
+
*/
|
|
14
|
+
import { getEnv, getEnvBool } from "@mostajs/config";
|
|
15
|
+
function parseBox(s) {
|
|
16
|
+
const n = s.split(",").map((x) => Number(x.trim()));
|
|
17
|
+
return n.length === 4 && n.every((v) => !Number.isNaN(v)) ? n : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function loadPdfSignConfig() {
|
|
20
|
+
const engine = getEnv("PDF_SIGN_ENGINE", "auto");
|
|
21
|
+
const box = getEnv("PDF_SIGN_FIELD_BOX", "");
|
|
22
|
+
const page = getEnv("PDF_SIGN_FIELD_PAGE", "");
|
|
23
|
+
return {
|
|
24
|
+
p12: getEnv("PDF_SIGN_P12", "") || undefined,
|
|
25
|
+
passphrase: getEnv("PDF_SIGN_PASSPHRASE", "") || undefined,
|
|
26
|
+
passphraseFile: getEnv("PDF_SIGN_PASSPHRASE_FILE", "") || undefined,
|
|
27
|
+
engine: (["auto", "node", "pyhanko"].includes(engine) ? engine : "auto"),
|
|
28
|
+
python: getEnv("PDF_SIGN_PYTHON", "") || undefined,
|
|
29
|
+
meta: {
|
|
30
|
+
reason: getEnv("PDF_SIGN_REASON", "") || undefined,
|
|
31
|
+
location: getEnv("PDF_SIGN_LOCATION", "") || undefined,
|
|
32
|
+
name: getEnv("PDF_SIGN_NAME", "") || undefined,
|
|
33
|
+
contact: getEnv("PDF_SIGN_CONTACT", "") || undefined,
|
|
34
|
+
},
|
|
35
|
+
field: {
|
|
36
|
+
page: page ? Number(page) : undefined,
|
|
37
|
+
box: box ? parseBox(box) : undefined,
|
|
38
|
+
stampImage: getEnv("PDF_SIGN_STAMP_IMAGE", "") || undefined,
|
|
39
|
+
},
|
|
40
|
+
restrict: getEnvBool("PDF_SIGN_RESTRICT", false),
|
|
41
|
+
trustPem: getEnv("PDF_SIGN_TRUST_PEM", "") || undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAerD,SAAS,QAAQ,CAAC,CAAS;IACzB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAsC,CAAC,CAAC,CAAC,SAAS,CAAC;AAClH,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;IAC/C,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,cAAc,EAAE,EAAE,CAAC,IAAI,SAAS;QAC5C,UAAU,EAAE,MAAM,CAAC,qBAAqB,EAAE,EAAE,CAAC,IAAI,SAAS;QAC1D,cAAc,EAAE,MAAM,CAAC,0BAA0B,EAAE,EAAE,CAAC,IAAI,SAAS;QACnE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAe;QACtF,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,EAAE,CAAC,IAAI,SAAS;QAClD,IAAI,EAAE;YACJ,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,EAAE,CAAC,IAAI,SAAS;YAClD,QAAQ,EAAE,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,IAAI,SAAS;YACtD,IAAI,EAAE,MAAM,CAAC,eAAe,EAAE,EAAE,CAAC,IAAI,SAAS;YAC9C,OAAO,EAAE,MAAM,CAAC,kBAAkB,EAAE,EAAE,CAAC,IAAI,SAAS;SACrD;QACD,KAAK,EAAE;YACL,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;YACrC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;YACpC,UAAU,EAAE,MAAM,CAAC,sBAAsB,EAAE,EAAE,CAAC,IAAI,SAAS;SAC5D;QACD,QAAQ,EAAE,UAAU,CAAC,mBAAmB,EAAE,KAAK,CAAC;QAChD,QAAQ,EAAE,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,IAAI,SAAS;KACxD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EngineAvailability } from "./types.js";
|
|
2
|
+
/** Répertoire des scripts Python embarqués (proven pipeline). */
|
|
3
|
+
export declare const PYTHON_DIR: string;
|
|
4
|
+
export declare const SIGN_PY: string;
|
|
5
|
+
/** Résout le binaire Python : explicite > venv local du module > python3. */
|
|
6
|
+
export declare function resolvePython(explicit?: string): string;
|
|
7
|
+
/** Vrai si ce Python importe pyhanko + pikepdf (moteur PAdES + permissions). */
|
|
8
|
+
export declare function pyhankoAvailable(python: string): boolean;
|
|
9
|
+
/** État des moteurs disponibles. */
|
|
10
|
+
export declare function detectEngines(explicitPython?: string): EngineAvailability;
|
|
11
|
+
//# sourceMappingURL=engines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engines.d.ts","sourceRoot":"","sources":["../src/engines.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGrD,iEAAiE;AACjE,eAAO,MAAM,UAAU,QAAgC,CAAC;AACxD,eAAO,MAAM,OAAO,QAA4C,CAAC;AAGjE,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAOxD;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAGzE"}
|
package/dist/engines.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — détection des moteurs (Node toujours, pyhanko si venv/deps).
|
|
3
|
+
*
|
|
4
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
* @license AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // dist/
|
|
12
|
+
/** Répertoire des scripts Python embarqués (proven pipeline). */
|
|
13
|
+
export const PYTHON_DIR = resolve(HERE, "..", "python");
|
|
14
|
+
export const SIGN_PY = resolve(PYTHON_DIR, "sign_with_perms.py");
|
|
15
|
+
const VENV_PY = resolve(PYTHON_DIR, ".venv", "bin", "python");
|
|
16
|
+
/** Résout le binaire Python : explicite > venv local du module > python3. */
|
|
17
|
+
export function resolvePython(explicit) {
|
|
18
|
+
if (explicit && existsSync(explicit))
|
|
19
|
+
return explicit;
|
|
20
|
+
if (existsSync(VENV_PY))
|
|
21
|
+
return VENV_PY;
|
|
22
|
+
return "python3";
|
|
23
|
+
}
|
|
24
|
+
/** Vrai si ce Python importe pyhanko + pikepdf (moteur PAdES + permissions). */
|
|
25
|
+
export function pyhankoAvailable(python) {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync(python, ["-c", "import pyhanko, pikepdf"], { stdio: "ignore" });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** État des moteurs disponibles. */
|
|
35
|
+
export function detectEngines(explicitPython) {
|
|
36
|
+
const python = resolvePython(explicitPython);
|
|
37
|
+
return { node: true, pyhanko: pyhankoAvailable(python), python };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=engines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engines.js","sourceRoot":"","sources":["../src/engines.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ;AAC9D,iEAAiE;AACjE,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AACxD,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;AACjE,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAE9D,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,QAAiB;IAC7C,IAAI,QAAQ,IAAI,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IACtD,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,IAAI,CAAC;QACH,YAAY,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,yBAAyB,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7E,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,aAAa,CAAC,cAAuB;IACnD,MAAM,MAAM,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;IAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;AACnE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — signature numérique de PDF, moteur hybride (Node + pyhanko).
|
|
3
|
+
*
|
|
4
|
+
* Cœur Node (PKCS#7 via @signpdf) + bascule pyhanko (PAdES + permissions AES-256) si
|
|
5
|
+
* disponible. Réutilise le pipeline éprouvé pyhanko+pikepdf. Config 100% `.env`.
|
|
6
|
+
*
|
|
7
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
8
|
+
* @license AGPL-3.0-or-later
|
|
9
|
+
*/
|
|
10
|
+
export { signPdf } from "./sign.js";
|
|
11
|
+
export { verifyPdf } from "./verify.js";
|
|
12
|
+
export { detectEngines, resolvePython, pyhankoAvailable } from "./engines.js";
|
|
13
|
+
export { loadPdfSignConfig, type PdfSignConfig } from "./config.js";
|
|
14
|
+
export type { SignOptions, SignResult, SignMeta, SignField, SignPermissions, SignEngine, VerifyOptions, VerifyResult, SignatureInfo, EngineAvailability, } from "./types.js";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,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,GAC/D,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — signature numérique de PDF, moteur hybride (Node + pyhanko).
|
|
3
|
+
*
|
|
4
|
+
* Cœur Node (PKCS#7 via @signpdf) + bascule pyhanko (PAdES + permissions AES-256) si
|
|
5
|
+
* disponible. Réutilise le pipeline éprouvé pyhanko+pikepdf. Config 100% `.env`.
|
|
6
|
+
*
|
|
7
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
8
|
+
* @license AGPL-3.0-or-later
|
|
9
|
+
*/
|
|
10
|
+
export { signPdf } from "./sign.js";
|
|
11
|
+
export { verifyPdf } from "./verify.js";
|
|
12
|
+
export { detectEngines, resolvePython, pyhankoAvailable } from "./engines.js";
|
|
13
|
+
export { loadPdfSignConfig } from "./config.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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,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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-signer.d.ts","sourceRoot":"","sources":["../src/node-signer.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D,iEAAiE;AACjE,wBAAsB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuBrE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — cœur Node (signature PKCS#7 détachée via @signpdf).
|
|
3
|
+
*
|
|
4
|
+
* Ne gère PAS les permissions/chiffrement ni le PAdES/LTV : pour cela, moteur pyhanko.
|
|
5
|
+
*
|
|
6
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
7
|
+
* @license AGPL-3.0-or-later
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { SignPdf } from "@signpdf/signpdf";
|
|
11
|
+
import { P12Signer } from "@signpdf/signer-p12";
|
|
12
|
+
import { plainAddPlaceholder } from "@signpdf/placeholder-plain";
|
|
13
|
+
/** Signe un PDF côté Node (PKCS#7 détaché, cachet invisible). */
|
|
14
|
+
export async function signNode(opts) {
|
|
15
|
+
if (opts.permissions) {
|
|
16
|
+
throw new Error("Le cœur Node ne gère pas les permissions/PAdES — utilisez engine:'pyhanko' (python/setup-venv.sh).");
|
|
17
|
+
}
|
|
18
|
+
const pdfBuffer = typeof opts.input === "string" ? readFileSync(opts.input) : Buffer.from(opts.input);
|
|
19
|
+
const p12Buffer = typeof opts.p12 === "string" ? readFileSync(opts.p12) : Buffer.from(opts.p12);
|
|
20
|
+
const withPlaceholder = plainAddPlaceholder({
|
|
21
|
+
pdfBuffer,
|
|
22
|
+
reason: opts.meta?.reason ?? "Signature",
|
|
23
|
+
contactInfo: opts.meta?.contact ?? "",
|
|
24
|
+
name: opts.meta?.name ?? "",
|
|
25
|
+
location: opts.meta?.location ?? "",
|
|
26
|
+
});
|
|
27
|
+
const signer = new P12Signer(p12Buffer, { passphrase: opts.passphrase ?? "" });
|
|
28
|
+
const signed = await new SignPdf().sign(withPlaceholder, signer);
|
|
29
|
+
if (opts.output) {
|
|
30
|
+
writeFileSync(opts.output, signed);
|
|
31
|
+
return { output: opts.output, engine: "node" };
|
|
32
|
+
}
|
|
33
|
+
return { bytes: new Uint8Array(signed), engine: "node" };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=node-signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-signer.js","sourceRoot":"","sources":["../src/node-signer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGjE,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAiB;IAC9C,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,oGAAoG,CAAC,CAAC;IACxH,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtG,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEhG,MAAM,eAAe,GAAG,mBAAmB,CAAC;QAC1C,SAAS;QACT,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,WAAW;QACxC,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE;QACrC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE;QAC3B,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,IAAI,EAAE;KACpC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,SAAS,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAEjE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SignOptions, SignResult } from "./types.js";
|
|
2
|
+
/** Signe via pyhanko (PAdES visible + éventuelles permissions restrictives). */
|
|
3
|
+
export declare function signPyhanko(opts: SignOptions, python: string): SignResult;
|
|
4
|
+
//# sourceMappingURL=pyhanko-signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pyhanko-signer.d.ts","sourceRoot":"","sources":["../src/pyhanko-signer.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAmB,UAAU,EAAE,MAAM,YAAY,CAAC;AAqB3E,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CA8BzE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — moteur pyhanko (PAdES + permissions AES-256), via le script Python embarqué.
|
|
3
|
+
*
|
|
4
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
* @license AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { SIGN_PY } from "./engines.js";
|
|
12
|
+
function toFile(dir, name, data) {
|
|
13
|
+
const p = join(dir, name);
|
|
14
|
+
writeFileSync(p, data);
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
function mapPermissions(p) {
|
|
18
|
+
return {
|
|
19
|
+
accessibility: p.accessibility ?? true,
|
|
20
|
+
extract: p.extract ?? false,
|
|
21
|
+
modify_annotation: p.modifyAnnotation ?? false,
|
|
22
|
+
modify_assembly: p.modifyAssembly ?? false,
|
|
23
|
+
modify_other: p.modifyOther ?? false,
|
|
24
|
+
print_lowres: p.printLowres ?? true,
|
|
25
|
+
print_highres: p.printHighres ?? true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Signe via pyhanko (PAdES visible + éventuelles permissions restrictives). */
|
|
29
|
+
export function signPyhanko(opts, python) {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), "mosta-pdf-sign-"));
|
|
31
|
+
try {
|
|
32
|
+
const inPath = typeof opts.input === "string" ? opts.input : toFile(dir, "in.pdf", opts.input);
|
|
33
|
+
const p12Path = typeof opts.p12 === "string" ? opts.p12 : toFile(dir, "cert.p12", opts.p12);
|
|
34
|
+
const outPath = opts.output ?? join(dir, "out.pdf");
|
|
35
|
+
let passFile = opts.passphraseFile;
|
|
36
|
+
if (!passFile)
|
|
37
|
+
passFile = toFile(dir, ".pass", opts.passphrase ?? "");
|
|
38
|
+
const perms = opts.permissions ? mapPermissions(opts.permissions) : null;
|
|
39
|
+
const cfg = {
|
|
40
|
+
meta: opts.meta ?? {},
|
|
41
|
+
field: {
|
|
42
|
+
name: opts.field?.name,
|
|
43
|
+
page: opts.field?.page ?? 0,
|
|
44
|
+
box: opts.field?.box ?? [320, 40, 575, 140],
|
|
45
|
+
stamp_image: opts.field?.stampImage,
|
|
46
|
+
},
|
|
47
|
+
permissions: perms,
|
|
48
|
+
};
|
|
49
|
+
const cfgPath = toFile(dir, "config.json", JSON.stringify(cfg));
|
|
50
|
+
execFileSync(python, [SIGN_PY, "sign", inPath, outPath, p12Path, passFile, cfgPath], { stdio: "inherit" });
|
|
51
|
+
if (opts.output)
|
|
52
|
+
return { output: outPath, engine: "pyhanko" };
|
|
53
|
+
return { bytes: readFileSync(outPath), engine: "pyhanko" };
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
rmSync(dir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=pyhanko-signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pyhanko-signer.js","sourceRoot":"","sources":["../src/pyhanko-signer.ts"],"names":[],"mappings":"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"}
|
package/dist/sign.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — dispatcher HYBRIDE.
|
|
3
|
+
*
|
|
4
|
+
* `auto` (défaut) : pyhanko si disponible (PAdES + permissions), sinon cœur Node.
|
|
5
|
+
* `node` : force PKCS#7 Node. `pyhanko` : force PAdES (erreur si venv absent).
|
|
6
|
+
* Fusionne les options avec la config `.env`.
|
|
7
|
+
*
|
|
8
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
9
|
+
* @license AGPL-3.0-or-later
|
|
10
|
+
*/
|
|
11
|
+
import type { SignOptions, SignResult } from "./types.js";
|
|
12
|
+
export declare function signPdf(opts: SignOptions): Promise<SignResult>;
|
|
13
|
+
//# sourceMappingURL=sign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM1D,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CA8BpE"}
|
package/dist/sign.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { detectEngines } from "./engines.js";
|
|
2
|
+
import { signNode } from "./node-signer.js";
|
|
3
|
+
import { signPyhanko } from "./pyhanko-signer.js";
|
|
4
|
+
import { loadPdfSignConfig } from "./config.js";
|
|
5
|
+
export async function signPdf(opts) {
|
|
6
|
+
const cfg = loadPdfSignConfig();
|
|
7
|
+
const merged = {
|
|
8
|
+
...opts,
|
|
9
|
+
p12: opts.p12 ?? cfg.p12,
|
|
10
|
+
passphrase: opts.passphrase ?? cfg.passphrase,
|
|
11
|
+
passphraseFile: opts.passphraseFile ?? cfg.passphraseFile,
|
|
12
|
+
meta: { ...cfg.meta, ...opts.meta },
|
|
13
|
+
field: { ...cfg.field, ...opts.field },
|
|
14
|
+
permissions: opts.permissions ?? (cfg.restrict ? {} : undefined),
|
|
15
|
+
engine: opts.engine ?? cfg.engine,
|
|
16
|
+
};
|
|
17
|
+
if (!merged.p12)
|
|
18
|
+
throw new Error("Certificat PKCS#12 manquant (opts.p12 ou PDF_SIGN_P12).");
|
|
19
|
+
const avail = detectEngines(cfg.python);
|
|
20
|
+
const want = merged.engine ?? "auto";
|
|
21
|
+
let engine;
|
|
22
|
+
if (want === "pyhanko") {
|
|
23
|
+
if (!avail.pyhanko)
|
|
24
|
+
throw new Error("Moteur pyhanko indisponible — lancer python/setup-venv.sh (ou PDF_SIGN_PYTHON).");
|
|
25
|
+
engine = "pyhanko";
|
|
26
|
+
}
|
|
27
|
+
else if (want === "node") {
|
|
28
|
+
engine = "node";
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
engine = avail.pyhanko ? "pyhanko" : "node"; // auto : préfère pyhanko
|
|
32
|
+
}
|
|
33
|
+
return engine === "pyhanko" ? signPyhanko(merged, avail.python) : signNode(merged);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=sign.js.map
|
package/dist/sign.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.js","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAiB;IAC7C,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAgB;QAC1B,GAAG,IAAI;QACP,GAAG,EAAE,IAAI,CAAC,GAAG,IAAK,GAAG,CAAC,GAAc;QACpC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,UAAU;QAC7C,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,GAAG,CAAC,cAAc;QACzD,IAAI,EAAE,EAAE,GAAG,GAAG,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE;QACnC,KAAK,EAAE,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE;QACtC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAChE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM;KAClC,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAE5F,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC;IAErC,IAAI,MAA0B,CAAC;IAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,iFAAiF,CAAC,CAAC;QACvH,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QAC3B,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,yBAAyB;IACxE,CAAC;IAED,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,MAAgB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC/F,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — types publics.
|
|
3
|
+
*
|
|
4
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
* @license AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
/** Métadonnées de la signature (motif, lieu, signataire). */
|
|
8
|
+
export interface SignMeta {
|
|
9
|
+
reason?: string;
|
|
10
|
+
location?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
contact?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Position du cachet de signature visible. Boîte en points PDF (origine bas-gauche). */
|
|
15
|
+
export interface SignField {
|
|
16
|
+
name?: string;
|
|
17
|
+
/** Index de page 0-based (0 = page 1). */
|
|
18
|
+
page?: number;
|
|
19
|
+
/** [x1, y1, x2, y2] en points (A4 = 595×842). */
|
|
20
|
+
box?: [number, number, number, number];
|
|
21
|
+
/** Image de fond du cachet visible (chemin ; logo/tampon derrière le texte). Moteur pyhanko. */
|
|
22
|
+
stampImage?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Permissions restrictives (chiffrement AES-256). Requiert le moteur pyhanko. */
|
|
25
|
+
export interface SignPermissions {
|
|
26
|
+
extract?: boolean;
|
|
27
|
+
modifyAnnotation?: boolean;
|
|
28
|
+
modifyAssembly?: boolean;
|
|
29
|
+
modifyOther?: boolean;
|
|
30
|
+
printLowres?: boolean;
|
|
31
|
+
printHighres?: boolean;
|
|
32
|
+
accessibility?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export type SignEngine = "auto" | "node" | "pyhanko";
|
|
35
|
+
export interface SignOptions {
|
|
36
|
+
/** PDF source : chemin ou octets. */
|
|
37
|
+
input: string | Uint8Array;
|
|
38
|
+
/** Chemin de sortie ; si absent, les octets signés sont renvoyés. */
|
|
39
|
+
output?: string;
|
|
40
|
+
/** Certificat PKCS#12 : chemin ou octets. */
|
|
41
|
+
p12: string | Uint8Array;
|
|
42
|
+
passphrase?: string;
|
|
43
|
+
passphraseFile?: string;
|
|
44
|
+
meta?: SignMeta;
|
|
45
|
+
field?: SignField;
|
|
46
|
+
/** Restrictions d'édition. `false`/absent = aucune (le cœur Node suffit) ; objet = PAdES pyhanko. */
|
|
47
|
+
permissions?: SignPermissions | false;
|
|
48
|
+
/** `auto` (défaut) = pyhanko si dispo sinon Node ; `node` = PKCS#7 ; `pyhanko` = PAdES. */
|
|
49
|
+
engine?: SignEngine;
|
|
50
|
+
}
|
|
51
|
+
export interface SignResult {
|
|
52
|
+
output?: string;
|
|
53
|
+
bytes?: Uint8Array;
|
|
54
|
+
engine: "node" | "pyhanko";
|
|
55
|
+
}
|
|
56
|
+
export interface VerifyOptions {
|
|
57
|
+
input: string;
|
|
58
|
+
/** Certificat de confiance (PEM) pour la validation pyhanko. */
|
|
59
|
+
trustPem?: string;
|
|
60
|
+
engine?: SignEngine;
|
|
61
|
+
}
|
|
62
|
+
export interface SignatureInfo {
|
|
63
|
+
field?: string;
|
|
64
|
+
intact?: boolean;
|
|
65
|
+
valid?: boolean;
|
|
66
|
+
trusted?: boolean;
|
|
67
|
+
signer?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface VerifyResult {
|
|
70
|
+
signed: boolean;
|
|
71
|
+
signatures: SignatureInfo[];
|
|
72
|
+
engine: "pyhanko" | "node";
|
|
73
|
+
}
|
|
74
|
+
export interface EngineAvailability {
|
|
75
|
+
node: boolean;
|
|
76
|
+
pyhanko: boolean;
|
|
77
|
+
python?: string;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,qCAAqC;IACrC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,qGAAqG;IACrG,WAAW,CAAC,EAAE,eAAe,GAAG,KAAK,CAAC;IACtC,2FAA2F;IAC3F,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,SAAS,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +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,CAsB3D"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/pdf-sign — vérification de signature.
|
|
3
|
+
*
|
|
4
|
+
* pyhanko (validation cryptographique complète) si disponible ; sinon détection Node
|
|
5
|
+
* basique (présence d'un dictionnaire de signature).
|
|
6
|
+
*
|
|
7
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
8
|
+
* @license AGPL-3.0-or-later
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { detectEngines, SIGN_PY } from "./engines.js";
|
|
13
|
+
import { loadPdfSignConfig } from "./config.js";
|
|
14
|
+
export function verifyPdf(opts) {
|
|
15
|
+
const cfg = loadPdfSignConfig();
|
|
16
|
+
const avail = detectEngines(cfg.python);
|
|
17
|
+
const trust = opts.trustPem ?? cfg.trustPem;
|
|
18
|
+
const want = opts.engine ?? cfg.engine;
|
|
19
|
+
if (avail.pyhanko && want !== "node") {
|
|
20
|
+
try {
|
|
21
|
+
const args = trust ? [SIGN_PY, "verify", opts.input, trust] : [SIGN_PY, "verify", opts.input];
|
|
22
|
+
const out = execFileSync(avail.python, args, { encoding: "utf-8" });
|
|
23
|
+
const line = out.trim().split("\n").pop() ?? "{}";
|
|
24
|
+
const parsed = JSON.parse(line);
|
|
25
|
+
return { signed: !!parsed.signed, signatures: parsed.signatures ?? [], engine: "pyhanko" };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* repli sur la détection Node */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Détection Node basique : présence d'un /ByteRange + dictionnaire de signature.
|
|
32
|
+
const s = readFileSync(opts.input).toString("latin1");
|
|
33
|
+
const signed = s.includes("/ByteRange") && /\/Type\s*\/Sig|adbe\.pkcs7|ETSI\.CAdES/.test(s);
|
|
34
|
+
return { signed, signatures: signed ? [{ intact: undefined, valid: undefined }] : [], engine: "node" };
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,UAAU,SAAS,CAAC,IAAmB;IAC3C,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC;IAEvC,IAAI,KAAK,CAAC,OAAO,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9F,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,MAAgB,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;YAC9E,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;YAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAChC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC7F,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;IACH,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,wCAAwC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACzG,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemple — signature de base (moteur auto) + vérification.
|
|
3
|
+
*
|
|
4
|
+
* Usage : node examples/sign-basic.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]
|
|
5
|
+
*
|
|
6
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
7
|
+
* @license AGPL-3.0-or-later
|
|
8
|
+
*/
|
|
9
|
+
import { signPdf, verifyPdf, detectEngines } from "../dist/index.js";
|
|
10
|
+
|
|
11
|
+
const [input, p12, passphrase, output = "signe.pdf"] = process.argv.slice(2);
|
|
12
|
+
if (!input || !p12) {
|
|
13
|
+
console.error("Usage : node examples/sign-basic.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log("Moteurs :", detectEngines());
|
|
18
|
+
|
|
19
|
+
const res = await signPdf({
|
|
20
|
+
input,
|
|
21
|
+
output,
|
|
22
|
+
p12,
|
|
23
|
+
passphrase,
|
|
24
|
+
meta: { reason: "Compte rendu d'hospitalisation", name: "Dr H. Madani", location: "Alger, DZ" },
|
|
25
|
+
});
|
|
26
|
+
console.log(`Signé (${res.engine}) → ${res.output}`);
|
|
27
|
+
|
|
28
|
+
console.log("Vérification :", JSON.stringify(verifyPdf({ input: output }), null, 2));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemple — signature PAdES + permissions restrictives (chiffrement AES-256).
|
|
3
|
+
* Exige le moteur pyhanko : bash python/setup-venv.sh puis exporter PDF_SIGN_PYTHON.
|
|
4
|
+
*
|
|
5
|
+
* Usage : node examples/sign-restricted.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]
|
|
6
|
+
*
|
|
7
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
8
|
+
* @license AGPL-3.0-or-later
|
|
9
|
+
*/
|
|
10
|
+
import { signPdf, detectEngines } from "../dist/index.js";
|
|
11
|
+
|
|
12
|
+
const [input, p12, passphrase, output = "signe-restreint.pdf"] = process.argv.slice(2);
|
|
13
|
+
if (!input || !p12) {
|
|
14
|
+
console.error("Usage : node examples/sign-restricted.mjs <in.pdf> <cert.p12> <passphrase> [out.pdf]");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const avail = detectEngines();
|
|
19
|
+
if (!avail.pyhanko) {
|
|
20
|
+
console.error("Moteur pyhanko indisponible — lancer python/setup-venv.sh et exporter PDF_SIGN_PYTHON.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const res = await signPdf({
|
|
25
|
+
input,
|
|
26
|
+
output,
|
|
27
|
+
p12,
|
|
28
|
+
passphrase,
|
|
29
|
+
engine: "pyhanko",
|
|
30
|
+
permissions: { extract: false, modifyAnnotation: false, modifyOther: false, printHighres: true },
|
|
31
|
+
meta: { reason: "Facture CNAS — valeur probante", name: "Établissement", location: "Alger, DZ" },
|
|
32
|
+
});
|
|
33
|
+
console.log(`Signé + restreint (${res.engine}) → ${res.output}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemple — cachet visible « image de fond + texte » (logo/tampon) + PAdES.
|
|
3
|
+
* Exige le moteur pyhanko (bash python/setup-venv.sh, PDF_SIGN_PYTHON).
|
|
4
|
+
*
|
|
5
|
+
* Usage : node examples/sign-stamped.mjs <in.pdf> <cert.p12> <passphrase> <logo.png> [out.pdf]
|
|
6
|
+
*
|
|
7
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
8
|
+
* @license AGPL-3.0-or-later
|
|
9
|
+
*/
|
|
10
|
+
import { signPdf, verifyPdf, detectEngines } from "../dist/index.js";
|
|
11
|
+
|
|
12
|
+
const [input, p12, passphrase, logo, output = "signe-cachet.pdf"] = process.argv.slice(2);
|
|
13
|
+
if (!input || !p12 || !logo) {
|
|
14
|
+
console.error("Usage : node examples/sign-stamped.mjs <in.pdf> <cert.p12> <passphrase> <logo.png> [out.pdf]");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (!detectEngines().pyhanko) {
|
|
18
|
+
console.error("Moteur pyhanko indisponible — lancer python/setup-venv.sh et exporter PDF_SIGN_PYTHON.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const res = await signPdf({
|
|
23
|
+
input, output, p12, passphrase, engine: "pyhanko", permissions: {},
|
|
24
|
+
meta: { reason: "Compte rendu d'hospitalisation", name: "Dr Hamid MADANI", location: "Alger, DZ" },
|
|
25
|
+
field: { page: 1, box: [320, 40, 575, 140], stampImage: logo }, // cachet image + texte, bas-droite p.2
|
|
26
|
+
});
|
|
27
|
+
console.log(`Signé avec cachet image (${res.engine}) → ${res.output}`);
|
|
28
|
+
console.log("Vérification :", JSON.stringify(verifyPdf({ input: output }), null, 2));
|
package/llms.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @mostajs/pdf-sign — fiche LLM
|
|
2
|
+
> Signature numérique de PDF, moteur HYBRIDE : cœur Node (PKCS#7 détaché via @signpdf, zéro Python) + bascule pyhanko (PAdES visible + permissions AES-256) quand un venv est disponible. Config 100 % `.env`, CLI + API. Portage générique du pipeline pyhanko+pikepdf éprouvé. IMPLÉMENTÉ 0.1.0.
|
|
3
|
+
|
|
4
|
+
- Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Chemin: mostajs/mosta-pdf-sign-stack/mosta-pdf-sign · UI: ../mosta-pdf-sign-ui
|
|
6
|
+
|
|
7
|
+
## RÔLE
|
|
8
|
+
Le mécanisme de scellement probant d'UN PDF (signer + vérifier), pas l'orchestration métier.
|
|
9
|
+
GÉNÉRIQUE : ne connaît aucun domaine. La sémantique (quel document, quel signataire, quelle
|
|
10
|
+
valeur légale) est portée par l'appelant via options/`.env`.
|
|
11
|
+
|
|
12
|
+
## POURQUOI (SIH & documents officiels)
|
|
13
|
+
Valeur probante médico-légale exigée par l'interopérabilité (chap. 11) et la sécurité (chap. 12) :
|
|
14
|
+
- cliniques : CRH, CRO, comptes rendus d'imagerie/biologie signés par le praticien ;
|
|
15
|
+
- administratifs : certificats de séjour, ordonnances, dossiers de sortie ;
|
|
16
|
+
- financiers/CNAS-CASNOS : factures, bordereaux de remboursement, prises en charge.
|
|
17
|
+
La signature PAdES + permissions AES-256 garantit intégrité, authenticité et non-répudiation.
|
|
18
|
+
|
|
19
|
+
## DEUX MOTEURS
|
|
20
|
+
- **node** (défaut si pas de venv) : PKCS#7 détaché, cachet invisible, aucune dépendance Python.
|
|
21
|
+
NE gère PAS permissions/chiffrement/PAdES.
|
|
22
|
+
- **pyhanko** (si venv) : PAdES visible + permissions restrictives (chiffrement AES-256, R=6) via le
|
|
23
|
+
script Python embarqué `python/sign_with_perms.py`. Pipeline : pikepdf chiffre → IncrementalPdfFileWriter
|
|
24
|
+
(writer.encrypt(owner)) → append_signature_field → PdfSigner PAdES. Préserve chiffrement ET signature.
|
|
25
|
+
- **auto** : pyhanko si dispo, sinon node.
|
|
26
|
+
|
|
27
|
+
## EXPORTS ("@mostajs/pdf-sign")
|
|
28
|
+
- signPdf(opts): Promise<SignResult> — fusionne opts + `.env`, dispatch node/pyhanko
|
|
29
|
+
- verifyPdf(opts): VerifyResult — validation pyhanko complète si dispo, sinon détection Node (/ByteRange)
|
|
30
|
+
- detectEngines(python?): EngineAvailability — { node:true, pyhanko:bool, python }
|
|
31
|
+
- resolvePython / pyhankoAvailable
|
|
32
|
+
- loadPdfSignConfig(): PdfSignConfig — lecture `.env` via @mostajs/config
|
|
33
|
+
- types SignOptions/SignResult/SignMeta/SignField/SignPermissions/SignEngine/Verify*
|
|
34
|
+
|
|
35
|
+
## CONFIG (.env — via @mostajs/config, rien en dur)
|
|
36
|
+
PDF_SIGN_P12 / PDF_SIGN_PASSPHRASE / PDF_SIGN_PASSPHRASE_FILE
|
|
37
|
+
PDF_SIGN_ENGINE (auto|node|pyhanko) · PDF_SIGN_PYTHON (bin du venv)
|
|
38
|
+
PDF_SIGN_REASON / _LOCATION / _NAME / _CONTACT (métadonnées du cachet)
|
|
39
|
+
PDF_SIGN_FIELD_PAGE / PDF_SIGN_FIELD_BOX ("x1,y1,x2,y2") · PDF_SIGN_STAMP_IMAGE (logo du cachet visible, pyhanko) · PDF_SIGN_RESTRICT (bool → permissions)
|
|
40
|
+
PDF_SIGN_TRUST_PEM (certificat de confiance pour la vérification)
|
|
41
|
+
|
|
42
|
+
## CLI
|
|
43
|
+
mosta-pdf-sign sign <in.pdf> [out.pdf] --p12 c.p12 --pass s [--engine …] [--restrict] [--reason …]
|
|
44
|
+
mosta-pdf-sign verify <pdf> [--trust cert.pem] [--engine …]
|
|
45
|
+
mosta-pdf-sign engines
|
|
46
|
+
|
|
47
|
+
## MOTEUR pyhanko (optionnel)
|
|
48
|
+
bash python/setup-venv.sh → .venv local → exporter PDF_SIGN_PYTHON=…/.venv/bin/python
|
|
49
|
+
requiert pyhanko>=0.25, pikepdf>=8.
|
|
50
|
+
|
|
51
|
+
## PIÈGES
|
|
52
|
+
- Le cœur Node lève si `permissions` est demandé : le PAdES/chiffrement EXIGE pyhanko.
|
|
53
|
+
- `engine:'pyhanko'` sans venv → erreur explicite (lancer setup-venv.sh).
|
|
54
|
+
- p12 obligatoire (opts.p12 ou PDF_SIGN_P12) sinon throw.
|
|
55
|
+
- box en points PDF, origine bas-gauche (A4 = 595×842) ; défaut cachet bas-droite [320,40,575,140].
|
|
56
|
+
- field.stampImage / PDF_SIGN_STAMP_IMAGE : cachet visible « image de fond + texte » (pyhanko stamp.TextStampStyle, opacité 0.35). Ignoré par le cœur Node.
|
|
57
|
+
- La passphrase peut passer par fichier (PASSPHRASE_FILE) pour ne jamais transiter en argv (pyhanko).
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/pdf-sign",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"mosta-pdf-sign": "dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"python/*.py",
|
|
23
|
+
"python/*.txt",
|
|
24
|
+
"python/*.sh",
|
|
25
|
+
"examples/*.mjs",
|
|
26
|
+
"README.md",
|
|
27
|
+
"llms.txt",
|
|
28
|
+
"CHANGELOG.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"test": "bash test-scripts/run-tests.sh"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"pdf",
|
|
36
|
+
"signature",
|
|
37
|
+
"pades",
|
|
38
|
+
"pkcs7",
|
|
39
|
+
"pkcs12",
|
|
40
|
+
"pyhanko",
|
|
41
|
+
"signpdf",
|
|
42
|
+
"sign",
|
|
43
|
+
"mostajs"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@mostajs/config": "^1.1.0",
|
|
50
|
+
"@signpdf/signpdf": "^3.2.4",
|
|
51
|
+
"@signpdf/signer-p12": "^3.2.4",
|
|
52
|
+
"@signpdf/placeholder-plain": "^3.2.4",
|
|
53
|
+
"pdf-lib": "^1.17.1"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@mostajs/mjs-unit": "^0.4.1",
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"typescript": "^5.6.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# @mostajs/pdf-sign — crée le venv Python du moteur pyhanko (optionnel : PAdES + permissions).
|
|
3
|
+
# @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
python3 -m venv "$HERE/.venv"
|
|
7
|
+
"$HERE/.venv/bin/pip" install --upgrade pip >/dev/null
|
|
8
|
+
"$HERE/.venv/bin/pip" install -r "$HERE/requirements.txt"
|
|
9
|
+
echo "OK — venv pyhanko prêt : $HERE/.venv (exporter PDF_SIGN_PYTHON=$HERE/.venv/bin/python)"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""sign_with_perms.py — signature PAdES + permissions restrictives (pyhanko + pikepdf).
|
|
3
|
+
|
|
4
|
+
Version paramétrée (config JSON) du pipeline éprouvé : applique éventuellement des
|
|
5
|
+
permissions PDF restrictives (chiffrement AES-256) PUIS signe en PAdES visible, en
|
|
6
|
+
incremental update sur le PDF chiffré — préservant l'authentification du chiffrement
|
|
7
|
+
ET la validité cryptographique de la signature.
|
|
8
|
+
|
|
9
|
+
Usage :
|
|
10
|
+
sign_with_perms.py sign INPUT.pdf OUTPUT.pdf CERT.p12 PASSFILE CONFIG.json
|
|
11
|
+
sign_with_perms.py verify PDF CERT.pem
|
|
12
|
+
|
|
13
|
+
Le moteur Node (@mostajs/pdf-sign) écrit CONFIG.json puis appelle ce script.
|
|
14
|
+
|
|
15
|
+
@author Dr Hamid MADANI <drmdh@msn.com>
|
|
16
|
+
@license AGPL-3.0-or-later
|
|
17
|
+
"""
|
|
18
|
+
import sys
|
|
19
|
+
import io
|
|
20
|
+
import json
|
|
21
|
+
import secrets
|
|
22
|
+
import string
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def do_sign(inp, outp, p12_path, p12_pass_file, config_path):
|
|
26
|
+
import pikepdf
|
|
27
|
+
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
|
|
28
|
+
from pyhanko.sign import signers, fields, PdfSignatureMetadata
|
|
29
|
+
from pyhanko.sign.fields import SigFieldSpec
|
|
30
|
+
|
|
31
|
+
with open(p12_pass_file, "rb") as f:
|
|
32
|
+
p12_pass = f.read().strip()
|
|
33
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
34
|
+
cfg = json.load(f)
|
|
35
|
+
|
|
36
|
+
meta_cfg = cfg.get("meta", {})
|
|
37
|
+
field_cfg = cfg.get("field", {})
|
|
38
|
+
perms_cfg = cfg.get("permissions") # None => pas de chiffrement/restriction
|
|
39
|
+
|
|
40
|
+
src_buf = inp
|
|
41
|
+
# --- 1. permissions + chiffrement (optionnel) via pikepdf, en mémoire ---
|
|
42
|
+
owner_pwd = None
|
|
43
|
+
if perms_cfg is not None:
|
|
44
|
+
print("-- 1/3 permissions (chiffrement AES-256) --", flush=True)
|
|
45
|
+
owner_pwd = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(40))
|
|
46
|
+
perms = pikepdf.Permissions(
|
|
47
|
+
accessibility=perms_cfg.get("accessibility", True),
|
|
48
|
+
extract=perms_cfg.get("extract", False),
|
|
49
|
+
modify_annotation=perms_cfg.get("modify_annotation", False),
|
|
50
|
+
modify_assembly=perms_cfg.get("modify_assembly", False),
|
|
51
|
+
modify_form=True, # requis pour signer après chiffrement
|
|
52
|
+
modify_other=perms_cfg.get("modify_other", False),
|
|
53
|
+
print_lowres=perms_cfg.get("print_lowres", True),
|
|
54
|
+
print_highres=perms_cfg.get("print_highres", True),
|
|
55
|
+
)
|
|
56
|
+
encryption = pikepdf.Encryption(user="", owner=owner_pwd, R=6, allow=perms)
|
|
57
|
+
buf = io.BytesIO()
|
|
58
|
+
with pikepdf.open(inp) as pdf:
|
|
59
|
+
pdf.save(buf, encryption=encryption)
|
|
60
|
+
buf.seek(0)
|
|
61
|
+
src_buf = buf
|
|
62
|
+
|
|
63
|
+
# --- 2. champ de signature ---
|
|
64
|
+
print("-- 2/3 champ de signature --", flush=True)
|
|
65
|
+
writer = IncrementalPdfFileWriter(src_buf if isinstance(src_buf, io.BytesIO) else open(src_buf, "rb"))
|
|
66
|
+
if owner_pwd is not None:
|
|
67
|
+
writer.encrypt(owner_pwd)
|
|
68
|
+
|
|
69
|
+
field_name = field_cfg.get("name", "Signature1")
|
|
70
|
+
box = tuple(field_cfg.get("box", [320, 40, 575, 140]))
|
|
71
|
+
on_page = field_cfg.get("page", 0)
|
|
72
|
+
fields.append_signature_field(
|
|
73
|
+
writer,
|
|
74
|
+
sig_field_spec=SigFieldSpec(sig_field_name=field_name, on_page=on_page, box=box),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# --- 3. signature PAdES ---
|
|
78
|
+
print("-- 3/3 signature PAdES --", flush=True)
|
|
79
|
+
signer = signers.SimpleSigner.load_pkcs12(pfx_file=p12_path, passphrase=p12_pass)
|
|
80
|
+
meta = PdfSignatureMetadata(
|
|
81
|
+
field_name=field_name,
|
|
82
|
+
reason=meta_cfg.get("reason", "Authentification du document"),
|
|
83
|
+
location=meta_cfg.get("location", ""),
|
|
84
|
+
name=meta_cfg.get("name", ""),
|
|
85
|
+
contact_info=meta_cfg.get("contact", ""),
|
|
86
|
+
subfilter=fields.SigSeedSubFilter.PADES,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Apparence du cachet visible : image de fond (logo/tampon) + texte, si une image est fournie.
|
|
90
|
+
stamp_style = _build_stamp_style(field_cfg.get("stamp_image"), meta_cfg)
|
|
91
|
+
|
|
92
|
+
with open(outp, "wb") as out_f:
|
|
93
|
+
signers.PdfSigner(signature_meta=meta, signer=signer, stamp_style=stamp_style).sign_pdf(writer, output=out_f)
|
|
94
|
+
print(f"OK -- PDF signé : {outp}", flush=True)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_stamp_style(stamp_image, meta_cfg):
|
|
98
|
+
"""Style de cachet visible « image de fond + texte » (ou None → texte par défaut de pyhanko)."""
|
|
99
|
+
if not stamp_image:
|
|
100
|
+
return None
|
|
101
|
+
from pyhanko import stamp
|
|
102
|
+
from pyhanko.pdf_utils import images
|
|
103
|
+
|
|
104
|
+
lines = [x for x in (meta_cfg.get("name"), meta_cfg.get("reason"), meta_cfg.get("location")) if x]
|
|
105
|
+
# %(ts)s est substitué par pyhanko (horodatage) ; le reste est du texte littéral.
|
|
106
|
+
text = ("\n".join(lines) + "\n" if lines else "") + "Signé le %(ts)s"
|
|
107
|
+
return stamp.TextStampStyle(
|
|
108
|
+
stamp_text=text,
|
|
109
|
+
background=images.PdfImage(stamp_image),
|
|
110
|
+
background_opacity=0.35,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _cert_name(cert):
|
|
115
|
+
"""Nom lisible du signataire (sujet du certificat) plutôt que le repr brut."""
|
|
116
|
+
try:
|
|
117
|
+
return cert.subject.human_friendly
|
|
118
|
+
except Exception:
|
|
119
|
+
return str(cert) if cert else ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def do_verify(pdf_path, cert_pem):
|
|
123
|
+
import logging
|
|
124
|
+
# Silence les logs internes (tracebacks de chaîne de confiance) : on renvoie le verdict en JSON.
|
|
125
|
+
for name in ("pyhanko", "pyhanko_certvalidator"):
|
|
126
|
+
logging.getLogger(name).setLevel(logging.CRITICAL)
|
|
127
|
+
|
|
128
|
+
from pyhanko.pdf_utils.reader import PdfFileReader
|
|
129
|
+
from pyhanko.sign.validation import validate_pdf_signature
|
|
130
|
+
from pyhanko_certvalidator import ValidationContext
|
|
131
|
+
from pyhanko.keys import load_cert_from_pemder
|
|
132
|
+
|
|
133
|
+
vc = None
|
|
134
|
+
try:
|
|
135
|
+
trust = load_cert_from_pemder(cert_pem) if cert_pem else None
|
|
136
|
+
vc = ValidationContext(trust_roots=[trust]) if trust else ValidationContext(allow_fetching=False)
|
|
137
|
+
except Exception:
|
|
138
|
+
vc = ValidationContext(allow_fetching=False)
|
|
139
|
+
|
|
140
|
+
with open(pdf_path, "rb") as f:
|
|
141
|
+
r = PdfFileReader(f)
|
|
142
|
+
# PDF chiffré (permissions restrictives) : s'authentifier avec le mot de passe
|
|
143
|
+
# utilisateur vide (lecture libre) avant d'énumérer les signatures.
|
|
144
|
+
if getattr(r, "encrypted", False):
|
|
145
|
+
try:
|
|
146
|
+
r.decrypt("")
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
sigs = r.embedded_signatures
|
|
150
|
+
if not sigs:
|
|
151
|
+
print(json.dumps({"signed": False, "signatures": []}))
|
|
152
|
+
return
|
|
153
|
+
out = []
|
|
154
|
+
for s in sigs:
|
|
155
|
+
status = validate_pdf_signature(s, vc)
|
|
156
|
+
out.append({
|
|
157
|
+
"field": s.field_name,
|
|
158
|
+
"intact": bool(status.intact),
|
|
159
|
+
"valid": bool(status.valid),
|
|
160
|
+
"trusted": bool(getattr(status, "trusted", False)),
|
|
161
|
+
"signer": _cert_name(getattr(status, "signing_cert", None)),
|
|
162
|
+
})
|
|
163
|
+
print(json.dumps({"signed": True, "signatures": out}))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
if len(sys.argv) >= 2 and sys.argv[1] == "sign" and len(sys.argv) == 7:
|
|
168
|
+
do_sign(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6])
|
|
169
|
+
elif len(sys.argv) >= 2 and sys.argv[1] == "verify" and len(sys.argv) >= 3:
|
|
170
|
+
do_verify(sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None)
|
|
171
|
+
else:
|
|
172
|
+
print(__doc__)
|
|
173
|
+
sys.exit(1)
|