ottobre 29, 2021 · greenpass python

Verificare un Green Pass

Nota: questo post è stato modificato dopo la pubblicazione iniziale poichè sono riuscito a usare le chiavi pubbliche prelevate dalla fonte ufficiale italiana (la stess ausata dall'app Verifica C19) anzichè da un indirizzo qualunque.

Come promesso, proseguo quando iniziato nel precedente post che vi consiglio di andare a leggere :-).

Con quanto scritto l'ultima volta eravamo arrivati al punto di estrarre tutte le informazioni dal QR code di un Green Pass. Il passo successivo è verificarlo, cioè assicurarsi che il contenuto provenga da una fonte attendibile e non sia stato alterato.

Come funziona una firma digitale

Il meccanismo si basa sul concetto di firma digitale mediante doppia chiave. Non voglio entrare troppo in dettaglio perchè non è il mio campo, ma a grandi linee funziona così. L'ente certificatore (suppongo il ministero della salute) crea due chiavi digitali complementari, che possiamo immaginare come due file contenenti dei grossi numeri generati in maniera casuale ma seguendo delle regole molto precise. Uno di questi file lo chiamiamo "chiave pubblica", l'altro "chiave privata".

La magia della cifratura in doppia chiave è che tutto quello che viene cifrato con la chiave privata può essere decifrato esclusivamente con la chiave pubblica corrispondente.

Il ministero, preso atto della vaccinazione o circostanze equivalenti, scrive i nostri dati (quelli che abbiamo visto nello scorso post) in un file, e ne calcola un hash. Un hash, per semplificare, è una stringa che descrive in maniera compatta i nostri dati.

A questo punto questo hash viene criptato mediante la chiave privata (diventa così la firma digitale) e viene inserito nel green pass, all'interno del messaggio COSE di cui abbiamo parlato in precedenza.

Per controllare se il mio messaggio è firmato correttamente, cerco di decodificare la firma con la chiave pubblica, che è a disposizione di tutti. Se la decodifica va a buon fine, ho la garanzia che la firma è stata prodotta da un ente accreditato (ricordiamo che quanto cifrato con una chiave privata può essere decifrato solo dalla corrispondente chiave pubblica). Provo poi a calcolare per conto mio l'hash del messaggio, e se l'hash calcolato corrisponde a quello estratto dalla firma ho anche la garanzia che il messaggio, dopo essere stato cifrato, non è stato alterato (altrimenti gli hash non corrisponderebbero).

Recuperare i certificati validi

I passaggi di cui sopra fortunatamente sono standard e vengono fatti in automatico dalla libreria COSE in Python. Il lavoro maggiore, nel nostro caso, è recuperare un elenco di chiavi pubbliche dei diversi enti che sono autorizzati a rilasciare Green Pass. In Italia, l'unico strumento riconosciuto per la verifica di un Green Pass è l'app VerificaC19. Inspiegabilmente, non sono riuscito a trovare una fonte ufficiale da cui procurarmi i certificati accettati dall'autorità italiana, ma fortunatamente l'app ufficiale è open source, ragion per cui è possibile analizzarne il codice e scoprire dove si procura i suddetti certificati.

In sostanza, il ministero ha predisposto un indirizzo web a cui fare una richiesta: https://get.dgc.gov.it/v1/dgc/signercertificate/update. Accedendo a questo indirizzo viene restituito il contenuto di un singolo certificato X509. Un certificato X509 è un oggetto digitale che contiene al suo interno diverse informazioni, come l'identificativo di chi lo ha emesso e, quello che interessa a noi, una chiave pubblica.

Facciamo un paio di prove. Possiamo partire dal programma scritto nel precedente post, ma dobbiamo arricchire l'elenco di librerie importate.

from pyzbar.pyzbar import decode
import cv2
import zlib
import base45
from base64 import b64decode, b64encode
import cbor2
import json
from urllib.request import urlopen
import urllib
import ssl
import OpenSSL.crypto
import datetime

from cryptography import x509
from cryptography.utils import int_to_bytes
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend

from cose.messages import CoseMessage
from cose.headers import KID, Algorithm
from cose.keys import CoseKey
from cose.keys.keytype import KtyEC2, KtyRSA
from cose.keys.keyparam import KpKty, KpAlg, EC2KpX, EC2KpY, EC2KpCurve, RSAKpE, RSAKpN
from cose.algorithms import Es256, EdDSA, Ps256
from cose.keys.curves import P256

Recuperiamo il certificato

Proviamo a leggere il certificato dall'url scritto sopra:

url = 'https://get.dgc.gov.it/v1/dgc/signercertificate/update'
with urlopen(request) as keyfile:
  key = keyfile.read()
  print(key)

Il risultato sarà qualcosa del genere:

b'MIICwDCCAmagAwIBAgIIPR9jkXY7CPEwCgYIKoZIzj0EAwIwPTELMAkGA1UEBhMCSFIxEzARBgNVBAoMCkFLRCBkLm8uby4xGTAXBgNVBAMMEENyb2F0aWEgREdDIENTQ0EwHhcNMjEwNTIwMTMxNzQ2WhcNMjMwNTIwMTMxNzQ1WjA/MQswCQYDVQQGEwJIUjETMBEGA1UECgwKQUtEIGQuby5vLjEbMBkGA1UEAwwSQ3JvYXRpYSBER0MgRFMgMDAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEt5hwD0cJUB5TeQIAaE7nLjeef0vV5mamR30kjErGOcReGe37dDrmFAeOqILajQTiBXzcnPaMxWUd9SK9ZRexzaOCAUwwggFIMB8GA1UdIwQYMBaAFDErHKPIgGXhH70EktAlPHyGj1LRMC8GA1UdEgQoMCaBEkNyb2F0aWEuREdDQGRnYy5ocqQQMA4xDDAKBgNVBAcMA0hSVjAvBgNVHREEKDAmgRJDcm9hdGlhLkRHQ0BkZ2MuaHKkEDAOMQwwCgYDVQQHDANIUlYwZwYDVR0fBGAwXjAtoCugKYYnaHR0cDovL2RnYzEuZGdjLmhyL2Nyb2F0aWEtZGdjLWNzY2EuY3JsMC2gK6AphidodHRwOi8vZGdjMi5kZ2MuaHIvY3JvYXRpYS1kZ2MtY3NjYS5jcmwwHQYDVR0OBBYEFB55yLnz+T3ShQFs345mxQEJZb7TMCsGA1UdEAQkMCKADzIwMjEwNTIwMTMxNzQ2WoEPMjAyMTExMTYxMzE3NDZaMA4GA1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAgNIADBFAiANYlqMzCo7P6/FbwxS88MCB43CIBgfpJDmQ+D120Ov0gIhALJNQbk8HdHnkd31GV88U1N4YghHSZslLY8eZX8wSYR/'

Sappiamo (andando a tentativi e smandruppando nel codice dell'app ufficiale) che si tratta di un certificato X509 archiviato in formato DER (ASN1). Siccome un certificato X509 è un oggetto binario, e nel valore qui sopra ci sono solo caratteri ASCII, presumibilmente è stato codificato in Base64. Proviamo a leggerlo con la libreria OpenSSL:

deckey = b64decode(key)
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, deckey)

Un oggetto X509 ha dei metodi per guardarci dentro, ad esempio get_issuer che restituisce le informazioni relative a chi ha rilasciato il certificato. Proviamo a vedere. NB: get_issuer restituisce una classe contenitore, per leggerne gli elementi dobbiamo a sua volta chiamare get_components:

print(x509.get_issuer().get_components())

[(b'C', b'HR'), (b'O', b'AKD d.o.o.'), (b'CN', b'Croatia DGC CSCA')]

Come di vede abbiamo una lista di tuple. Ogni tupla contiene un codice e del testo. Il codice rappresenta il significato del testo. Ad esempio C rappresenta "country", il paese; CN rappresenta "Common Name", il nome dell'issuer; O rappresenta "Organization", il nome dell'organizzazione. Proviamo a estrarre queste informazioni in maniera più ordinata, e già che ci siamo usiamo anche i metodi get_notBefore e get_notAfter per leggere le date di validità del certificato stesso:

iss = x509.get_issuer().get_components()
c = [(x.decode('UTF-8'), y.decode('UTF-8')) for (x,y) in iss if x.decode('UTF-8') == "C"]
if(c): print(f"Country: {c[0][1]}")
cn = [(x.decode('UTF-8'), y.decode('UTF-8')) for (x,y) in iss if x.decode('UTF-8') == "CN"]
if(cn): print(f"Common name: {cn[0][1]}")
o = [(x.decode('UTF-8'), y.decode('UTF-8')) for (x,y) in iss if x.decode('UTF-8') == "O"]
if(o): print(f"Organization: {o[0][1]}") 

valid_from = datetime.datetime.strptime(x509.get_notBefore().decode('ASCII'),"%Y%m%d%H%M%SZ")
print(f"Valido da: {valid_from}")
valid_to = datetime.datetime.strptime(x509.get_notAfter().decode('ASCII'),"%Y%m%d%H%M%SZ")
print(f"Valido fino a: {valid_to}")

E vediamo cosa viene fuori:

Country: HR
Common name: Croatia DGC CSCA
Organization: AKD d.o.o.
Valido da: 2021-05-20 13:17:46
Valido fino a: 2023-05-20 13:17:45

Recuperiamo la chiave pubblica dal certificato

Interessante. Ora, possiamo estrarre la chiave pubblica da questo certificato con il metodo get_pubkey. Da questa chiave pubblica possiamo estrarre il contenuto formattato in formato ASN1 (DER) e poi darlo in pasto a una libreria python che costituirà il nostro punto di partenza per esaminare questa chiave pubblica.

pk_data = x509.get_pubkey()
pub_key = OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_ASN1, pk_data)
pub = serialization.load_der_public_key(pub_key)

Questa chiave deve essere convertita in un formato accettabile dalla libreria COSE. La faccenda è abbastanza complicata, ma cercherò di riassumerla. Il primo passo (quello appena fatto) è leggere la chiave mediante la libreria standard "cryptography" di Python. Questa libreria legge il contenuto della chiave e restituisce l'istanza di una classe. Poi dobbiamo capire di che tipo di chiave si tratta (ce ne sono diversi) e usare i suoi parametri per convertirla in una chiave nel formato desiderato da COSE.

Dalla documentazione ufficiale risulta che le chiavi usate per i Green Pass dovrebbero usare l'algoritmo RSSA-PSS con SHA-2 oppure EC-DSA con SHA2 e curva NIST-p-256. Tutte cose di cui ho una estremamente superficiale conoscenza, ma poco importa.

Quello che importa è che abbiamo una vaga idea di che parametri passare a COSE quando gli chiederemo di costruire la sua chiave per ciascuna delle chiavi che abbiamo trovato. Analizziamo la chiave, capiamo se si tratta di una chiave ECDSA o RSAPSS, estraiamo i valori costituenti la chiave stessa (X,Y per ECDSA, E,N per RSA) e li usiamo per costruire la chiave COSE.

if (isinstance(pub, EllipticCurvePublicKey)):
    # ECDSA SHA256
    chiave_cose = CoseKey.from_dict({
        KpKty: KtyEC2,
        KpAlg: Es256,
        EC2KpCurve: P256,
        EC2KpX: pub.public_numbers().x.to_bytes(32, byteorder="big"),
        EC2KpY: pub.public_numbers().y.to_bytes(32, byteorder="big")
    })    
elif (isinstance(pub, RSAPublicKey)):
    # RSSA-PSS SHA-56 MFG1
    chiave_cose = CoseKey.from_dict({
        KpKty: KtyRSA,
        KpAlg: Ps256, 
        RSAKpE: int_to_bytes(pub.public_numbers().e),
        RSAKpN: int_to_bytes(pub.public_numbers().n)
    })
else:
      print(f"Tipo chiave sconosciuto {pub.__class__.__name__}).")  

C'è ancora un punto da chiarire. Ogni chiave è identificata da un KID (Key ID) che la distingue in maniera univoca. Quando andremo a vedere il nostro Green Pass, scopriremo che dentro c'è scritto il KID della chiave da usare per la verifica. Dov'è questo KID? Sorpresa sorpresa, questo KID ci è stato restituito sotto forma di header quando abbiamo fatto la richiesta HTTP! Possiamo andare a modificare il nostro codice di recupero per leggere anche il KID (codificato in base 64):

url = 'https://get.dgc.gov.it/v1/dgc/signercertificate/update'
with urlopen(request) as keyfile:
  key = keyfile.read()
  b64kid = keyfile.getheader('X-KID')     # <- ecco qui
  print(key)

Un solo certificato?

Rimane un unico punto aperto. In tutta Europa ogni paese ha creati diversi certificati per firmare i propri Green Pass (tutti riconosciuti correttamente da Verifica C19), ma qui ne ho letto solo uno. Come mai? La risposta è che il ministero, probabilmente allo scopo di complicarsi la vita perchè se è troppo semplice non ci piace, ha architettato un meccanismo per cui per recuperare il set di certificati validi è necessario fare più richieste, con un certificato restituito ad ogni richiesta. Le richieste sono contraddistinte da un header, "X-RESUME-TOKEN", che viene restituito ad ogni chiamata. Per ottenere il certificato successivo devo effettuare una nuova richiesta allo stesso url ma inviando un header "X-RESUME-TOKEN" uguale a quello che ho ricevuto dalla chiamata precedente. E così via, fino a quando la richiesta smette di restituire informazioni. Una cosa del genere:

url = 'https://get.dgc.gov.it/v1/dgc/signercertificate/update'
token = ''

while True:
    
    request = urllib.request.Request(url)
    if (token):
        request.add_header('X-RESUME-TOKEN', token)
        
    with urlopen(request) as keyfile:
        
        key = keyfile.read()
        if (not key): break
        token = keyfile.getheader('X-RESUME-TOKEN')
        b64kid = keyfile.getheader('X-KID')
        
        ...elaborazione certificato...

A questo punto abbiamo tutti i tasselli, vediamo di metterli assieme. Possiamo creare un dizionario che abbia come chiave il KID del certificato e come contenuto la chiave pubblica già convertita in formato COSE. Già che ci siamo creiamo un altro dizionario, sempre usando il KID come chiave ma al cui interno andremo a registrare tutti i certificati x509 nella loro forma originale (ci serviranno se vorremo estrarre, per esempio, l'issuer in un secondo momento). Il codice completo, costruito mettendo assieme i pezzetti descritti prima, è questo:

url = 'https://get.dgc.gov.it/v1/dgc/signercertificate/update'
token = ''

certificates = {}
x509_collection = {}

while True:
    
    request = urllib.request.Request(url)
    if (token):
        request.add_header('X-RESUME-TOKEN', token)
        
    with urlopen(request) as keyfile:
        
        key = keyfile.read()
        if (not key): break
        token = keyfile.getheader('X-RESUME-TOKEN')
        b64kid = keyfile.getheader('X-KID')
                
        deckey = b64decode(key)
        x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, deckey)
        x509_collection[b64kid] = x509
        pk_data = x509.get_pubkey()
        
        pub_key = OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_ASN1, pk_data)
        pub = serialization.load_der_public_key(pub_key)
        
        if (isinstance(pub, EllipticCurvePublicKey)):
            # ECDSA SHA256
            certificates[b64kid] = CoseKey.from_dict({
                KpKty: KtyEC2,
                KpAlg: Es256,
                EC2KpCurve: P256,
                EC2KpX: pub.public_numbers().x.to_bytes(32, byteorder="big"),
                EC2KpY: pub.public_numbers().y.to_bytes(32, byteorder="big")
            })    
        elif (isinstance(pub, RSAPublicKey)):
            # RSSA-PSS SHA-56 MFG1
            certificates[b64kid] = CoseKey.from_dict({
                KpKty: KtyRSA,
                KpAlg: Ps256, 
                RSAKpE: int_to_bytes(pub.public_numbers().e),
                RSAKpN: int_to_bytes(pub.public_numbers().n)
            })
        else:
              print(f"Tipo chiave sconosciuto {pub.__class__.__name__}).") 

Ricollegandoci a quanto fatto nello scorso post, ora dobbiamo scoprire quali di queste chiavi è quella da usare per il nostro certificato. Per trovarla, troviamo il KID del nostro certificato (nel messaggio COSE) e lo usiamo per andare a prendere la corrispondente chiave nella lista che abbiamo appena creato.

kid = b64encode(cose.get_attr(KID)).decode('ASCII')
pk_to_use = certificates[kid]

Tanto per curiosità, possiamo verificare nel file originale dei certificati cosa c'è nei campi aggiuntivi di quello che abbiamo trovato. In questo esempio per avere dei valori interessanti sto usando il certificato usato per firmare il mio Green Pass.

#Stampa attributi chiave trovata
print(f"KID: {kid}")
iss = x509_collection[kid].get_issuer().get_components()
c = [(x.decode('UTF-8'), y.decode('UTF-8')) for (x,y) in iss if x.decode('UTF-8') == "C"]
if(c): print(f"Country: {c[0][1]}")
cn = [(x.decode('UTF-8'), y.decode('UTF-8')) for (x,y) in iss if x.decode('UTF-8') == "CN"]
if(cn): print(f"Common name: {cn[0][1]}")
o = [(x.decode('UTF-8'), y.decode('UTF-8')) for (x,y) in iss if x.decode('UTF-8') == "O"]
if(o): print(f"Organization: {o[0][1]}")   

valid_from = datetime.datetime.strptime(x509_collection[kid].get_notBefore().decode('ASCII'),"%Y%m%d%H%M%SZ")
print(f"Valido da: {valid_from}")
valid_to = datetime.datetime.strptime(x509_collection[kid].get_notAfter().decode('ASCII'),"%Y%m%d%H%M%SZ")
print(f"Valido fino a: {valid_to}")

Risultato:

KID: NJpCsMLQco4=
Country: IT
Common name: Italy DGC CSCA 1
Organization: Ministero della Salute
Valido da: 2021-05-12 08:18:17
Valido fino a: 2023-05-12 08:11:59

Una ulteriore riprova che abbiamo trovato il certificato giusto (se fosse emerso che il mio green pass è stato firmato dal ministero della salute bielorusso probabilmente avrei avuto dei dubbi).

A questo punto il gioco è fatto! Possiamo inserire la chiave pubblica nel messaggio COSE (che abbiamo estratto dal QR code nella scorsa puntata) e dirgli di verificare.

cose.key = pk_to_use
if not cose.verify_signature():
    print("No")
else:
    print("OK")

A questo punto potremmo fermarci, la verifica della firma è conclusa e, se tutto è andato bene, il nostro Green Pass è valido. O no?

Revoca certificati (una specie)

Nell'app Verifica C19 è stato previsto un meccanismo di revoca dei certificati. In linea di principio un certificato potrebbe essere compromesso se un malintenzionato si impadronisse della chiave privata; a quel punto potrebbe creare dei Green Pass validi a tutti gli effetti. Per limitare questo rischi il ministero ha previsto un meccanismo per rendere un certificato inaccettabile per la verifica. Ci sono modi molto intelligenti e molto standard per revocare un certificato, ma al nostro ministero non li conoscono e hanno quindi deciso un sistema fatto in casa: oltre alla lista dei certificati di cui sopra è previsto un altro url che restituisce la lista dei KID ancora validi. Possiamo recuperare questa lista in maniera analoga a quanto visto in precedenza:

valid_url = 'https://get.dgc.gov.it/v1/dgc/signercertificate/status'
with urlopen(valid_url) as valid_file:
    valid_list = valid_file.read()
    print(valid_list)

Il risultato è semplicemente una lista di stringhe, ciascuna delle quali rappresenta un KID:

["25QCxBrBJvA=","NAyCKly+hCg=","ODqaG8mnbro=","e4lH6I4iMIM=","vvYa1vaWkGg=","NJpCsMLQco4=","/IcqIBnnZzc=","3IsdmTYkAAM=","4Qmniw7B0gc=","6ag2wJkSHtk=","ARrNkCRtprY=","BEnvMVnNFK8=","FDNJjaSCWi0=","GMFMBu1RlCg=","GuQPQRxbMsU=","IaGR283U1jA=","JHd4CkNzadI=","Jjql9rBrjHI=","L7XIA2gi2ps=","MtI93IMknMk=","NCc6YSsVioM=","YRYidQ+wetg=","YU9+X9nepqU=","ZcfkloEvfGQ=","dhSzPDr4G2M=","e9SH8dtWwdY=","hgpHHrTb4ws=","nPKEYm3gXzU=","qFNF2dC+mjQ=","tCM87WnaaQE=","ub6Qmv9xtAo=","x3ch4ml934I=","02vdAOY/+gI=","0kAwFy+vLpg=","1+da8dKEjlE=","2BGoyFIyYPs=","3LCRmucB9kU=","3jqajzfHpKE=","3lrBUHc4iQE=","5xtSr6KkAGA=","6FNkACSMLEc=","7XLhQx1KXdQ=","8AnF/hcilSo=","AQCGDydsS1Q=","CvktK3hdjeY=","DusseXrzqO8=","EzYR1uk/E0I=","IZftFLRmKGY=","Is2JtrOJhik=","MxhfdcoHinc=","NCdyt3s+cak=","OKpEjMo/2MY=","R7q7yd90ZPU=","TpQIkAHAym4=","Uj77p+qIQNs=","XuCERkHu8kY=",......

Possiamo estrarre gli elementi da questa lista:

valid_url = 'https://get.dgc.gov.it/v1/dgc/signercertificate/status'
valid_list = []
with urlopen(valid_url) as valid_file:
    valid_list = valid_file.read()
    valid_list = json.loads(valid_list.decode('ASCII'))

...e usarli per vedere se il KID con cui ho verificato il mio certificato è presente o meno:

if (kid not in valid_list):
    print("doh")
else:
    print("yeeh")

A questo punto siamo a posto.. o no?

Revoca singoli Green Pass

Negli ultimi tempi sono circolati in Italia una manciata di Green Pass perfettamente genuini ma attribuiti a nomi poco plausibili (Hitler, Topolino, Craxi). E' improbabile che sia stata compromessa una chiave privata, è più plausibile che questi certificati siano stati creati da un operatore abilitato (per scherzo o per screditare il sistema), o magari da qualcuno che ha avuto accesso a un computer abilitato alla firma in una qualche istituzione. In questi casi si evita di revocare la chiave (questo invaliderebbe migliaia di certificati legittimi) e si revoca il singolo certificato. Naturalmente ci sono metodi standard e consolidati per revocare i certificati, e altrettanto naturalmente il ministero ha raffazzonato un metodo grezzo, probabilmente per tamponare in fretta l'imbarazzo mediatico del green pass di Topolino che viene convalidato dall'app ufficiale.

Esiste un terzo url, oltre ai due visti sopra, da cui l'app preleva informazioni di configurazione interessanti. Ad esempio, la durata del certificato in funzione del tipo (tampone, vaccinazione...). Tra queste informazioni è stata di recente aggiunta una lista di certificati non validi. Andiamo a vedere.

settings_url = 'https://get.dgc.gov.it/v1/dgc/settings'
with urlopen(settings_url) as settings_file:
    settings = settings_file.read()
    print(settings)

Risultato:

b'[{"name":"vaccine_end_day_complete","type":"EU/1/20/1525","value":"365"},{"name":"vaccine_start_day_complete","type":"EU/1/20/1525","value":"15"},{"name":"vaccine_end_day_not_complete","type":"EU/1/20/1525","value":"365"},{"name":"vaccine_start_day_not_complete","type":"EU/1/20/1525","value":"15"},{"name":"vaccine_end_day_complete","type":"EU/1/21/1529","value":"365"},{"name":"vaccine_start_day_complete","type":"EU/1/21/1529","value":"0"},{"name":"vaccine_end_day_not_complete","type":"EU/1/21/1529","value":"84"},{"name":"vaccine_start_day_not_complete","type":"EU/1/21/1529","value":"15"},{"name":"vaccine_end_day_complete","type":"EU/1/20/1507","value":"365"},{"name":"vaccine_start_day_complete","type":"EU/1/20/1507","value":"0"},{"name":"vaccine_end_day_not_complete","type":"EU/1/20/1507","value":"42"},{"name":"vaccine_start_day_not_complete","type":"EU/1/20/1507","value":"15"},{"name":"vaccine_end_day_complete","type":"EU/1/20/1528","value":"365"},{"name":"vaccine_start_day_complete","type":"EU/1/20/1528","value":"0"},{"name":"vaccine_end_day_not_complete","type":"EU/1/20/1528","value":"42"},{"name":"vaccine_start_day_not_complete","type":"EU/1/20/1528","value":"15"},{"name":"rapid_test_start_hours","type":"GENERIC","value":"0"},{"name":"rapid_test_end_hours","type":"GENERIC","value":"48"},{"name":"molecular_test_start_hours","type":"GENERIC","value":"0"},{"name":"molecular_test_end_hours","type":"GENERIC","value":"72"},{"name":"recovery_cert_start_day","type":"GENERIC","value":"0"},{"name":"recovery_cert_end_day","type":"GENERIC","value":"180"},{"name":"ios","type":"APP_MIN_VERSION","value":"1.1.2"},{"name":"android","type":"APP_MIN_VERSION","value":"1.1.2"},{"name":"vaccine_start_day_not_complete","type":"Covishield","value":"15"},{"name":"vaccine_end_day_not_complete","type":"Covishield","value":"84"},{"name":"vaccine_start_day_complete","type":"Covishield","value":"0"},{"name":"vaccine_end_day_complete","type":"Covishield","value":"365"},{"name":"vaccine_start_day_not_complete","type":"R-COVI","value":"15"},{"name":"vaccine_end_day_not_complete","type":"R-COVI","value":"84"},{"name":"vaccine_start_day_complete","type":"R-COVI","value":"0"},{"name":"vaccine_end_day_complete","type":"R-COVI","value":"365"},{"name":"vaccine_start_day_not_complete","type":"Covid-19-recombinant","value":"15"},{"name":"vaccine_end_day_not_complete","type":"Covid-19-recombinant","value":"84"},{"name":"vaccine_start_day_complete","type":"Covid-19-recombinant","value":"0"},{"name":"vaccine_end_day_complete","type":"Covid-19-recombinant","value":"365"},{"name":"black_list_uvci","type":"black_list_uvci","value":"URN:UVCI:01:FR:W7V2BE46QSBJ#L;URN:UVCI:01:FR:T5DWTJYS4ZR8#4;URN:UVCI:01DE/A80013335/TCXSI5Q08B0DIJGMIZJDF#T;URN:UVCI:01:PL:1/AF2AA5873FAF45DFA826B8A01237BDC4;01IT8523A6CADC834919A5214EA30779372D#1;01IT3DA01DD1A0AA4E4E92A10C11B04D39DB#8;01ITD85669BBC8E145FCA9CC83555B413952#3;01ITE901A12151934BAE92D7A537824BA260#3;01IT6E37CB3368F64C6884B3D2A74C266772#0;01ITC9E3B8B6B0344918AE7508E7094D9BCA#7;01ITB00C5897FF354FAFACFAFA391E613F59#8;01IT4C850BC31D3B41329A7EFC1F2AB5D2C5#2;01IT851F1A334C6B450DAD611A5DA4836404#6;01IT8658CD2A96F8435685D63B5C1DBC243F#5;01ITC78D2B44EA9D4C52A77C14C970676B62#9;URN:UVCI:01:PL:1/2A992C33754A4D379A7F61089485BB75;"}]'

Tra queste informazioni vediamo come ultimo elemento una "black_list_uvci", che contiene appunto gli identificativi dei Green Passi invalidati. Estraiamo questa lista:

settings_url = 'https://get.dgc.gov.it/v1/dgc/settings'
revoked_list =[]

with urlopen(settings_url) as settings_file:
    settings = settings_file.read()
    settings = json.loads(settings.decode('UTF-8'))
    
    revoked_data = [x for x in settings if x['name'] == 'black_list_uvci']    
    if (revoked_data): revoked_list = revoked_data[0]['value'].split(';')

print(revoked_list)

Ecco qui la lista:

['URN:UVCI:01:FR:W7V2BE46QSBJ#L', 'URN:UVCI:01:FR:T5DWTJYS4ZR8#4', 'URN:UVCI:01DE/A80013335/TCXSI5Q08B0DIJGMIZJDF#T', 'URN:UVCI:01:PL:1/AF2AA5873FAF45DFA826B8A01237BDC4', '01IT8523A6CADC834919A5214EA30779372D#1', '01IT3DA01DD1A0AA4E4E92A10C11B04D39DB#8', '01ITD85669BBC8E145FCA9CC83555B413952#3', '01ITE901A12151934BAE92D7A537824BA260#3', '01IT6E37CB3368F64C6884B3D2A74C266772#0', '01ITC9E3B8B6B0344918AE7508E7094D9BCA#7', '01ITB00C5897FF354FAFACFAFA391E613F59#8', '01IT4C850BC31D3B41329A7EFC1F2AB5D2C5#2', '01IT851F1A334C6B450DAD611A5DA4836404#6', '01IT8658CD2A96F8435685D63B5C1DBC243F#5', '01ITC78D2B44EA9D4C52A77C14C970676B62#9', 'URN:UVCI:01:PL:1/2A992C33754A4D379A7F61089485BB75', '']

Per verificare se il nostro certificato è valido dobbiamo semplicemente verificare se il nostro ID univoco è presente o meno nella lista. Ricordando il post precedente, avevamo estratto tutte le informazioni del nostro green pass (il payload del messaggio CBOR, codificato in formato COSE). Ricordiamo che in quel post avevo usato un Green Pass di test, reperito online.

print(data)

{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'}}}

Il codice che ci interessa è il campo "ci". Lo andiamo a prendere e lo cerchiamo nella nostra lista. Se non lo troviamo allora siamo a posto.

gp_id = data[-260][1]['v'][0]['ci']

if (gp_id in revoked_list):
    print("Green Pass revocato")
else:
    print("Green Pass valido")

Cosa manca?

L'unica cosa che ci manca per replicare in tutto e per tutto il funzionamento dell'app Verifica C19 è il controllo di validità del certificato in funzione della data. Per ogni evento che ha causato il rilascio, infatti, è previsto un giorno di inizio validità e un giorno di fine validità (il tampone molecolare dura dal momento dell'effettuazione fino a 72 ore; la guarigione garantisce una copertura del Green Pass per 180 giorni, eccetera). Queste informazioni sono reperite nell'url "settings" che abbiamo recuperato per estrapolare la lista di revoca dei singoli Green Pass. Non vedo un particolare valore accademico nel mettermi a scrivere pagine di codice per verificare tutte le casistiche, non è particolarmente complicato, solo molto noioso. Quindi lo lascio come esercizio per il lettore :-)

Ci vediamo al prossimo esperimento!