Verificare la firma di eseguibili e installatori

Un perito riceve un setup.exe e lavora su un Mac. Oppure riceve un .dmg e ha davanti un PC Windows. Gli strumenti che leggono quelle firme non attraversano il confine: signtool esiste solo su Windows, codesign e spctl solo su macOS. Probatio non chiama nessuno dei tre: legge i byte del file, e quindi verifica la firma ovunque giri — nelle due direzioni.

Tre domande da non confondere

Davanti a un eseguibile firmato le domande sono tre, e conviene tenerle separate fin dall'inizio:

  1. Chi l'ha firmato? Sta scritto nel certificato del firmatario, dentro la firma.
  2. È cambiato dopo la firma? Si risponde ricalcolando l'impronta del contenuto e confrontandola con quella firmata.
  3. Il sistema operativo lo accetterà? Dipende da regole che vivono fuori dal file: il programma di certificati radice di Microsoft, Gatekeeper e i server di notarizzazione di Apple.

Alle prime due si risponde con i soli byte, ed è a queste che risponde Probatio, riconoscendo il formato dal contenuto e non dall'estensione: un installatore rinominato in .txt non nasconde quello che è. Sulla terza torniamo alla fine, perché è quella che più spesso finisce in relazione con le parole sbagliate.

Windows: l'hash Authenticode non è l'hash del file

La firma di un .exe o di una .dll — Authenticode — non è appesa in coda «e basta». Sta nella Certificate Table, la cui posizione è dichiarata nella quinta voce (indice 4) della data directory dell'intestazione PE. E soprattutto non copre tutto il file: l'impronta firmata salta di proposito tre pezzi.

Un file PE dall'inizio alla fine: cosa entra nell'hash Authenticode nell'hash escluso MZ · intestazione DOS PE · file header · optional header CheckSum · 4 byte a PE+0x58 riscritto dal linker: la firma sarebbe fragile resto dell'optional header · voci 0–3 voce 4 · Certificate Table · 8 byte dice dove sta la firma: ignoto prima di firmare voci 5–15 · tabella delle sezioni sezione .text sezione .rdata sezione .data byte dopo l'ultima sezione coperti anche loro WIN_CERTIFICATE · PKCS#7 SignedData la firma non può firmare se stessa in ordine di posizione nel file, non nell'ordine della tabella Lo SHA-256 del file intero non coincide con l'hash Authenticode, e non deve. Probatio li mostra separati: il primo identifica il reperto, il secondo si confronta con quello firmato.
Tre zone restano fuori dall'impronta firmata: il CheckSum, la voce che dice dove sta la firma, e la firma stessa. Tutto il resto, sezioni comprese, è coperto.
  • il campo CheckSum dell'optional header, 4 byte: il linker lo riscrive quando ritocca il file, e includerlo renderebbe la firma fragile senza alcun guadagno;
  • la voce di directory della Certificate Table, 8 byte: contiene posizione e dimensione della firma, che non si conoscono finché la firma non è stata fatta;
  • la firma stessa, in coda: nessuna firma può firmare se stessa.

Il resto entra nell'hash in un ordine preciso: le intestazioni a pezzi, poi le sezioni nell'ordine in cui stanno nel file — non in quello della tabella delle sezioni, che può essere diverso — e infine gli eventuali byte rimasti fra l'ultima sezione e la firma, che alcuni linker lasciano lì.

Per questo lo SHA-256 del file intero non coincide mai con l'impronta firmata, e non deve. Probatio mostra entrambe: l'impronta del file esaminato identifica il reperto, l'hash Authenticode ricalcolato si confronta con quello dichiarato nella firma.

Dove sta l'impronta: SpcIndirectDataContent

La firma è un PKCS#7 SignedData. Il contenuto firmato non è il file, ma una piccola struttura Microsoft, lo SpcIndirectDataContent, che porta l'algoritmo e l'hash Authenticode. La catena è quindi: byte del file → hash Authenticode → SpcIndirectDataContent → attributo messageDigest → firma del titolare del certificato. Probatio controlla i due capi: ricalcola l'hash sul file e verifica crittograficamente la firma.

C'è un dettaglio che fa fallire in silenzio le implementazioni frettolose. Il CMS moderno (RFC 5652) vuole il contenuto avvolto in un OCTET STRING; Authenticode, nato sul PKCS#7 della generazione precedente, ci mette la struttura direttamente. Un verificatore che dà per scontato l'involucro digerisce i byte sbagliati e dichiara non valida una firma buona. Probatio accetta entrambe le codifiche.

Doppia firma: SHA-1 e SHA-256 annidata

Molti eseguibili firmati negli anni della transizione portano due firme: una SHA-1, per i sistemi che non conoscevano altro, e una SHA-256 infilata negli attributi non firmati della prima (OID 1.3.6.1.4.1.311.2.4.1). Chi mostra solo la firma esterna racconta il file peggio di com'è — oppure meglio, se è proprio l'annidata a essere rotta.

Probatio segue l'annidamento, verifica ogni firma per conto suo e ricalcola l'hash Authenticode con l'algoritmo che ciascuna dichiara. Le firme annidate compaiono come tali, ciascuna con il proprio esito.

La marca temporale, in due forme

Un certificato di firma del codice scade. Senza una marca temporale, dopo la scadenza non si può più stabilire che la firma sia stata apposta quando il certificato era valido, e Probatio lo segnala come rilievo. Le forme sono due, e si leggono entrambe:

  • la contro-firma PKCS#9, la forma storica: un secondo firmatario — la TSA — che porta un signingTime nei propri attributi firmati. Probatio ne verifica la firma con il certificato della TSA presente nella busta e controlla il legame: il messageDigest firmato dalla TSA deve essere l'impronta del valore della firma principale. Senza quel controllo, una contro-firma autentica presa da un altro file e incollata qui darebbe a questa firma una data che non le appartiene. La marca risulta verificata solo se reggono entrambe le cose;
  • il token RFC 3161, la forma attuale, che Microsoft colloca in un proprio attributo non firmato. Lo verifica lo stesso motore delle marche nelle firme CAdES e PAdES (ne parla la guida sulla marca temporale RFC 3161).

Due difetti trovati su file reali

Le prove su eseguibili veri hanno fatto emergere due difetti nel motore crittografico di Probatio, lo stesso delle firme CAdES e PAdES, ora corretti per tutti:

  • i certificati di VeriSign e Symantec scrivono nome e organizzazione in BMPString (UTF-16): chi non la decodifica mostra una firma valida senza firmatario, un dato che sembra assente ed è solo non letto;
  • alcune contro-firme di quelle TSA mettono l'impronta nuda dopo il riempimento RSA, senza la struttura DigestInfo di PKCS#1. Ora sono accettate senza allentare nulla: algoritmo e impronta devono coincidere, cambia solo l'involucro.

Gli installatori MSI

Un .msi è un contenitore CFB, un piccolo filesystem dentro un file. La firma sta in uno stream di servizio (DigitalSignature, preceduto dal carattere 0x05) ed è ancora un PKCS#7 con uno SpcIndirectDataContent, ma l'impronta copre il contenuto degli stream in un ordine stabilito, non un intervallo di byte. Per quell'impronta vale un limite che trovi più sotto.

Apple: si firma la CodeDirectory, non il file

Una firma Apple non è un PKCS#7 appeso a un binario. Il comando di caricamento LC_CODE_SIGNATURE punta a un SuperBlob, un contenitore di blocchi: la CodeDirectory, i requisiti, gli entitlements, la busta CMS e, quando c'è, il ticket di notarizzazione.

Il pezzo centrale è la CodeDirectory. Contiene l'identificatore (di norma il bundle id), il Team ID dello sviluppatore, l'algoritmo, i flag — runtime per l'hardened runtime, adhoc, library-validation e altri — e soprattutto l'impronta di ogni pagina del binario, di norma da 4 KiB. La firma CMS è calcolata sulla CodeDirectory, e l'impronta della CodeDirectory stessa è il cdhash: lo stesso valore che mostra codesign -dv su un Mac.

Verificare una firma Apple richiede quindi due controlli distinti: che la firma CMS sia valida sulla CodeDirectory, e che le impronte di pagina scritte nella CodeDirectory corrispondano davvero al file. Farne uno solo è l'errore classico. Una firma perfettamente valida su una CodeDirectory che non descrive più il binario è esattamente ciò che si vede su un file manomesso. Probatio li fa entrambi, e si ferma alla prima pagina discordante.

Una firma ad-hoc non ha certificato, quindi non ha firmatario. Probatio le dà un verdetto proprio, «firma ad-hoc, nessun firmatario», distinto sia da «valida» sia da «non valida»: le impronte di pagina si ricontrollano comunque, quindi attesta che il binario non è cambiato da quando è stato firmato, non da chi provenga. È il caso di ogni binario arm64 firmato soltanto dal linker.

Binari universali: una firma per architettura

fat header · cafebabe due architetture, due firme indipendenti x86_64 p0 p1 p2 p3 p4 p5 p6 p7 pagine di codice da 4 KiB CodeDirectory identificatore · Team ID · flag un'impronta per ogni pagina firma CMS calcolata sulla CodeDirectory impronte di pagina corrispondenti firma CMS verificata arm64 p0 p1 p2 p3 p4 p5 p6 p7 pagine di codice da 4 KiB CodeDirectory identificatore · Team ID · flag un'impronta per ogni pagina firma CMS calcolata sulla CodeDirectory pagina p3 diversa da quella firmata eppure la firma CMS è valida Verdetto del file: firma non valida. Vale il peggiore.
La firma dell'architettura arm64 è crittograficamente valida, ma la sua CodeDirectory non descrive più il binario: una pagina è cambiata. È il caso che sfugge a chi verifica solo la firma, o una sola architettura.

Un binario universale («fat») contiene più binari, uno per architettura, e ognuno ha la propria firma. Verificarne uno e dichiarare valido il file è un errore concreto: le architetture possono essere firmate da soggetti diversi, oppure una sola può essere stata alterata. Probatio le verifica tutte, e basta un'architettura con una pagina che non torna o una firma che non si verifica perché il verdetto del file sia «firma non valida». Un'architettura priva di firma, anche se le altre sono integre, porta il verdetto a «verifica parziale»: il file è firmato solo in parte, e su quell'architettura non c'è nulla che ne attesti origine o integrità.

DMG: il trailer koly e il ticket che si legge senza rete

Un'immagine disco non ha nulla in testa che la identifichi: si riconosce da un trailer di 512 byte in coda, che comincia con koly. Dal byte 296 del trailer stanno posizione e lunghezza della firma, un dato misurato su immagini reali. La firma è lo stesso SuperBlob dei binari, con una differenza: il contenuto non è diviso in pagine, e la CodeDirectory copre con un'impronta sola tutti i byte che precedono la firma.

Restano fuori i 512 byte del trailer, che descrivono la geometria dell'immagine. Apple li protegge con uno slot speciale della CodeDirectory, calcolato sul trailer con il campo della lunghezza della firma azzerato, perché quel valore non si conosce finché la firma non è fatta. Probatio verifica anche quello: alterando i soli ultimi 512 byte di un DMG valido il verdetto diventa «firma non valida», e se lo slot manca il rapporto dichiara che il trailer non è coperto.

Poi c'è il ticket di notarizzazione. Quando un'immagine è stata sottoposta ad Apple e poi «pinzata» con stapler, il ticket viene allegato come blocco del SuperBlob. Probatio lo trova e lo mostra — «notarizzata (ticket allegato)» — anche su Windows, anche senza rete. È la risposta alla domanda «questo DMG è notarizzato?» per chi non ha un Mac.

Due precisazioni, entrambe da riportare in relazione. L'assenza del ticket non significa che l'immagine non sia stata notarizzata: può esserlo stata senza che il ticket venisse allegato, e allora l'installazione richiede la rete. E la presenza del ticket dice che Apple ha notarizzato l'immagine a suo tempo, non che la consideri valida oggi.

«Firma valida» non vuol dire «il sistema la accetterà»

Qui sta il punto più facile da sbagliare. Che la firma sia crittograficamente valida e il contenuto integro dice che quei byte sono quelli usciti dalle mani del titolare del certificato. Non dice cosa farà Windows o macOS con quel file: Windows consulta il proprio programma di certificati radice, macOS consulta Gatekeeper e i server di notarizzazione di Apple.

DomandaDove sta la rispostaProbatio
Chi ha firmato?certificato dentro la firmanome, organizzazione, emittente, validità, seriale, radice della catena
È cambiato dopo la firma?impronta ricalcolata sui bytesì: hash Authenticode, pagine Mach-O, contenuto e trailer DMG
Quando è stato firmato?marca RFC 3161 o contro-firmasì, con verifica della firma della TSA e del suo legame con la firma principale
Il certificato è stato revocato?risponditore OCSPa richiesta, online
Il sistema lo accetterà?radici di Microsoft o di Apple, Gatekeeper, server Appleno: nomina la radice, ma questo verdetto non lo emette

Probatio risale la catena con i certificati contenuti nella firma e ne nomina la radice. Se il certificato in cima è auto-firmato, il rilievo dice che la catena arriva alla radice «X», inclusa nella firma; altrimenti, che la catena contenuta nella firma termina con un certificato emesso da «X» e che la radice non è inclusa. Accanto al nome indica, se lo riconosce, a quale famiglia appartiene — programma Apple, programma Microsoft, CA commerciale nota — oppure che non è fra quelle che riconosce. È l'informazione su cui il perito ragiona, non un verdetto: se il sistema di destinazione accetterà la firma dipende dal suo programma di certificati radice.

Una cosa diversa è l'ancoraggio: la catena risulta ancorata solo se arriva a una CA dell'elenco di fiducia AgID/UE tenuto in cache. Le catene della firma del codice di norma lì non arrivano, e il rapporto scrive «non ancorata a una radice nota a Probatio»: non è un difetto del file. La stessa distinzione fra firma valida e firma attendibile è sviluppata nella guida su come verificare una firma digitale.

Cosa Probatio non verifica

  • Il verdetto di fiducia di Windows e di macOS: Probatio nomina la radice e dice se la riconosce, ma non porta con sé i programmi di certificati radice di Microsoft e di Apple, e non si pronuncia al loro posto.
  • La notarizzazione online: del ticket si controlla la presenza nel file; Apple non viene interrogata, e che il ticket sia ancora valido oggi non è verificato.
  • L'impronta degli MSI: il calcolo non è ancora stato validato su un installatore firmato di riferimento. Se coincide, l'integrità è confermata; se non coincide, Probatio non dichiara la manomissione e resta alla «verifica parziale», perché la causa può stare nel file o nel calcolo. Firmatario, catena e marca sono verificati senza riserve.
  • Le .app nel loro insieme: si verifica l'eseguibile, non il sigillo delle risorse (_CodeSignature/CodeResources). Delle impronte «speciali» della CodeDirectory — Info.plist, requisiti, entitlements — non se ne ricalcola nessuna, salvo quella del trailer dei DMG.
  • Altri contenitori: .pkg, .cab, .cat, APPX/MSIX e script firmati (.ps1, .vbs) non sono ancora riconosciuti.
  • File PE, Mach-O o DMG oltre 2 GiB: si leggono per intero in memoria, e oltre quella soglia Probatio rifiuta dicendo perché.

In pratica

  1. Annota l'impronta del file esaminato. Il rapporto PDF del modulo Firma software la riporta in testa, con l'atto di verifica: quando il controllo è stato eseguito, non quando si stampa.
  2. Guarda tutte le firme: la SHA-256 annidata su un eseguibile datato, ogni architettura su un binario universale.
  3. Su un DMG tieni separati firma, notarizzazione e ticket allegato.
  4. Scrivi che cosa non hai verificato, a cominciare dal verdetto del sistema operativo: la radice nominata non lo sostituisce.

Una firma del codice risponde con certezza a due domande — chi, e se il file è cambiato — e lascia la terza a chi quel file lo farà girare. Tenerle distinte è metà del lavoro di chi deve scriverne.