Bibliothèque Python pour décoder et vérifier les 2D-DOC de l'ANTS (Agence Nationale des Titres Sécurisés).
Les 2D-DOC sont des codes-barres 2D (DataMatrix) présents sur de nombreux documents administratifs français (avis d'impôts, permis de conduire, cartes grises, etc.). Cette bibliothèque permet de :
- Parser la structure DC04 du 2D-DOC
- Décoder les champs métier selon le type de document
- Vérifier la signature cryptographique (ECDSA) avec la TSL ANTS
poetry add git+https://github.com/betagouv/2ddoc-parser.gitpip install git+https://github.com/betagouv/2ddoc-parser.git# Avec Poetry
poetry add git+https://github.com/betagouv/2ddoc-parser.git#v1.0.0
# Avec pip
pip install git+https://github.com/betagouv/2ddoc-parser.git@v1.0.0L'API principale se résume à une seule fonction : decode_2d_doc()
from fr_2ddoc_parser.api import decode_2d_doc
# Chaîne brute extraite d'un DataMatrix (par exemple avec pyzbar)
raw_2d_doc = "DC04FR000001FFFF23DC2801FR432,75<GS>44227801234567845202146RETI PATRICK<GS>4A310720224Y145 RUE JULLIARD/ZASPECIMEN/78320/LEVIS STNOM<GS>4163198<GS>47300112345678948RETISOPHIE<GS>4907019877654324V3542<GS>4W182<GS>4X3724<GS><US>6W76EBC3I2LWHBVGNNYTL34SC6V32S2GDCIQQZLZNMTKCHNVEUISJYUQH5WE3AJJICBNG3YMQ2NXXHP5ZHVOQE332R6TUJDHNOHQ6BI"
# Décode le 2D-DOC
result = decode_2d_doc(raw_2d_doc)
# Résultat
print(result.header.doc_type) # "28" (Avis d'impôts)
print(result.is_valid) # True si la signature est valide
print(result.typed) # Objet typé selon le type de documentLe résultat retourné est un objet Decoded2DDoc contenant :
result.header.marker # "DC" (identifiant 2D-DOC)
result.header.version # 4 (version du format)
result.header.doc_type # Type de document (ex: "28" pour avis d'impôts)
result.header.perimeter # Périmètre (ex: "01")
result.header.country # Pays (ex: "FR")
result.header.ca_id # ID de l'autorité de certification (ex: "FR06")
result.header.cert_id # ID du certificat
result.header.issue_date # Date d'émission (objet date)
result.header.signature_date # Date de signature (objet date)result.fields # Dict[str, str] - Tous les champs parsés
# Exemple : {"43": "2", "44": "1442569", "45": "2024", "46": "DOE JOHN", ...}result.signature.present # True si une signature est présente
result.signature.raw # bytes - Signature brute
result.signature.alg_hint # Algorithme détecté (P-256, P-384, P-521)result.typed # Objet spécifique au type de document
# Pour un avis d'impôts (type 28) :
result.typed.annee_revenue # 2024
result.typed.reference_avis # "1442569"
result.typed.declarant1 # "DOE JOHN"
result.typed.revenue_fiscal_de_reference # 30000
result.typed.adresse.full # "123 RUE DE PARIS, 75001 PARIS"result.is_valid # True/False - Signature cryptographique vérifiéefrom fr_2ddoc_parser.api import decode_2d_doc
raw = "DC04FR000001FFFF23DC2801FR432,75<GS>44227801234567845202146RETI PATRICK<GS>4A310720224Y145 RUE JULLIARD/ZASPECIMEN/78320/LEVIS STNOM<GS>4163198<GS>47300112345678948RETISOPHIE<GS>4907019877654324V3542<GS>4W182<GS>4X3724<GS><US>6W76EBC3I2LWHBVGNNYTL34SC6V32S2GDCIQQZLZNMTKCHNVEUISJYUQH5WE3AJJICBNG3YMQ2NXXHP5ZHVOQE332R6TUJDHNOHQ6BI"
result = decode_2d_doc(raw)
# Vérifier le type de document
if result.header.doc_type == "28":
avis = result.typed # AvisImposition
print(f"Année des revenus : {avis.annee_revenue}")
print(f"Référence : {avis.reference_avis}")
print(f"Déclarant : {avis.declarant1}")
print(f"Revenu fiscal de référence : {avis.revenue_fiscal_de_reference} €")
print(f"Nombre de parts : {avis.nombre_parts}")
# Adresse
if avis.adresse.full:
print(f"Adresse : {avis.adresse.full}")
else:
print(f"Adresse : {avis.adresse.voie}, {avis.adresse.code_postal} {avis.adresse.commune}")
# Signature valide ?
if result.is_valid:
print("✅ Signature valide")
else:
print("❌ Signature invalide")from fr_2ddoc_parser.api import decode_2d_doc
from fr_2ddoc_parser.exception.exceptions import (
TwoDDocFormatError,
TwoDDocUnsupportedVersion,
TwoDDocSignatureError
)
try:
result = decode_2d_doc(raw_data)
except TwoDDocFormatError as e:
print(f"Format invalide : {e}")
except TwoDDocUnsupportedVersion as e:
print(f"Version non supportée : {e}")
except TwoDDocSignatureError as e:
print(f"Signature invalide : {e}")
except ValueError as e:
print(f"Erreur de validation : {e}")| Type | Description | Classe typée |
|---|---|---|
| 28 | Avis d'impôts sur le revenu | AvisImposition |
| Autres | Document générique | GenericDoc |
Pour ajouter de nouveaux types de documents, consultez le CONTRIBUTING.md.
La bibliothèque vérifie automatiquement la signature ECDSA du 2D-DOC en utilisant :
- La TSL (Trusted Service List) ANTS embarquée dans le package
- L'algorithme détecté automatiquement (P-256, P-384 ou P-521)
- Le certificat correspondant au CA ID et Cert ID du header
La vérification se fait automatiquement lors de l'appel à decode_2d_doc().
Le KeyResolver charge automatiquement les certificats publics depuis le fichier tsl_signed.xml embarqué dans le package. Ce fichier contient la liste de confiance (Trusted Service List) ANTS au format ETSI TS 102 231.
Processus de résolution :
-
Chargement de la TSL : Au démarrage, le fichier
tsl_signed.xmlest parsé pour extraire tous les certificats X.509 des autorités de certification (CA) ANTS. -
Indexation intelligente : Pour chaque certificat, plusieurs identifiants candidats sont générés :
- 4/6/8 derniers caractères hexadécimaux du numéro de série du certificat
- 4/6/8 derniers caractères hexadécimaux du SKI (Subject Key Identifier)
- 4/6 premiers caractères de l'empreinte SHA-1 de la clé publique (SPKI)
-
Résolution lors du décodage : Lors de la vérification, le
KeyResolver:- Récupère le CA ID (ex: "FR06") et le Cert ID (ex: "FPE6") depuis le header DC04
- Recherche le certificat correspondant dans l'index
- Si un seul certificat existe pour ce CA, il est utilisé automatiquement (fallback)
- Extrait la clé publique du certificat pour vérifier la signature ECDSA
Exemple de résolution :
from fr_2ddoc_parser.crypto.key_resolver import local_key_resolver
# Résoudre une clé publique manuellement
public_key = local_key_resolver.resolve(ca_id="FR06", cert_id="FPE6")
# Lister les cert_id disponibles pour un CA donné
cert_ids = local_key_resolver.available_cert_ids("FR06")
print(f"Certificats disponibles pour FR06 : {cert_ids}")
# Exemple : {'FPE6', '1442', '569A', ...}Cette approche permet une résolution robuste même si le cert_id dans le 2D-DOC utilise différentes stratégies d'identification (serial, SKI, ou empreinte).
fr_2ddoc_parser/
├── api.py # Point d'entrée principal (decode_2d_doc)
├── parser/
│ ├── parser.py # Parsing de la structure DC04
│ ├── spec.py # Spécifications du format
│ └── helper.py # Fonctions d'aide (conversions)
├── crypto/
│ ├── crypto.py # Vérification de signature ECDSA
│ ├── key_resolver.py # Résolution des clés depuis la TSL
│ └── keys/
│ └── tsl_signed.xml # TSL ANTS embarquée
├── type/
│ ├── base.py # GenericDoc (fallback)
│ └── doc28_avis_impots.py # Avis d'impôts (type 28)
├── model/
│ └── models.py # Modèles de données (Decoded2DDoc, Header, etc.)
├── registry/
│ └── registry.py # Registre des handlers de types
└── exception/
└── exceptions.py # Exceptions personnalisées
# Lancer tous les tests
poetry run pytest
# Tests avec verbose
poetry run pytest -v
# Tests d'un fichier spécifique
poetry run pytest tests/test_avis_impots.py
# Coverage
poetry run pytest --cov=fr_2ddoc_parserLes contributions sont les bienvenues ! Consultez CONTRIBUTING.md pour :
- Ajouter un nouveau type de document
- Corriger un bug
- Améliorer la documentation
Ce projet est sous licence MIT. Voir le fichier LICENSE pour plus de détails.
Cette bibliothèque ne lit pas les codes-barres depuis des images. Elle attend une chaîne de caractères déjà extraite.
Pour extraire le DataMatrix d'une image, utilisez une bibliothèque comme :
- pyzbar (recommandé)
- pylibdmtx
- opencv + pyzbar
Exemple avec pyzbar :
from pyzbar.pyzbar import decode
from PIL import Image
from fr_2ddoc_parser.api import decode_2d_doc
# Lire l'image
img = Image.open("avis_impots.jpg")
# Extraire les DataMatrix
barcodes = decode(img)
for barcode in barcodes:
if barcode.type == 'DATAMATRIX':
raw_data = barcode.data.decode('utf-8')
# Décoder le 2D-DOC
result = decode_2d_doc(raw_data)
print(result.typed)- Les dates dans le header sont au format
datePython - Les dates dans les champs typés sont au format
datePython - Les dates brutes dans
fieldssont des strings (formatDDMMYYYY)
Certains champs sont optionnels selon le type de document. Les champs non présents auront la valeur None.
Les champs inconnus ou non mappés sont disponibles dans typed.extras (dict).