Version fonctionnelle

Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
This commit is contained in:
Yohann D'ANELLO 2021-08-08 14:43:02 +02:00
commit cfbba31711
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
6 changed files with 275 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__
*.pyc
*.txt

64
README.rst Normal file
View File

@ -0,0 +1,64 @@
Vérification de pass sanitaire
==============================
Ce script permet de contrôler la validité d'un pass sanitaire
européen, tel que le fait l'application TAC-Vérif lors de
la crise sanitaire de Covid-19 en 2021.
Usage
-----
.. code:: bash
./main.py [file] [--full] [--dontcheck]
Le script prend en entrée un QR code au format textuel, et affiche l'état
de validité du pass sanitaire.
Le QR code doit être donné dans sa version textuelle.
Il peut être combiné avec des outils tels que ``zbar``.
Si vous disposez d'une webcam :
.. code:: bash
zbarcam -1 --raw | ./main.py
Ou en cas de QR code sur une image :
.. code:: bash
zbarimg -1 --raw [file] | ./main.py
En mode normal, seuls le nom, le prénom et la date de naissance sont
affichés, sous réserve de pass valide. Avec l'option ``--full``,
les informations de vaccination ou de test sont affichées.
Enfin, l'option ``--dontcheck`` saute la vérification de la signature.
Cela peut être utile si vous ne disposez pas des certificats de signature.
Vérification de signature
-------------------------
Les certificats de signature n'étant pas publics, ils ne sont pas partagés
avec le code. De fait, si vous ne les possédez pas, vous ne pourrez pas
vérifier les signatures, et vous devrez utiliser l'option ``--dontcheck``.
Pour installer des certificats, placez-les dans le dossier ``certs`` avec
pour nom ``{{{KID}}.pem``, où ``{{KID}}`` est l'identifiant du certificat.
Si ``{{KID}}`` contient un ``/``, remplacez-le par un ``_``.
Modalités d'utilisation
-----------------------
Ce module peut être utilisé à des fins privés afin de mieux comprendre
la structure d'un pass sanitaire et des données stockées, mais ne peut pas
être utilisé pour contrôle tel que la loi n° 2021-1040 le précise.
Merci d'utiliser l'application officielle « TousAntiCovid - Vérif » sur
téléphone pour une utilisation normale de vérification de pass sanitaire,
si vous êtes autorisé⋅e à l'utiliser.

7
main.py Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env python
from pscheck import pscheck
if __name__ == '__main__':
pscheck.main()

0
pscheck/__init__.py Normal file
View File

61
pscheck/base45.py Normal file
View File

@ -0,0 +1,61 @@
"""
BSD 2-Clause License
Copyright (c) 2021, Kirei AB
All rights reserved.
"""
from typing import Union
BASE45_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
BASE45_DICT = {v: i for i, v in enumerate(BASE45_CHARSET)}
def b45encode(buf: bytes) -> bytes:
"""Convert bytes to base45-encoded string"""
res = ""
buflen = len(buf)
for i in range(0, buflen & ~1, 2):
x = (buf[i] << 8) + buf[i + 1]
e, x = divmod(x, 45 * 45)
d, c = divmod(x, 45)
res += BASE45_CHARSET[c] + BASE45_CHARSET[d] + BASE45_CHARSET[e]
if buflen & 1:
d, c = divmod(buf[-1], 45)
res += BASE45_CHARSET[c] + BASE45_CHARSET[d]
return res.encode()
def b45decode(s: Union[bytes, str]) -> bytes:
"""Decode base45-encoded string to bytes"""
try:
if isinstance(s, str):
buf = [BASE45_DICT[c] for c in s.strip()]
elif isinstance(s, bytes):
buf = [BASE45_DICT[c] for c in s.decode()]
else:
raise TypeError("Type must be 'str' or 'bytes'")
buflen = len(buf)
if buflen % 3 == 1:
raise ValueError("Invalid base45 string")
res = []
for i in range(0, buflen, 3):
if buflen - i >= 3:
x = buf[i] + buf[i + 1] * 45 + buf[i + 2] * 45 * 45
if x > 0xFFFF:
raise ValueError
res.extend(divmod(x, 256))
else:
x = buf[i] + buf[i + 1] * 45
if x > 0xFF:
raise ValueError
res.append(x)
return bytes(res)
except (ValueError, KeyError, AttributeError):
raise ValueError("Invalid base45 string")

140
pscheck/pscheck.py Normal file
View File

@ -0,0 +1,140 @@
#!/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 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.
"""
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.")
# TODO: Vérifier les données d'un test
if valid:
print("Pass sanitaire valide")
# Information utile
p = payload[-260][1]
print("Nom :", p['nam']['fn'])
print("Prénom :", p['nam']['gn'])
print("Date de naissance :", p['dob'])
if additional_info:
if 'v' in p:
# Vaccination
# TODO Meilleur affichage
print("Informations de vaccination :", p['v'])
else:
# TODO Gérer les tests positifs / négatifs
print("Informations de test :", p)
else:
print("Pass sanitaire invalide")
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)