#!/usr/bin/env python import argparse import base64 import cbor2 from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import \ encode_dss_signature import datetime import json import os import sys from textwrap import wrap import zlib from . import base45 def read_qrcode(file) -> str: """ Lit le contenu du fichier. Peut être combiné avec zbar pour directement lire le contenu du QRCode. """ with file: content = file.read() content = content.replace('\n', '') return content def analyse_qrcode(qrcode: str, additional_info: bool = False, check_signature: bool = True) -> None: """ Analyse les données du QR code pour extraire les données et vérifier la signature. Si `additional_info` est vrai, les informations de vaccination/test sont affichées. Renvoie la validité du pass sous forme de booléen. """ if not qrcode.startswith('HC1:'): raise ValueError("QR code invalide.") plain_input = qrcode.replace('HC1:', '') # QR Code en base 45 compressed_cose = base45.b45decode(plain_input) # Décompression ZIP si nécessaire cose = compressed_cose if compressed_cose[0] == 0x78: fb = compressed_cose[1] if fb == 0x01 or fb == 0x5E or fb == 0x9C or fb == 0xDA: cose = zlib.decompress(compressed_cose) # Chargement de l'objet CBOR obj = cbor2.loads(cose) header = cbor2.loads(obj.value[0]) payload = cbor2.loads(obj.value[2]) signature = obj.value[3] if check_signature: # Récupération du certificat utilisé kid = base64.b64encode(header[4]).decode() cert_name = kid.replace('/', '_') base_dir = os.path.dirname(__file__) cert_path = os.path.join(base_dir, 'certs', f"{cert_name}.pem") if not os.path.isfile(cert_path): print(f"Le certificat {kid} n'a pas été trouvé.") print("Utilisez l'option --dontcheck pour sauter " "la vérification.") print("Si vous disposez du certificat, installez-le dans", cert_path) exit(1) with open(os.path.join(base_dir, 'certs', f"{cert_name}.pem")) as f: cert_asc = f.read() cert = x509.load_pem_x509_certificate(cert_asc.encode()) public_key = cert.public_key() # Calcul de la bonne signature et des données signées data = cbor2.dumps( ["Signature1", cbor2.dumps(header), bytes(), cbor2.dumps(payload)] ) l = len(signature) r = int.from_bytes(signature[:l // 2], 'big') s = int.from_bytes(signature[l // 2:], 'big') signature = encode_dss_signature(r, s) try: # Vérification de la signature public_key.verify(signature, data, ec.ECDSA(cert.signature_hash_algorithm)) valid = True except: valid = False else: valid = True print("Attention : la signature du QR code n'a pas été vérifiée.") # Information utile p = payload[-260][1] if 'v' in p: # Les vaccins sont valides 7 jours après la dernière dose vaccin = p['v'][0] # Toutes les doses sont requises valid = valid and vaccin['dn'] == vaccin['sd'] # Vérification de la date date = datetime.date.fromisoformat(vaccin['dt']) today = datetime.date.today() delta = today - date valid = valid and delta.days >= 7 elif 't' in p: # Les tests négatifs sont valables moins de 72h test = p['t'][0] assert test['tr'] in ['260373001', '260415000'] if test['tr'] == 260373001: # Test positif valid = False test_date = datetime.datetime.fromisoformat(test['sc']) tzinfo = test_date.tzinfo delta = datetime.datetime.now(tzinfo) - test_date valid = valid and delta.days < 3 # 3 jours de validité elif 'r' in p: # les tests positifs entre 11 et 182 jours après test = p['r'][0] valid_from = datetime.datetime.fromisoformat(test['df']) valid_until = datetime.datetime.fromisoformat(test['du']) tzinfo = valid_from.tzinfo valid = valid and \ valid_from <= datetime.datetime.now(tzinfo) <= valid_until else: print("Type de passe inconnu.") if valid: print("Pass sanitaire valide") print("Nom :", p['nam']['fn']) print("Prénom :", p['nam']['gn']) print("Date de naissance :", p['dob']) if additional_info: # TODO Meilleur affichage if 'v' in p: # Vaccination print("Informations de vaccination :", json.dumps(p['v'][0], indent=2)) elif 't' in p: print("Informations de test :", json.dumps(p['t'][0], indent=2)) elif 'r' in p: print("Informations de test :", json.dumps(p['r'][0], indent=2)) else: print("Pass sanitaire invalide") return valid def main(): parser = argparse.ArgumentParser() parser.add_argument('file', nargs='?', type=argparse.FileType('r'), default=sys.stdin, help="QR Code à lire, en format texte.") parser.add_argument('--full', '-f', action='store_true', help="Affiche toutes les informations.") parser.add_argument('--dontcheck', action='store_true', help="Ne pas vérifier la signature.") args = parser.parse_args() qrcode = read_qrcode(args.file) analyse_qrcode(qrcode, args.full, not args.dontcheck)