Version fonctionnelle

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

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)