Version fonctionnelle
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
This commit is contained in:
commit
cfbba31711
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.txt
|
64
README.rst
Normal file
64
README.rst
Normal 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
7
main.py
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from pscheck import pscheck
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pscheck.main()
|
0
pscheck/__init__.py
Normal file
0
pscheck/__init__.py
Normal file
61
pscheck/base45.py
Normal file
61
pscheck/base45.py
Normal 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
140
pscheck/pscheck.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user