ELAI S.r.l.

Stessi dati JSON, hash diversi: quale identità stiamo controllando?

Byte, oggetti, Unicode e fiducia nel riferimento: esempi eseguiti spiegano che cosa dimostra un checksum e che cosa richiede un contratto diverso.

Stessi dati JSON, hash diversi: quale identità stiamo controllando?

Un controllo rosso senza una modifica ai dati

Una pipeline AI riceve due file di configurazione. Uno è compatto, l’altro impaginato con righe e spazi; l’ordine dei campi cambia. Il programma li legge e ottiene lo stesso oggetto, ma gli hash sono diversi. Dobbiamo trattare il secondo file come una modifica? Dipende da ciò che vogliamo identificare: il file esatto oppure i dati che rappresenta. Confondere queste due domande può produrre allarmi inutili o, nella direzione opposta, cancellare differenze importanti.

La domanda centrale è come scegliere un’identità coerente con il controllo. Costruiremo esempi riproducibili di serializzazione JSON, cioè trasformazione di dati in testo, e calcolo SHA-256 sui byte. Vedremo anche perché un hash uguale a quello di un manifesto non dimostri automaticamente chi abbia prodotto un file. Non proviamo attacchi a sistemi reali e non certifichiamo un’infrastruttura: tutti i documenti sono sintetici e il codice usa soltanto la libreria standard Python.

Tre livelli che conviene nominare

Al livello dei byte, due file sono uguali se ogni byte coincide nella stessa posizione. Al livello dei dati, qui due oggetti JSON sono uguali dopo il parsing se hanno le stesse chiavi e gli stessi valori, senza dare significato all’ordine delle proprietà. Al livello applicativo possiamo aggiungere altre equivalenze, ma dobbiamo definirle: un numero di versione intero 1 e la stringa “1” non sono intercambiabili per il nostro contratto. La somiglianza visiva non basta a stabilire equivalenza.

SHA-256 prende una sequenza di byte e produce un digest di 256 bit, spesso scritto con 64 cifre esadecimali. È deterministico: stessi byte danno lo stesso digest. L’uguaglianza degli hash non è una dimostrazione matematica di uguaglianza degli ingressi, perché lo spazio degli ingressi è più grande di quello dei digest; in questo esperimento controlliamo anche i byte direttamente. Un hash diverso prova che gli ingressi byte non erano identici, non che i dati applicativi avessero significato diverso.

Due file, un oggetto

L’oggetto guida contiene name uguale a café e version uguale all’intero 1. La prima serializzazione scrive prima name, poi version, senza spazi e con la é direttamente codificata in UTF-8. La seconda inverte l’ordine, aggiunge indentazione e rappresenta il carattere non ASCII con un escape JSON. Il parser ricostruisce lo stesso oggetto. I file occupano però 28 e 41 byte e hanno digest diversi. Il JSON dei risultati conserva testo, byte in esadecimale e SHA-256 completo, così il confronto non dipende da un’abbreviazione dell’hash.

parse(raw1) = parse(raw2) raw1 != raw2 SHA256(raw1) != SHA256(raw2)

Un rappresentante stabile, entro un dominio dichiarato

Se vogliamo confrontare i dati anziché la formattazione, definiamo una funzione locale C: ordina le chiavi, usa separatori compatti, conserva le stringhe e produce UTF-8. L’esperimento ammette soltanto oggetti con chiavi ASCII, stringhe Unicode codificabili, liste e interi fra −(2^53−1) e 2^53−1. Rifiuta float, booleani e altri tipi. È un contratto volutamente ristretto per poter capire il risultato; non è una libreria di canonicalizzazione JSON generale.

Sui due oggetti guida C produce gli stessi 28 byte e lo stesso digest. Questo è ciò che volevamo dimostrare: la variabilità di impaginazione e ordine non incide più sul confronto del nostro dominio. Non segue che ogni trasformazione prima dell’hash sia legittima. Se C eliminasse un campo significativo o convertisse tutte le versioni in stringhe, potrebbe nascondere una modifica importante. Una forma stabile deve preservare le distinzioni richieste dall’applicazione.

Le stringhe che sembrano uguali

Scriviamo ora café in due modi: con é come singolo punto di codice U+00E9 oppure con e seguita dall’accento combinante U+0301. Possono apparire uguali sullo schermo, ma le stringhe sono diverse. Nel nostro documento la forma composta occupa 28 byte, quella decomposta 29; C preserva la differenza e i digest non coincidono. Il codice mostra anche che una normalizzazione Unicode NFC esplicita può far coincidere queste due stringhe, ma non la applica silenziosamente a C. Per un identificativo, cambiare i caratteri potrebbe cambiare il dato che vogliamo proteggere.

CoppiaStesso oggettoStessi byteStesso digest
raw1 / raw2TFF
C(raw1) / C(raw2)TTT
composto / decompostoFFF

T indica vero e F falso. Nella riga C(raw1) intendiamo prima il parsing e poi C: C opera sui dati, non direttamente sul testo grezzo. Le righe non sono percentuali o probabilità di collisione; sono confronti eseguiti sui file specificati. La figura affianca dimensioni in byte e risultati logici, mantenendo separati i due tipi di misura.

Dimensioni UTF-8 dei documenti eseguiti e confronto fra identità dei dati, dei byte e del digest. C è la funzione locale ristretta dell’esperimento, non una implementazione completa di JCS.
Dimensioni UTF-8 dei documenti eseguiti e confronto fra identità dei dati, dei byte e del digest. C è la funzione locale ristretta dell’esperimento, non una implementazione completa di JCS.

Il riferimento JCS e il limite del nostro codice

RFC 8785 descrive JSON Canonicalization Scheme, o JCS, ed è un documento informativo del giugno 2020. Nelle sezioni tecniche lette richiede regole precise per numeri, proprietà e stringhe; la normalizzazione Unicode non viene applicata e le chiavi sono ordinate secondo unità di codice UTF-16. Il nostro sort_keys Python non dimostra conformità generale: fuori dalle chiavi ASCII il codice include U+E000 e U+10000, il cui ordine per punti di codice differisce da quello UTF-16. Inoltre non implementiamo la serializzazione numerica ECMAScript. Per questo chiamiamo C una convenzione locale, non “JCS fatto con una riga”.

Un hash di riferimento deve avere una storia di fiducia

Cambiamo version da 1 a 2. Il digest cambia e il confronto con il digest originale attendibile fallisce. Se invece chi può sostituire il documento può anche sostituire il manifesto non attendibile con il nuovo digest, il confronto passa. È esattamente ciò che il codice verifica. Non è una collisione né una debolezza matematica di SHA-256: abbiamo confrontato con il riferimento sbagliato. La funzione hash da sola non lega il contenuto a un’identità o a un momento di approvazione.

Una firma o un codice di autenticazione può rispondere ad altre domande, con chiavi e politiche di fiducia appropriate; non li implementiamo qui e non presentiamo questa prova come audit crittografico. Anche un file autentico può contenere dati sbagliati. Integrità rispetto a un riferimento, provenienza e correttezza del contenuto richiedono evidenze diverse. La canonicalizzazione aiuta a ripetere un confronto concordato, ma non decide quale fonte meriti fiducia.

Applicazione concreta e limiti

Per i materiali editoriali di questa lavorazione sono stati davvero registrati SHA-256 e confrontati i byte degli asset scaricati dal sito con i file preparati. È un controllo del trasferimento di quei file, non un prodotto di sicurezza EL-AI né una prova di revisione scientifica umana. Se interessa preservare l’esatto PNG o archivio ZIP, non dobbiamo prima trasformarlo in un oggetto diverso: il confronto byte per byte è proprio l’obiettivo. Se interessa una configurazione JSON indipendente dall’impaginazione, serve invece definire e versionare il contratto di rappresentazione.

L’esperimento non esegue una batteria di conformità JCS, non misura throughput e non gestisce un protocollo completo per input ostili. Il contratto rifiuta diversi tipi prima di serializzare; i test registrano quei rifiuti anziché fingere di supportarli. Ordinare m chiavi richiede confronti dell’ordine di m log m, oltre al costo di leggere e scrivere il contenuto; il costo effettivo dipende anche dalle lunghezze delle stringhe. Nessun numero di prestazione viene dedotto da questa descrizione algoritmica.

Risposta: decidere prima quale uguaglianza serve

Gli hash dei due file guida sono diversi perché i byte sono diversi, anche se il parser produce lo stesso oggetto. Una rappresentazione locale stabile elimina le differenze di formato concordate, preservando quelle nei dati; non crea provenienza e non sostituisce un riferimento attendibile. Prima di chiedere se il checksum è corretto, dobbiamo quindi stabilire che cosa deve rimanere uguale e chi garantisce il valore atteso. È questo contratto, insieme al calcolo, a rendere utile la verifica.

Fonti e codice eseguito

RFC 8785 — JSON Canonicalization Scheme, §§3.1–3.2, 5.

Python 3.14 — hashlib.

import json,hashlib
a={'name':'café','version':1}
b={'version':1,'name':'café'}
x=json.dumps(a,ensure_ascii=False,separators=(',',':')).encode('utf8')
y=json.dumps(b,ensure_ascii=True,indent=2).encode('utf8')
assert json.loads(x)==json.loads(y)
assert x!=y and hashlib.sha256(x).digest()!=hashlib.sha256(y).digest()
print(len(x),len(y))  # 28 41

Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 21 ottobre 2026.