9 KiB
TODO - Conformité PAdES Complète (Niveau AES eIDAS)
🎯 Objectif
Améliorer la conformité de la signature Odentas Sign pour atteindre le niveau AES (Advanced Electronic Signature) selon le règlement eIDAS et la norme ETSI TS 102 778 (PAdES).
🔴 Problèmes détectés par le validateur EU DSS
1. FORMAT_FAILURE: PDF-NOT-ETSI au lieu de PAdES-BASELINE-B
Statut actuel : ❌ Non conforme
Impact : La signature n'est pas reconnue comme PAdES standard
Priorité : 🔥 CRITIQUE
Erreurs spécifiques :
The signed attribute: 'signing-certificate' is absent!The /ByteRange dictionary is not consistent!The reference data object is not intact!
2. Attribut signing-certificate-v2 manquant
Description :
L'attribut signing-certificate-v2 (OID: 1.2.840.113549.1.9.16.2.47) est obligatoire selon ETSI TS 102 778-3 pour PAdES-BASELINE-B.
Ce qu'il contient :
- Hash SHA-256 du certificat de signature
- IssuerSerial du certificat
Modifications nécessaires :
Fichier : lambda-odentas-pades-sign/helpers/pades.js
// 1. Ajouter l'OID (DÉJÀ FAIT)
const OID_ATTR_SIGNING_CERTIFICATE_V2 = '1.2.840.113549.1.9.16.2.47';
// 2. Modifier la signature de buildSignedAttributesDigest
export function buildSignedAttributesDigest(pdfWithRevision, byteRange, signingTime, signerCertDer) {
// ... code existant ...
// Calculer le hash du certificat
const certHash = crypto.createHash('sha256').update(signerCertDer).digest();
// Construire ESSCertIDv2
const essCertIDv2 = new asn1js.Sequence({
value: [
new asn1js.Sequence({ // hashAlgorithm
value: [
new asn1js.ObjectIdentifier({ value: '2.16.840.1.101.3.4.2.1' }), // SHA-256
]
}),
new asn1js.OctetString({ valueHex: certHash }), // certHash
new asn1js.Sequence({ // issuerSerial
value: [
// Extraire issuer et serial du certificat
signerCert.issuer,
signerCert.serialNumber
]
})
]
});
// Construire SigningCertificateV2
const signingCertV2 = new asn1js.Sequence({
value: [
new asn1js.Sequence({
value: [essCertIDv2]
})
]
});
const attrSigningCertV2 = new Attribute({
type: OID_ATTR_SIGNING_CERTIFICATE_V2,
values: [signingCertV2]
});
// Ajouter dans signedAttrs
const signedAttrsForDigest = new asn1js.Set({
value: [
attrContentType.toSchema(),
attrSigningTime.toSchema(),
attrMessageDigest.toSchema(),
attrSigningCertV2.toSchema() // ← NOUVEAU
]
});
// ...
return {
signedAttrs: [attrContentType, attrSigningTime, attrMessageDigest, attrSigningCertV2],
signedAttrsDigest,
byteRange,
pdfDigest
};
}
Fichier : lambda-odentas-pades-sign/index.js
// Passer le certificat à buildSignedAttributesDigest
const {
signedAttrs,
signedAttrsDigest,
pdfDigest
} = pades.buildSignedAttributesDigest(
pdfWithRevision,
byteRange,
signingTime,
chainPem // ← Passer la chaîne de certificats
);
Tests à effectuer :
- Rebuild Lambda avec modifications
- Tester génération de signature
- Valider sur https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation
- Vérifier que
signing-certificate-v2apparaît dans les attributs signés
3. ByteRange inconsistant
Problème actuel :
Document ByteRange : [0, 30578, 96114, 647]
Le dernier segment (647 bytes) semble trop court. Le ByteRange devrait couvrir tout le document sauf le placeholder de signature.
Formule correcte :
ByteRange = [0, a, b, c]
où:
- a = position de /Contents <
- b = position après le placeholder
- c = taille du fichier - b
Vérification à ajouter :
// Dans finalizePdfWithCms()
const fileSize = finalPdf.length;
const expectedEnd = byteRange[2] + byteRange[3];
if (expectedEnd !== fileSize) {
console.warn(`⚠️ ByteRange ne couvre pas tout le fichier!`);
console.warn(` - Taille fichier: ${fileSize}`);
console.warn(` - ByteRange end: ${expectedEnd}`);
console.warn(` - Différence: ${fileSize - expectedEnd} bytes`);
}
Cause possible :
- Le placeholder de signature est trop grand (65536 bytes)
- Des bytes supplémentaires sont ajoutés après la signature
- Le calcul du ByteRange dans
preparePdfWithPlaceholder()est incorrect
Tests à effectuer :
- Vérifier la taille du placeholder vs taille réelle du CMS
- Ajuster la taille du placeholder si nécessaire
- S'assurer que
byteRange[3]va jusqu'à EOF
4. SubFilter doit être /ETSI.CAdES.detached
Statut actuel : ✅ Déjà correct dans le code
/SubFilter /ETSI.CAdES.detached
Pas de modification nécessaire.
🟡 Améliorations optionnelles (pour AES complet)
5. Ajouter l'attribut content-hints (optionnel)
Pour indiquer le type de contenu signé :
const OID_ATTR_CONTENT_HINTS = '1.2.840.113549.1.9.16.2.4';
const attrContentHints = new Attribute({
type: OID_ATTR_CONTENT_HINTS,
values: [
new asn1js.Sequence({
value: [
new asn1js.UTF8String({ value: 'Contrat de travail' }),
new asn1js.ObjectIdentifier({ value: OID_ID_DATA })
]
})
]
});
6. Intégrer le timestamp TSA dans le CMS (pour PAdES-LT)
Objectif : Passer de PAdES-B à PAdES-LT (Long Term)
Modifications nécessaires :
- Obtenir le TSA après la signature
- L'ajouter comme unsignedAttrs dans le SignerInfo
// Dans buildCmsSignedData(), après avoir créé signerInfo
const tsaResponse = await fetch(process.env.TSA_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/timestamp-query' },
body: createTSQ(signatureBytes)
});
const tsr = await tsaResponse.arrayBuffer();
signerInfo.unsignedAttrs = new SignedAndUnsignedAttributes({
type: 1, // unsigned
attributes: [
new Attribute({
type: '1.2.840.113549.1.9.16.2.14', // id-aa-signatureTimeStampToken
values: [new asn1js.OctetString({ valueHex: Buffer.from(tsr) })]
})
]
});
Impact :
- ✅ Résout le problème "L'heure de signature est déterminée à partir de l'horloge"
- ✅ Conforme PAdES-LT (Long Term validation)
- ✅ Plus robuste pour archivage long terme
📋 Plan d'action
Phase 1 : Conformité PAdES-BASELINE-B (PRIORITÉ HAUTE)
- Implémenter
signing-certificate-v2 - Corriger le ByteRange
- Tester avec validateur EU DSS
- Vérifier que format = "PAdES-BASELINE-B"
Temps estimé : 4-6 heures
Complexité : Moyenne
Phase 2 : Intégration TSA dans CMS (PRIORITÉ MOYENNE)
- Ajouter unsignedAttrs avec timestamp
- Modifier workflow pour obtenir TSA après signature
- Tester conformité PAdES-LT
Temps estimé : 6-8 heures
Complexité : Élevée
Phase 3 : Certification (PRIORITÉ BASSE)
- Décider si viser AES ou rester en SES
- Évaluer coût certification ISO 27001
- Audit externe du système
Temps estimé : Plusieurs mois
Coût estimé : 10-20k€
🧪 Tests de validation
Validateur officiel EU DSS
https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation
Critères de succès :
- Format = "PAdES-BASELINE-B" (au lieu de "PDF-NOT-ETSI")
- Indication = "TOTAL_PASSED" (au lieu de "TOTAL_FAILED")
- Aucune erreur "FORMAT_FAILURE"
- Attribut
signing-certificate-v2présent
Outils complémentaires
- Adobe Acrobat Reader (vérification visuelle)
- pdfsig (Poppler) - validation technique
- VeraPDF - conformité PDF/A
📚 Références
Standards eIDAS
- Règlement eIDAS : (UE) N°910/2014
- ETSI TS 102 778 : PAdES (PDF Advanced Electronic Signatures)
- ETSI TS 119 172 : PAdES Baseline Profile
- RFC 5035 : ESS - Enhanced Security Services (ESSCertIDv2)
Documentation technique
- PKI.js : https://github.com/PeculiarVentures/PKI.js
- ASN.1 JavaScript : https://github.com/PeculiarVentures/ASN1.js
- PDF Reference 1.7 : https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf
Validateurs
- EU DSS Validator : https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation
- ANSSI : https://www.ssi.gouv.fr/
- VeraPDF : https://verapdf.org/
💡 Notes importantes
- Ne pas casser l'existant : Les signatures actuelles fonctionnent, gardons la compatibilité
- Tests progressifs : Valider chaque modification avec le validateur EU
- Backup des certificats : Les clés privées sont critiques, bien les sauvegarder
- Documentation : Documenter chaque changement pour audit futur
✅ Statut actuel
Niveau eIDAS : SES (Signature Électronique Simple)
Format signature : PDF-NOT-ETSI (non conforme PAdES)
Validation EU DSS : ❌ TOTAL_FAILED
Objectif : PAdES-BASELINE-B conforme, validation ✅ TOTAL_PASSED
Dernière mise à jour : 28 octobre 2025 Créé par : GitHub Copilot