Le QR-code contient un fichier binaire encodé en base 45, précdé d'un en-tête HC1:
.
Ce fichier est compressé au format zlib.
Sa décompression est une structure CBOR.
Le pass est rendu infalsifiable grâce à une signature numérique utilisant les courbes elliptiques.
Les modules base45, cbor2 et ecdsa sont à installer préalablement.
import base45, cbor2, zlib, json
from time import ctime
from base64 import *
import codecs
import ecdsa
# un exemple de données lues en flashant un QR-code
x = '''HC1:6BFOXN%TSMAHN-H3YS1IK47ES6IXJR4E47X5*T917VF+UOGIS1RYZV:X9:IMJZTCV4*XUA2PSGH.+H$NI4L6HUC%UG/YL WO*Z7ON13:LHNG7H8H%BFP8FG4T 9OKGUXI$NIUZUK*RIMI4UUIMI.J9WVHWVH+ZEOV1AT1HRI2UHD4TR/S09T./08H0AT1EYHEQMIE9WT0K3M9UVZSVV*001HW%8UE9.955B9-NT0 2$$0X4PCY0+-CVYCRMTB*05*9O%0HJP7NVDEBO584DKH78$ZJ*DJWP42W5P0QMO6C8PL353X7H1RU0P48PCA7T5MCH5:ZJ::AKU2UM97H98$QP3R8BH9LV3*O-+DV8QJHHY4I4GWU-LU7T9.V+ T%UNUWUG+M.1KG%VWE94%ALU47$71MFZJU*HFW.6$X50*MSYOJT1MR96/1Z%FV3O-0RW/Q.GMCQS%NE'''
x
len(set(x)) # il y a bien 45 caractères différents ...
# Il faut retirer l'en-tête HC1:
base45.b45decode(x[4:])
data = _
zlib.decompress(data)
# C'est une structure CBOR qu'il faut décoder
cb = _
cbor2.decoder.loads(cb)
Le tag 18 indique un COSE Single Signer Data Object.
loads = _
loads.value
Ce premier décodage produit une liste de 4 champs.
Le premier est un dictionnaire dont la clé 1 (valeur -7) indique l'algorithme cryptographique utilisé, et la clé 4 est une valeur binaire dont l'encodage en base 64 donne l'identifiant (kid) de la clé publique.
Le second champ est un dictionnaire inutilisé (vide).
Le troisième contient les informations du certificat, et le quatrième est une signature numérique.
# Ici, les données sont un peu étranges, le pass n'est certainement pas valide !
import pprint
for a in loads.value:
try:
pprint.pprint (cbor2.decoder.loads(a))
except: print ('****', str(a))
ctime(1697234400) # 4: est censé être la date d'émission (2023=erreur ?)
ctime(1635199742) # 6: est censé être la date de fin de validité (erreur ?)
D'après le site :
Le champ annoté -260 contient les faits à l'origine du certificat.
# le kid (key identifier)
b64encode(b'\xe7qN\x8d\x7f\xf8h\x9b')
kid = _.decode('ascii')
Les clés publiques du système sont rassemblées dans un fichier json
publiquement accessible.
kid
!grep -B 0 -C 16 53FOjX/4aJs= Digital_Green_Certificate_Signing_Keys.json
kid = b64encode(cbor2.decoder.loads(loads.value[0])[4]).decode('ascii')
kid
with open('Digital_Green_Certificate_Signing_Keys.json') as f:
keys = json.load(f)
keypem = keys[kid]['publicKeyPem']
keypem
Le module ecdsa
(Elliptic Curve Digital Signature Algorithm) permet de vérifier la signature (à condition de savoir
comment le message est composé). Il faut préciser la fonction de hachage (sha256 ici).
ecdsa.VerifyingKey.from_pem(keypem, hashfunc=ecdsa.util.sha256)
vk = _
# On récupère la signature
sg = loads.value[3]
sg
len(sg)
Le message à signer est un tableau CBOR composé de la chaîne "Signature1", du champ 0, une chaîne vide, et le champ 2 (données) de la structure originale.
info = loads.value[2]
xx = [ "Signature1", loads.value[0], b'', info]
msg = cbor2.encoder.dumps(xx)
xx
cbor2.encoder.dumps(xx)
msg=_
vk.verify(sg,msg)
La signature est valide ! L'infalsifiable a été falsifié !
Que s'est il passé ?
L'enquête est en cours, voir cet article.