Évitons les malentendus. Je suis vacciné, et je crois qu’il faut se faire vacciner. C’est juste mon avis, je ne cherche à convaincre personne ni de ça ni du contraire. Le présent billet a pour objectif de vous faire découvrir quelles sont les informations qui sont encodées dans le QR code d’un pass vaccinal. Il s’agit d’informations factuelles et techniques uniquement.

Après vaccination, le papier qu’on vous remet contient généralement deux images :

Que ce soit le 2D-Doc ou le QR code, ils peuvent être lus avec la plupart des applis (téléphone ou ordi). Toutefois le pass français et le pass européen ne contiennent pas les mêmes informations (je n’ai pas de pass français sous la main pour vérifier, mais j’aimerais bien pouvoir le faire).

Attention, n’utilisez pas un décodeur de QRCode ou 2D-Doc en ligne. Vous ne saurez jamais ce qu’il advient des infos envoyées (sont-elles stockées par le serveur ? réutilisées ?) Il n’y a rien de très confidentiel dans ces codes, mais tout de même… après ça vous ne pourrez plus mentir sur votre âge.

Dans le cas du 2D-Doc, la page Wikipédia est un bon départ : 2D-Doc. En particulier on trouvera le lien vers la spécification technique (joli PDF de 170 pages). À noter que ces 2D-Doc sont utilisés dans d’autres contextes administratifs (en France), et qu’il est probablement intéressant d’y regarder de plus près. Mais depuis le premier juillet, les pass sanitaires sont des pass européens. Exit le 2D-Doc. Bonjour le QR-Code.

La manière d’encoder une chaîne dans un QR-Code n’est pas nouvelle. On trouve facilement des informations partout. Une fois le QR-Code décodé, on obtient une chaîne de caractères incompréhensible, qui dans le cas du pass sanitaire, commence par HC1:. La compréhension de cette chaîne nécessite de lire les spécifications associées, disponibles dans un document intitulé : Guidelines on Technical Specifications for Digital Green Certificates Volume 3 Interoperable 2D Code (en ligne ici).

On apprend page 6 que ce qui est encodé est un objet CBOR (Concice Binary Objet Representation, voir la RFC8949). On apprend toujours page 6 que le préfixe identifiant le type d’informations est HC1 (Health Certificate Version 1) ou HC2. Le mien est un HC1. On peut ensuite enlever le préfixe, et on a l’objet CBOR, qui est compressé (zlib), puis encodé en Base45. Il faut donc procéder en sens inverse : décode le base45, décompresser, et décoder l’objet CBOR.

Là aussi, Base45 ou Zlib sont connus depuis longtemps. On trouve des informations un peu partout (toutefois, pas d’encodage Base45 par défaut dans Python, il faut un module tiers).

La récupération de l’objet CBOR le fait donc en suivant ces étapes :

On peut décode l’objet CBOR en lisant la RFC… ou en utilisant le module Python cbor2 (par exemple). On obtient alors 4 “morceaux”, dont le deuxième est vide.

Le premier morceau est le signing header. C’est aussi un objet CBOR, qu’on décode pour obtenir 2 champs. Le champ tagué 1 et le champ tagué 4 (c’est le tag des champs qui est dans les données, et on obtient leur nom dans la spec Digital Green Certificates. Pour moi le champ 1 vaut -7 et le champ 4 est une série de 8 octets. La spec nous indique (il faut en consulter plusieurs…) que -7 correspond à ECDSA 256, (soit algorithme de signature numérique par courbe élliptiques). La série de 8 octets est le début d’un hash qui permet probablement de rapidement rejeter le document s’il est mal construit.

Le deuxième morceau est vide.

Le troisième est celui qui contient le payload : nom prénom etc… On trouve les premières infos dans la même spec, section 2.6.3 Common Payload Values. Puis la suite est dans un autre document nommé : Guidelines on Technical Specifications for EU Digital COVID Certificates : JSON Schema Specification (en ligne ici) on y a la description de la partie utile du payload, qui est au format JSON.

Dedans, il y a :

Et dans le cas d’une vaccination (s’il y a un v donc…) :

Dans le pass européen délivré depuis le 1er juillet 2021, tout est ceci indiqué.
Encore une fois ce n’est pas (vraiment) un secret… sauf s’il advient à un moment qu’on ne veut plus des personnes ayant reçu tel type de vaccin ou tel autre, et ça pour de mauvaises raisons.

Il est nécessaire de comprendre que laisser son QR-code exposé, c’est donner toutes ces informations. Certes, les applications dédiées qui vérifient la validité des pass sanitaires n’affichent que le nom, la date de naissance, et la validité du pass (et c’est normal, on ne peut pas faire moins, car il faut bien pouvoir vérifier l’identité du porteur), mais rien n’empêche d’utiliser une autre application ou de faire une photo du QR code pour l’analyser ensuite. Il ne s’agit pas d’être paranoïaque, mais juste de savoir de quoi il retourne.

Enfin, les champs qui indiquent le fabricant, le type et le produit donnent juste un numéro ou une référence. Ces informations sont détaillées dans ce document Value sets for EU Digital Covid Certificates

Pour finir, le quatrième morceau semble être la signature… j’ai arrêté là les investigations pour le moment.

Ensemble des documents eHealth Network : ici ou .

Bout de code pour obtenir les infos à partir d’une image (propre) du QR-Code (tout le travail avait déjà été fait par @hannob)

import sys
import zlib
import pprint
# Modules tiers (Pillow, Pyzbar, base45, cbor2)
import PIL.Image
import pyzbar.pyzbar
import base45
import cbor2

img = PIL.Image.open("qrcode.png")
data = pyzbar.pyzbar.decode(img)
cert = data[0].data.decode()
b45data = cert.replace("HC1:", "")
zlibdata = base45.b45decode(b45data)
cbordata = zlib.decompress(zlibdata)
decoded = cbor2.loads(cbordata)
print("Header\n----------------");
pprint.pprint(cbor2.loads(decoded.value[0]))
print("\nPayload\n----------------");
pprint.pprint(cbor2.loads(decoded.value[2]))
print("\nSignature ?\n----------------");
print(decoded.value[3])