- 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)
115 lines
4 KiB
JavaScript
115 lines
4 KiB
JavaScript
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
import { KMSClient, SignCommand } from '@aws-sdk/client-kms';
|
|
import * as pades 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 || '';
|
|
|
|
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');
|
|
|
|
console.log('[START] requestRef:', requestRef, 'sourceKey:', sourceKey);
|
|
|
|
// 1. Télécharger PDF source
|
|
const getPdf = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: sourceKey }));
|
|
const inputPdf = await streamToBuffer(getPdf.Body);
|
|
console.log('[PDF] Downloaded, size:', inputPdf.length, 'bytes');
|
|
|
|
// 2. Télécharger chain.pem
|
|
const getChain = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: CHAIN_S3_KEY }));
|
|
const chainPem = await streamToBuffer(getChain.Body);
|
|
console.log('[CHAIN] Downloaded, size:', chainPem.length, 'bytes');
|
|
|
|
// 3. Préparer le PDF avec les vraies valeurs ByteRange (calculées en 2 passes)
|
|
const {
|
|
pdfWithRevision,
|
|
byteRange,
|
|
contentsPlaceholder,
|
|
signingTime
|
|
} = await pades.preparePdfWithPlaceholder(inputPdf);
|
|
console.log('[PREPARE] PDF with revision ready, size:', pdfWithRevision.length, 'bytes');
|
|
console.log('[PREPARE] Signing time:', signingTime);
|
|
console.log('[PREPARE] ByteRange:', byteRange);
|
|
|
|
// 4. Calculer le digest des SignedAttributes (ByteRange déjà correct dans le PDF)
|
|
const {
|
|
signedAttrs,
|
|
signedAttrsDigest,
|
|
pdfDigest
|
|
} = pades.buildSignedAttributesDigest(pdfWithRevision, byteRange, signingTime);
|
|
|
|
// 5. Signer avec KMS
|
|
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(signedAttrsDigest),
|
|
MessageType: 'DIGEST',
|
|
SigningAlgorithm: 'RSASSA_PSS_SHA_256'
|
|
}));
|
|
const signatureBytes = Buffer.from(signResp.Signature);
|
|
console.log('[KMS] Signature length:', signatureBytes.length, 'bytes');
|
|
|
|
// 6. Construire le CMS SignedData
|
|
const cmsDer = await pades.buildCmsSignedData(signedAttrs, signatureBytes, chainPem);
|
|
const cmsHex = cmsDer.toString('hex');
|
|
console.log('[CMS] Built, hex length:', cmsHex.length);
|
|
|
|
// 7. Finaliser le PDF avec la signature (remplacer UNIQUEMENT /Contents)
|
|
const finalPdf = pades.finalizePdfWithCms(
|
|
pdfWithRevision,
|
|
byteRange,
|
|
cmsHex
|
|
);
|
|
|
|
const finalSha256 = crypto.createHash('sha256').update(finalPdf).digest('hex');
|
|
console.log('[FINAL] PDF size:', finalPdf.length, 'bytes, SHA256:', finalSha256);
|
|
|
|
// 8. Upload vers S3
|
|
const signedKey = `${SIGNED_PREFIX}${requestRef}.pdf`;
|
|
await s3.send(new PutObjectCommand({
|
|
Bucket: BUCKET,
|
|
Key: signedKey,
|
|
Body: finalPdf,
|
|
ContentType: 'application/pdf',
|
|
Metadata: {
|
|
requestRef,
|
|
pades: 'BES',
|
|
sha256: finalSha256,
|
|
}
|
|
}));
|
|
|
|
console.log('[SUCCESS] Signed PDF uploaded to:', signedKey);
|
|
|
|
return {
|
|
statusCode: 200,
|
|
body: JSON.stringify({
|
|
status: 'signed',
|
|
requestRef,
|
|
signed_pdf_s3_key: signedKey,
|
|
sha256: finalSha256
|
|
})
|
|
};
|
|
} catch (err) {
|
|
console.error('[ERROR]', err);
|
|
return {
|
|
statusCode: 500,
|
|
body: JSON.stringify({ error: String(err), stack: err.stack })
|
|
};
|
|
}
|
|
};
|
|
|
|
async function streamToBuffer(stream) {
|
|
const chunks = [];
|
|
for await (const chunk of stream) chunks.push(chunk);
|
|
return Buffer.concat(chunks);
|
|
}
|