- 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)
94 lines
4 KiB
JavaScript
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);
|
|
}
|