espace-paie-odentas/lambda-odentas-pades-sign/index_backup.js
odentas b790faf12c feat: Implémentation complète du système Odentas Sign
- 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)
2025-10-27 19:03:07 +01:00

94 lines
4 KiB
JavaScript

import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { KMSClient, SignCommand } from '@aws-sdk/client-kms';
import fs from 'node:fs/promises';
import { PDFDocument } from 'pdf-lib';
import * as createPAdES from './helpers/pades.js';
import crypto from 'node:crypto';
const region = process.env.REGION || 'eu-west-3';
const s3 = new S3Client({ region });
const kms = new KMSClient({ region });
const BUCKET = process.env.BUCKET || 'odentas-sign';
const SIGNED_PREFIX = process.env.SIGNED_PREFIX || 'signed-pades/';
const CHAIN_S3_KEY = process.env.SIGNER_CHAIN_S3_KEY || 'certs/chain.pem';
const KMS_KEY_ID = process.env.KMS_KEY_ID || ''; // ARN ou KeyId
const TSA_LAMBDA_NAME = process.env.TSA_LAMBDA_NAME || 'odentas-tsa-stamp';
export const handler = async (event) => {
try {
const requestRef = event.requestRef || `REQ-${Date.now()}`;
const sourceKey = event.sourceKey || event.pdfS3Key;
if (!sourceKey) throw new Error('sourceKey manquant');
// 1) Télécharger PDF source
const get = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: sourceKey }));
const inputPdf = await streamToBuffer(get.Body);
// 2) Télécharger chain.pem (signer.crt [+ intermediate]) depuis S3
const chainObj = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: CHAIN_S3_KEY }));
const chainData = await streamToBuffer(chainObj.Body); // Buffer (PEM texte ou DER)
// 3) Construire le PDF avec placeholder signature et obtenir le "toSign" (byteRange bytes)
const { pdfWithPlaceholder, byteRangeInfo, pdfStructure, incrementalUpdate } = await createPAdES.preparePdfWithPlaceholder(inputPdf, requestRef);
// 4) Calculer digest des SignedAttributes (signature-ready digest) via helper
const { signedAttributesDer, signedAttributesDigest, tempPdfWithRevision, contentsStart, contentsEnd } = await createPAdES.buildSignedAttributesDigest(pdfWithPlaceholder, byteRangeInfo, pdfStructure, incrementalUpdate);
// 5) Demander à KMS de signer le digest (MessageType = DIGEST)
if (!KMS_KEY_ID) throw new Error('KMS_KEY_ID non défini');
const signResp = await kms.send(new SignCommand({
KeyId: KMS_KEY_ID,
Message: Buffer.from(signedAttributesDigest),
MessageType: 'DIGEST',
SigningAlgorithm: 'RSASSA_PSS_SHA_256'
}));
const signatureBytes = Buffer.from(signResp.Signature);
console.log('[KMS] Signature length:', signatureBytes.length, 'bytes');
// 6) Construire la structure PKCS#7 SignedData (pkijs helper) en injectant signatureBytes et chainPem
const cmsDer = await createPAdES.buildCmsSignedData(signedAttributesDer, signatureBytes, chainData);
// 7) (optionnel) demander et intégrer le token TSA (PAdES-T) — TODO pour v2
// 8) Injecter le CMS dans le PDF (Contents) et finaliser
const finalPdf = await createPAdES.finalizePdfWithCms(pdfWithPlaceholder, byteRangeInfo, pdfStructure, incrementalUpdate, cmsDer, tempPdfWithRevision, contentsStart, contentsEnd);
// Hash utile pour vérification et traçabilité
const finalSha256 = crypto.createHash('sha256').update(finalPdf).digest('hex');
console.info('[pades] final PDF bytes =', finalPdf.length, ' sha256=', finalSha256);
// 9) Upload PDF final (avec métadonnées)
const signedKey = `${SIGNED_PREFIX}${requestRef}.pdf`;
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: signedKey,
Body: finalPdf,
ContentType: 'application/pdf',
Metadata: {
requestRef,
pades: 'BES-proto',
sha256: finalSha256,
}
}));
return {
statusCode: 200,
body: JSON.stringify({
status: 'signed',
requestRef,
signed_pdf_s3_key: signedKey,
sha256: finalSha256
})
};
} catch (err) {
console.error(err);
return { statusCode: 500, body: JSON.stringify({ error: String(err) }) };
}
};
async function streamToBuffer(stream) {
const chunks = [];
for await (const c of stream) chunks.push(c);
return Buffer.concat(chunks);
}