Qualche settimana fa ho scaricato il mio EU Digital Covid Certificate e mi sono un pochino incuriosito.
Ma cosa diavolo è contenuto dentro quel QR Code?
Vediamo di dare una occhiata. La prima cosa da fare è decodificare il QRCode e vedere che cosa contiene. Per qualche strana ragione la mia webcam oggi non ne voleva sapere di funzionare con OpenCV. Un peccato perché con un paio di righe codice avrei semplicemente decodificato il QRCode. Nessun problema. Online esistono una quantità enorme di lettori di QRCode. Ho usato uno di questi ed in pochi secondi mi sono trovato con la stringa contenente i dati.
Dato che non credo sia una buona idea condividere con voi tutti i miei dati personali, usiamo dei dati di test per dare una idea.
Il contenuto del QRCode è questo:
HC1:6BFOXN%TS3DH0YOJ58S S-W5HDC *M0II5XHC9B5G2+$N IOP-IA%NFQGRJPC%OQHIZC4.OI1RM8ZA.A5:S9MKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HB2PL/IB*09B9LW4T*8+DCMH0LDK2%K:XFE70*LP$V25$0Q:J:4MO1P0%0L0HD+9E/HY+4J6TH48S%4K.GJ2PT3QY:GQ3TE2I+-CPHN6D7LLK*2HG%89UV-0LZ 2ZJJ524-LH/CJTK96L6SR9MU9DHGZ%P WUQRENS431T1XCNCF+47AY0-IFO0500TGPN8F5G.41Q2E4T8ALW.INSV$ 07UV5SR+BNQHNML7 /KD3TU 4V*CAT3ZGLQMI/XI%ZJNSBBXK2:UG%UJMI:TU+MMPZ5$/PMX19UE:-PSR3/$NU44CBE6DQ3D7B0FBOFX0DV2DGMB$YPF62I$60/F$Z2I6IFX21XNI-LM%3/DF/U6Z9FEOJVRLVW6K$UG+BKK57:1+D10%4K83F+1VWD1NE
Bellino, non è vero? Ad ogni modo, se vi interessa, i dati di test li ho presi qui: https://github.com/eu-digital-green-certificates/dgc-testdata/tree/main/IT/2DCode/raw
Bene. ora abbiamo i dati del QRCode ma che cavolo c’è dentro.
Vediamo innanzitutto il processo che viene usato per creare il QRCode:
Direi che è sufficientemente chiaro. Se volete saperne un pochino di più, leggete qui.
A questo punto noi dobbiamo percorrere la catena a ritroso. In realtà quello che ci interessa è il contenuto originale del JSON Document, il secondo elemento della catena, perché non è nostra intenzione verificare l’autenticità della firma digitale ma solo esplorare il contenuto.
Primo passo. Levarci di torno i primi caratteri ed ottenere una bella stringhetta Base45. Eccola qui:
6BFOXN%TS3DH0YOJ58S S-W5HDC *M0II5XHC9B5G2+$N IOP-IA%NFQGRJPC%OQHIZC4.OI1RM8ZA.A5:S9MKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HB2PL/IB*09B9LW4T*8+DCMH0LDK2%K:XFE70*LP$V25$0Q:J:4MO1P0%0L0HD+9E/HY+4J6TH48S%4K.GJ2PT3QY:GQ3TE2I+-CPHN6D7LLK*2HG%89UV-0LZ 2ZJJ524-LH/CJTK96L6SR9MU9DHGZ%P WUQRENS431T1XCNCF+47AY0-IFO0500TGPN8F5G.41Q2E4T8ALW.INSV$ 07UV5SR+BNQHNML7 /KD3TU 4V*CAT3ZGLQMI/XI%ZJNSBBXK2:UG%UJMI:TU+MMPZ5$/PMX19UE:-PSR3/$NU44CBE6DQ3D7B0FBOFX0DV2DGMB$YPF62I$60/F$Z2I6IFX21XNI-LM%3/DF/U6Z9FEOJVRLVW6K$UG+BKK57:1+D10%4K83F+1VWD1NE
Uno sforzone… abbiamo solo cancellato i primi quattro caratteri della stringa di prima.
Ora decodifichiamo i dati in Base45. Otteniamo questo:
b'x\x9c\xbb\xd4\xe2\xbb\x88\xc5\xc3\xd2@<\xe3\xec-VaF\xb5\x05\x91\x8c\x8cKX\xa4\x12\xa7\xbc\x98\xc1&\x95\xb0\xbc\xa7\x831\xc93\xc4\x92\x91y!\xe3\x92\xc4\xb2\xc6UI)yLI\xb9\x89\xb9\xfeA\xee\xba\x86\x06\x06\x06\xc6\x06F\x86\xa6Ie\x05Y\x86\x86\x86\x96\xc6&\x96\x06\x06\xe6I)%YF@a]\x03\x13\xa0\x92\xa4\xe4|\xa0\x01I\xc9\x99\x15j\x06\x86\x9e!\xae\xe6\xc6\x06\x06\xae\x86\x8eNF\x8e\x16&\xce\xe6\x86@\r&n\x86\x06\xc6.\xceN\x86n\xe6\x06\x8e\xcafI\xb9\x059\xae\xa1\xfa\x86\xfaF\x06\xfa\x86\xa6F\x16I\x99\xc5 \x03\x8aS\x98\x92J\xd23-L\x0cL\x8d\x81\x9a\xcc\x92\xf3\x12s\x97$\xa7\xe5\x95d\xbax\xda8;\x06\x04y\xfa\'\xa5\xe5e\xbad*8\'\x16\x14e\xe6\'\xa7\xe7\x95\xe4\xfa:\x06y\xfa\x84\xda\x84\xb8\x06\xb9\x06;&\xa5\xe7\xe5\xf9&\x16e\xe6\x1c\xde\xa9\x10\x92Z\x94Z\x9c\x98\\\x06\xa4\x0c\xf5\x0c\xf4\x0c\x92S\xf2\x93\xb2\x0c-\xcd\xcdu\r\xcct\r\xcd"\x1c\x96\xbc\x9b vp\xb9\xcf\xf99\xabv\xb2z\xeaf\xf6gNZ\xf1\xcb\xe0\x10\xef6\t~6\x16\x1e\x8f\x82\x15\xae\xbb\xbd\xadd\x1e\x7fy\x1b4\xe7x_\x9a\x91\xaa\xfb55\xf3\xe3^\x1b+%\x0f\xac\nZ&R\x197\x0f\x00l\x94r\xca'
Dalla immagine che abbiamo utilizzato prima sappiamo che questi dati sono compressi con zlib. Decomprimiamoli ed otteniamo questo:
b'\xd2\x84M\xa2\x04H90\x17h\xcd\xda\x05\x13\x01&\xa0Y\x01\x01\xa4\x04\x1aa\x94\xe8\x98\x06\x1a`\xa7\x8c\x88\x01bIT9\x01\x03\xa1\x01\xa4av\x81\xaabdn\x02bmamORG-100030215bvpj1119349007bdtj2021-04-10bcobITbcix&01ITE7300E1AB2A84C719004F103DCB1F70A#6bmplEU/1/20/1528bisbITbsd\x02btgi840539006cnam\xa4cfntiDI<CAPRIObfniDi CapriocgntmMARILU<TERESAbgnnMaril\xc3\xb9 Teresacvere1.0.0cdobj1977-06-16X@\xa4\xee\x90\x16\xc1\xa7L\xcf\x9c\xaa\xb9\x05I-i\x8fi\x92\xa8\xfa0\xc2\r\xb6\x18\x0f\x06\x04\x0cHp\xa8E\xbbK:\x1c\xe3\xf4\xedR\x9c\xc7\x8ef2%G\xd6&7\xc7J\xb1y\x19\xc0\xaaR\xa6\x14y^\x9e'
Cominciamo a vedere qualcosa di intellegibile. Sempre usando come riferimento l’immagine sappiamo che la rappresentazione di questi dati è COSE: CBOR Object Signing and Encryption.
Da questo documento capiamo che ci sono due header, un payload e la firma digitale. Facciamo un altro giro di decodifica ed otteniamo questo:
{4: 1637148824, 6: 1621593224, 1: 'IT', -260: {1: {'v': [{'dn': 2, 'ma': 'ORG-100030215', 'vp': '1119349007', 'dt': '2021-04-10', 'co': 'IT', 'ci': '01ITE7300E1AB2A84C719004F103DCB1F70A#6', 'mp': 'EU/1/20/1528', 'is': 'IT', 'sd': 2, 'tg': '840539006'}], 'nam': {'fnt': 'DI<CAPRIO', 'fn': 'Di Caprio', 'gnt': 'MARILU<TERESA', 'gn': 'Marilù Teresa'}, 'ver': '1.0.0', 'dob': '1977-06-16'}}}
Direi che siamo quasi alla fine. E’ il JSON Document che stavamo cercando. Facciamo un pretty print del file e finalmente abbiamo il risultato che cercavamo:
{
"4": 1637148824,
"6": 1621593224,
"1": "IT",
"-260": {
"1": {
"v": [
{
"dn": 2,
"ma": "ORG-100030215",
"vp": "1119349007",
"dt": "2021-04-10",
"co": "IT",
"ci": "01ITE7300E1AB2A84C719004F103DCB1F70A#6",
"mp": "EU/1/20/1528",
"is": "IT",
"sd": 2,
"tg": "840539006"
}
],
"nam": {
"fnt": "DI<CAPRIO",
"fn": "Di Caprio",
"gnt": "MARILU<TERESA",
"gn": "Maril\u00f9 Teresa"
},
"ver": "1.0.0",
"dob": "1977-06-16"
}
}
}
Lo schema del file JSON è specificato qui.
E quindi non c’è niente di magico all’interno del QRCode. Sono quasi deluso :-). Diciamo che, comunque, Marilu è un bel nome per dei dati di test. Chissà se è davvero casuale o se è la fidanzata, o la mamma, di qualcuno che ha contribuito a scrivere le specifiche od il codice.
Se proprio volete ecco il codice che ho usato per fare tutto questo. Quick and dirty, ovviamente:
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
from base45 import b45decode
import cv2
import flynn
import zlib
import json
data = "6BFOXN%TS3DH0YOJ58S S-W5HDC *M0II5XHC9B5G2+$N IOP-IA%NFQGRJPC%OQHIZC4.OI1RM8ZA.A5: S9MKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HB2PL/IB*09B9LW4T*8+DCMH0LDK2%K:XFE70*LP$V25$0Q:J:4MO1P0%0L0HD+9E/HY+4J6TH48S%4K. GJ2PT3QY:GQ3TE2I+-CPHN6D7LLK*2HG%89UV-0LZ 2ZJJ524-LH/CJTK96L6SR9MU9DHGZ%P WUQRENS431T1XCNCF+47AY0-IFO0500TGPN8F5G. 41Q2E4T8ALW.INSV$ 07UV5SR+BNQHNML7 /KD3TU 4V*CAT3ZGLQMI/XI%ZJNSBBXK2:UG%UJMI:TU+MMPZ5$/PMX19UE:-PSR3/ $NU44CBE6DQ3D7B0FBOFX0DV2DGMB$YPF62I$60/F$Z2I6IFX21XNI-LM%3/DF/U6Z9FEOJVRLVW6K$UG+BKK57:1+D10%4K83F+1VWD1NE"
print(f"Raw Data: <{data}>")
decoded_data = b45decode(data)
print(f"Decoded Data: <{decoded_data}>")
decompressed_data = zlib.decompress(decoded_data)
print(f"Decompressed Data: <{decompressed_data}>")
(_, (h1, h2, payload, signature)) = flynn.decoder.loads(decompressed_data)
data = flynn.decoder.loads(payload)
print(f"Certificate Data: <{data}>")
print(f"JSON Data:")
print(json.dumps(data, indent=4))
print(f"Header 1: <{h1}>")
print(f"Header 2: <{h2}>")
print(f"Signature: <{signature}>")
Ovviamente in Python 🙂
Il contenuto del file “vc.txt”, utilizzato dal codice, è questo:
6BFOXN%TS3DH0YOJ58S S-W5HDC *M0II5XHC9B5G2+$N IOP-IA%NFQGRJPC%OQHIZC4.OI1RM8ZA.A5:S9MKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HB2PL/IB*09B9LW4T*8+DCMH0LDK2%K:XFE70*LP$V25$0Q:J:4MO1P0%0L0HD+9E/HY+4J6TH48S%4K.GJ2PT3QY:GQ3TE2I+-CPHN6D7LLK*2HG%89UV-0LZ 2ZJJ524-LH/CJTK96L6SR9MU9DHGZ%P WUQRENS431T1XCNCF+47AY0-IFO0500TGPN8F5G.41Q2E4T8ALW.INSV$ 07UV5SR+BNQHNML7 /KD3TU 4V*CAT3ZGLQMI/XI%ZJNSBBXK2:UG%UJMI:TU+MMPZ5$/PMX19UE:-PSR3/$NU44CBE6DQ3D7B0FBOFX0DV2DGMB$YPF62I$60/F$Z2I6IFX21XNI-LM%3/DF/U6Z9FEOJVRLVW6K$UG+BKK57:1+D10%4K83F+1VWD1NE
L’operazione inversa è altrettanto semplice ed è sufficiente seguire i passi illustrati nella decodifica in ordine inverso e codificare invece di decodificare. Ovviamente data la natura della creazione del codice QR non sarai mai in grado di ottenere un codice QR in grado di passare la verifica della applicazione perché non essendo in possesso delle chiavi digitali non sarai in grado di firmarlo in maniera valida.
Ottimo articolo!
Piuttosto, se volessi fare l’operazione inversa come si procede?
Grazie
Intepretazione corretta. Puoi creare semplicemente un QR Code valido e con dati verosimili. I dati anagrafici sarebbero, ovviamente, corretti mentre i dati relativi alla vaccinazione devi necessariamente inventarli. Un qualsiasi lettore di QR Code riporterebbe i dati corretti, sebbene in un formato non proprio leggibile. L’applicazione ufficiale di verifica non riconoscerebbe il codice come valido per via della assenza della firma digitale ottenuta con le credenziali in possesso del Governo.
Grazie quindi con a logica, con questo controllo, un falso non funzionerebbe.
Cioè non è che bisogna prendere le chiavi pubbliche e fare la verifica?
Immagina che una malintenzionato crea tutto con la logica corretta e la sua firma…. senza controllo tu leggi i dati lo stesso…?
Grazie ancora
Ciao Paolo,
Dal punto di vista tecnico fare il controllo della firma è una cosa decisamente banale e te la cavi con due o tre righe di codice.
Il problema, insormontabile, è che per fare questo controllo avrei bisogno di essere in possesso delle chiavi private con cui viene firmato il certificato digitale prodotto. Queste chiavi sono in possesso solo ed esclusivamente, almeno mi auguro sia così, del Governo.
Se io avessi le chiavi sì che potrei creare dei falsi e non il contrario.
ciao ma il controllo della firma non lo fai? Potrebbe esserci il caso in cui viene generato un falso e firmato in altro modo? GRAAZIE