- Remplacement de DocuSeal par solution souveraine Odentas Sign - Système d'authentification OTP pour signataires (bcryptjs + JWT) - 8 routes API: send-otp, verify-otp, sign, pdf-url, positions, status, webhook, signers - Interface moderne avec canvas de signature et animations (framer-motion, confetti) - Système de templates pour auto-détection des positions de signature (CDDU, RG, avenants) - PDF viewer avec @react-pdf-viewer (compatible Next.js) - Stockage S3: source/, signatures/, evidence/, signed/, certs/ - Tables Supabase: sign_requests, signers, sign_positions, sign_events, sign_assets - Evidence bundle automatique (JSON metadata + timestamps) - Templates emails: OTP et completion - Scripts Lambda prêts: pades-sign (KMS seal) et tsaStamp (RFC3161) - Mode test détecté automatiquement (emails whitelist) - Tests complets avec PDF CDDU réel (2 signataires)
110 lines
4.1 KiB
JavaScript
110 lines
4.1 KiB
JavaScript
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
import crypto from "node:crypto";
|
|
import { spawn } from "node:child_process";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { createWriteStream } from "node:fs";
|
|
import { pipeline } from "node:stream/promises";
|
|
|
|
const s3 = new S3Client({ region: process.env.REGION || process.env.AWS_REGION || "eu-west-3" });
|
|
const BUCKET = process.env.BUCKET;
|
|
const TSA_URL = process.env.TSA_URL || "https://timestamp.sectigo.com";
|
|
const DEFAULT_TSR_PREFIX = process.env.DEFAULT_TSR_PREFIX || "evidence/tsa/";
|
|
const DEFAULT_REQ_PREFIX = process.env.DEFAULT_REQ_PREFIX || "evidence/tsq/";
|
|
|
|
/**
|
|
* Attends un event JSON de la forme :
|
|
* {
|
|
* "requestRef": "CDDU-2025-0102",
|
|
* "pdfSha256": "<hex>", // optionnel si pdfS3Key présent
|
|
* "pdfS3Key": "source/contrat.pdf", // optionnel si pdfSha256 présent
|
|
* "tsrS3Key": "evidence/tsa/CDDU-2025-0102.tsr" // optionnel (sinon généré)
|
|
* }
|
|
*/
|
|
export const handler = async (event = {}) => {
|
|
try {
|
|
const requestRef = event.requestRef || randomRef();
|
|
|
|
let pdfSha256 = (event.pdfSha256 || "").trim();
|
|
if (!pdfSha256 && event.pdfS3Key) {
|
|
pdfSha256 = await sha256OfS3Object(BUCKET, event.pdfS3Key);
|
|
}
|
|
if (!pdfSha256 || !/^[0-9a-fA-F]{64}$/.test(pdfSha256)) {
|
|
throw new Error("pdfSha256 manquant ou invalide (attendu : hex 64 chars)");
|
|
}
|
|
|
|
// 1) Générer la requête RFC3161 (.tsq) via openssl
|
|
const tsqPath = `/tmp/${requestRef}.tsq`;
|
|
await genTsqWithOpenssl(pdfSha256, tsqPath);
|
|
|
|
// 2) Appeler la TSA
|
|
const tsrPath = `/tmp/${requestRef}.tsr`;
|
|
await postTsqToTsa(tsqPath, TSA_URL, tsrPath);
|
|
|
|
// 3) Hasher la réponse TSA
|
|
const tsrBuf = await readFile(tsrPath);
|
|
const tsrSha256 = crypto.createHash("sha256").update(tsrBuf).digest("hex");
|
|
|
|
// 4) Uploader .tsq et .tsr dans S3
|
|
const tsqKey = event.tsqS3Key || `${DEFAULT_REQ_PREFIX}${requestRef}.tsq`;
|
|
const tsrKey = event.tsrS3Key || `${DEFAULT_TSR_PREFIX}${requestRef}.tsr`;
|
|
|
|
await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: tsqKey, Body: await readFile(tsqPath), ContentType: "application/timestamp-query" }));
|
|
await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: tsrKey, Body: tsrBuf, ContentType: "application/timestamp-reply" }));
|
|
|
|
// 5) Réponse
|
|
return ok({
|
|
requestRef,
|
|
tsa_url: TSA_URL,
|
|
pdf_sha256: pdfSha256.toLowerCase(),
|
|
tsq_s3_key: tsqKey,
|
|
tsr_s3_key: tsrKey,
|
|
tsr_sha256: tsrSha256,
|
|
message: "RFC3161 timestamp acquired"
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
return errResp(err);
|
|
}
|
|
};
|
|
|
|
// Utilitaires
|
|
function randomRef() {
|
|
return `TS-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
|
}
|
|
|
|
async function sha256OfS3Object(bucket, key) {
|
|
const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
const hash = crypto.createHash("sha256");
|
|
await pipeline(res.Body, hash);
|
|
return hash.digest("hex");
|
|
}
|
|
|
|
async function genTsqWithOpenssl(hexDigest, outPath) {
|
|
// openssl ts -query -sha256 -digest <hex> -cert -no_nonce -out /tmp/req.tsq
|
|
await exec("openssl", ["ts", "-query", "-sha256", "-digest", hexDigest, "-cert", "-no_nonce", "-out", outPath]);
|
|
}
|
|
|
|
async function postTsqToTsa(tsqPath, url, outPath) {
|
|
// curl -sS -H "Content-Type: application/timestamp-query" --data-binary @file url > out
|
|
await exec("curl", ["-sS", "-H", "Content-Type: application/timestamp-query", "--data-binary", `@${tsqPath}`, url, "-o", outPath]);
|
|
}
|
|
|
|
async function exec(cmd, args) {
|
|
return new Promise((resolve, reject) => {
|
|
const p = spawn(cmd, args);
|
|
let stderr = "";
|
|
p.stderr.on("data", (d) => (stderr += d.toString()));
|
|
p.on("exit", (code) => {
|
|
if (code === 0) resolve(0);
|
|
else reject(new Error(`${cmd} exited with ${code}: ${stderr}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
function ok(payload) {
|
|
return { statusCode: 200, headers: { "content-type": "application/json" }, body: JSON.stringify(payload) };
|
|
}
|
|
function errResp(err) {
|
|
return { statusCode: 500, headers: { "content-type": "application/json" }, body: JSON.stringify({ error: String(err) }) };
|
|
}
|
|
|