2021-08-08 12:43:02 +00:00
|
|
|
#!/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
|
2021-08-16 15:41:07 +00:00
|
|
|
import datetime
|
2021-08-08 12:43:02 +00:00
|
|
|
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:
|
2021-08-23 11:18:59 +00:00
|
|
|
content = file.readline()
|
2021-08-08 12:43:02 +00:00
|
|
|
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.
|
2021-08-16 15:41:07 +00:00
|
|
|
|
|
|
|
Renvoie la validité du pass sous forme de booléen.
|
2021-08-08 12:43:02 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
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.")
|
|
|
|
|
2021-08-16 15:41:07 +00:00
|
|
|
# Information utile
|
|
|
|
p = payload[-260][1]
|
|
|
|
|
|
|
|
if 'v' in p:
|
2021-08-17 17:51:38 +00:00
|
|
|
# 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
|
2021-08-16 15:41:07 +00:00
|
|
|
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.")
|
|
|
|
|
2021-08-08 12:43:02 +00:00
|
|
|
|
|
|
|
if valid:
|
|
|
|
print("Pass sanitaire valide")
|
|
|
|
|
|
|
|
|
|
|
|
print("Nom :", p['nam']['fn'])
|
|
|
|
print("Prénom :", p['nam']['gn'])
|
|
|
|
print("Date de naissance :", p['dob'])
|
2021-08-16 15:41:07 +00:00
|
|
|
|
2021-08-08 12:43:02 +00:00
|
|
|
if additional_info:
|
2021-08-16 15:41:07 +00:00
|
|
|
# TODO Meilleur affichage
|
2021-08-08 12:43:02 +00:00
|
|
|
if 'v' in p:
|
|
|
|
# Vaccination
|
2021-08-16 15:41:07 +00:00
|
|
|
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))
|
2021-08-08 12:43:02 +00:00
|
|
|
else:
|
|
|
|
print("Pass sanitaire invalide")
|
|
|
|
|
2021-08-16 15:41:07 +00:00
|
|
|
return valid
|
|
|
|
|
2021-08-08 12:43:02 +00:00
|
|
|
|
|
|
|
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)
|
2021-08-17 18:10:49 +00:00
|
|
|
valid = analyse_qrcode(qrcode, args.full, not args.dontcheck)
|
|
|
|
|
|
|
|
exit(0 if valid else 2)
|