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": "", // 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 -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) }) }; }