espace-paie-odentas/TODO_PADES_CONFORMITE.md

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 :


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 :

  1. Obtenir le TSA après la signature
  2. 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-v2 pré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

Validateurs


💡 Notes importantes

  1. Ne pas casser l'existant : Les signatures actuelles fonctionnent, gardons la compatibilité
  2. Tests progressifs : Valider chaque modification avec le validateur EU
  3. Backup des certificats : Les clés privées sont critiques, bien les sauvegarder
  4. 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