De aanstaande overstap naar SHA-256 als standaard in Git 3.0 zal een kostbare fout zijn

Ik heb dit onderwerp een paar jaar laten rusten, voornamelijk omdat er slimmerere mensen zijn die zich hierop hebben geconcentreerd en ik niet graag een "achterbankchauffeur" ben. Echter, ik geloof dat de release van Git 3.0 iedereen veel tijd en angst zal kosten voor weinig voordeel, terwijl vrijwel niemand weet wat er eraan komt.

Laat me vertellen hoe een van de aanstaande breaking changes in Git 3.0 zal uitlopen op een enorme, kostbare globale treinramp met bijna geen praktische waarde.

Basis: SHA-1 in Git

Git is wat we een content addressable database noemen. Dit betekent dat wanneer je gegevens wilt opslaan en verzenden, Git een hash van de inhoud berekent en deze als sleutel gebruikt in een key-value database (waarbij de waarde de eigenlijke inhoud is). Dezelfde inhoud krijgt wereldwijd altijd dezelfde hash.

Dit is gunstig, omdat dezelfde bestandsinhoud nooit twee keer wordt opgeslagen. Bovendien coderen commits de hash van de commit die eraan voorafging, wat betekent dat deze integriteit zich voortplant. Je kunt de hash van niets veranderen zonder de hash van alles wat daarna komt te veranderen. Dit zorgt voor "cryptografische integriteit": het hashen van de laatste commit betekent in feite dat potentieel miljoenen bestandsinhouden, trees en commits die eraan voorafgingen, ook zijn gehasht.

In Git is deze hashfunctie altijd SHA-1 geweest. Linus Torvalds koos hiervoor in 2005 bij de start van Git. Het heeft 20 jaar lang goed gewerkt; het is relatief snel en het is in de praktijk onmogelijk dat twee verschillende bestanden per ongeluk dezelfde hashwaarde krijgen.

Voor zover bekend is dit nooit gebeurd in de geschiedenis van alle bestanden, trees en commits die ooit in Git-repositories zijn gemaakt — miljarden en miljarden. Wiskundig gezien betekent de birthday bound voor de 160-bit output van SHA-1 dat je ongeveer 1,4 septiljoen willekeurige bestanden (1,4 quadriljoen miljard bestanden) in één project zou moeten hebben om per ongeluk hash-collisies te krijgen.

SHA-1 is "gebroken"

Er is echter een probleem: wiskundig gezien wordt SHA-1 nu als semi-"gebroken" beschouwd omdat er gepubliceerde collisie-aanvallen zijn geweest (zoals SHAttered in 2017 en SHA-1 is a Shambles in 2020). Hoewel dit niet praktisch exploiteerbaar is gebleken, is het theoretisch mogelijk.

Als reactie hierop is gepland om in Git 3.0 het standaard hashing-algoritme te wijzigen van het semi-"gebroken" SHA-1 naar het sterkere SHA-256 algoritme.

Maar laten we eerst stilstaan bij wat "gebroken" precies betekent. Vanuit het perspectief van cryptografisch hashen betekent dit in deze context dat het vinden van collisies niet onmogelijk is. Met genoeg geld en GPU's kun je voor bepaalde inhoudsstructuren opzettelijk twee verschillende dingen creëren die exact dezelfde hashwaarde hebben.

Dit betekent dat hoewel het nog steeds bijna onmogelijk is om per ongeluk twee redelijke bestanden met verschillende inhoud te hebben die dezelfde hash hebben, het technisch niet onmogelijk is om twee verschillende bestanden te manifactureren die naar hetzelfde hashen. Theoretisch bestaan er aanvalvectoren waarbij iemand de inhoud van een bestand kan vervangen door een kwaadaardige versie, waarbij Git het verschil niet kan zien omdat de hash wiskundig identiek is.

Collisie-aanvallen

Laten we een stap terug doen en kijken naar deze collisies. Er zijn twee hoofdproblemen bij hashfuncties wanneer we kijken naar aanvallen: de "collisie-aanval" en de "second-preimage-aanval".

Collisie-aanval (Collision attack)

Bij een collisie-aanval genereert de aanvaller opzettelijk twee bestanden met dezelfde hash: één onschuldig en één kwaadaardig. De aanvaller verspreidt eerst het onschuldige bestand om vertrouwen te winnen, en vervangt dit later door het kwaadaardige bestand. Omdat de hash matcht, kan Git het verschil niet zien. Men kan zelfs ondertekende tags of commits op trees hebben die het onschuldige bestand bevatten, waardoor het lijkt alsof het slechte bestand is ondertekend.

Second-preimage-aanval

Een second-preimage-aanval is wanneer een aanvaller een bestaand bestand ziet dat hij wil vervangen en vervolgens een tweede, kwaadaardig bestand creëert dat toevallig dezelfde hash heeft. Dit betekent niet dat de auteur van het originele bestand ook de aanvaller hoeft te zijn.

Het belangrijkste punt hier is dat, hoewel second-preimage-aanvallen zorgwekkender zijn, vrijwel geen enkele veelgebruikte hashfunctie hier gevoelig voor is. Zelfs als Git MD5 zou gebruiken (wat als extreem gebroken wordt beschouwd), zou het effectief immuun zijn voor een second-preimage-aanval.

Ter illustratie: als elke GPU op aarde (ongeveer 3 miljard) zou worden vervangen door een RTX 5090 en 100% van de tijd alleen maar MD5 zou berekenen, zou de verwachte tijd om een specifieke preimage te brute-forcen nog steeds ongeveer 16 miljard jaar zijn (ongeveer de leeftijd van het universum).

$$\frac{3.4 \times 10^{38} \text{ checks}}{3 \times 10^9 \text{ GPUs} \times 2.2 \times 10^{11} \text{ hashes/sec}} \approx 5.2 \times 10^{17} \text{ seconden} \approx 16 \text{ miljard jaar}$$

Theoretische aanvalvectoren

Een realistische aanvalsvector leunt dus op een collisie-aanval, wat betekent dat de persoon die het originele bestand introduceert, ook de kwaadaardige versie heeft voorberekend.

Zelfs als we conservatief zijn en aannemen dat een preimage makkelijk te maken is — stel dat je op je laptop in een uur een tweede preimage kunt maken voor elk bekend bestand — is er nog steeds een probleem. Hoe zorg je ervoor dat:

  1. Dat bestand wordt opgehaald door mensen die je niet kennen en nooit een vorige versie van deze content hebben gepulld?
  2. Zij het op een manier uitvoeren die voor jou nuttig is?

Hashing is in de wereld van SCM (Source Control Management) niet het primaire mechanisme van vertrouwen. Het biedt cryptografische integriteit, maar vertrouwen is fundamenteel gebaseerd op distributie.

Linus Torvalds beargumenteerde dit al bij de geboorte van Git in 2005:

"I really think people should not consider the sha1 the 'security'. The real security is in distribution."

Actuele aanvallen

Wat ziet een werkelijke aanval eruit in de praktijk? Het blijkt dat het invoegen van onbetrouwbare content in codebases regelmatig gebeurt, maar niet via GPU's en hash-collisies.

Het gebeurt via social engineering: iemand verkrijgt schrijfrechten op een npm-package die door miljoenen projecten wordt gebruikt. Dit is miljarden keren eenvoudiger, goedkoper en effectiever dan het brute-forcen van een hash-collisie.

Als ik kwaadaardige code in Android zou willen krijgen, is het veel simpeler om de maintainer van een populair downstream-project om te kopen of te overtuigen, het project over te nemen en code te injecteren via een vertrouwde bron-URL, dan te proberen een hash-collisie te engineeren en deze op een URL te plaatsen waar niemand ooit van zou pullen.

Vertrouwen in de Git-wereld

Ik pull code van https://github.com/rust-lang/rust niet omdat ik vertrouw op de GPG-signatuur van de laatste commit-SHA, maar omdat ik erop vertrouw dat GitHub zijn authenticatie op orde heeft, waardoor het onwaarschijnlijk is dat iemand kwaadaardigs iets heeft gepushed zonder dat de maintainer het weet.

Elk scenario dat ik ken ter verdediging van de dure SHA-256 migratie is onrealistisch. Deze risico's worden eenvoudiger en goedkoper gemitigeerd door betere bescherming van bronnen en content, in plaats van door een iets sterker hash-algoritme.

Als we accepteren dat SHA-1 simpelweg een goede en snelle methode is om unieke sleutels voor content te genereren in een vertrouwde repository, is er geen reden om het te vervangen. Accidentele content-collisies zijn wiskundig bijna onmogelijk.

Als we dit niet accepteren, raken we in een loop: elke keer dat er een nieuw theoretisch collisie-exploit wordt gepubliceerd, moeten we het hele Git-ecosysteem migreren. We gaan nu door de SHA-1 naar SHA-256 migratie, en zodra quantumcomputers SHA-256 breken, zitten we weer in hetzelfde schuitje. We verwarren cryptografische integriteit met vertrouwen.

De logistieke nachtmerrie

De kosten van deze overstap zullen aanzienlijk zijn. Laten we bekijken wat er gebeurt als Git 3.0 met deze nieuwe standaard wordt uitgebracht.

Repositories en compatibiliteit

Alle nieuwe repositories die worden aangemaakt met git init zullen SHA-256 gebruiken (dit kan nu al worden getest met git init --object-format=sha256).

Het eerste probleem is dat je deze code niet naar GitHub kunt pushen (hoewel dit waarschijnlijk is opgelost tegen de tijd dat 3.0 uitkomt). Maar wanneer dat wel kan, moet je bij het aanmaken van de repository op de server aangeven dat dit een SHA-256 project is. Projecten kunnen niet gemengd worden.

Dit zal leiden tot veel frustratie. Gebruikers moeten weten welke versie van Git ze hebben gebruikt bij git init en de juiste keuze maken op GitHub. We zullen veel van de volgende foutmeldingen zien: fatal: the receiving end does not support this repository's hash algorithm

Bibliotheken, links en tooling

  • Submodules: Deze kunnen alleen worden gebruikt met projecten van hetzelfde type. Bibliotheken zullen twee versies moeten bijhouden.
  • Conversie: Bestaande projecten die overstappen naar SHA-256 moeten elk object in het project converteren, wat alle bestaande signaturen breekt. Iedereen die aan het project werkt, moet tegelijkertijd overstappen om split head problemen te voorkomen.
  • Links: Alle bestaande URL's of links in Slack of e-mails die een SHA-1 hash bevatten, zullen niet meer werken.
  • Tooling: Alle interne tools die rekenen op hashes van 40 tekens zullen breken. Omdat veel Git-ecosysteem tools eigen implementaties gebruiken in plaats van de Git-binary, zal er veel schade zijn.

Een alternatief: Onafhankelijke Tree Hash Headers

In plaats van het hele ecosysteem te bifurceren, kunnen we een simpelere oplossing gebruiken voor de projecten die echt om extra veiligheid geven: gebruik de hash niet om content te vertrouwen.

De voorgestelde aanpak

Wat als we de tree-inhoud onafhankelijk rehashen met een ander algoritme (bijv. SHA-256) en die header injecteren in de objecten die we signeren? We signeren dan beide hashes: SHA-1 voor content-ophaal en SHA-256 voor onafhankelijke verificatie.

Dit is geen nieuw idee; git-evtag doet dit al sinds 2015. Het voegt een checksum over de commit, tree en elke blob toe aan de tag voordat deze wordt ondertekend.

Voordelen en prestaties

Als SHA-256 ooit wordt gecompromitteerd, voegen we simpelweg een nieuwe hashfunctie toe (bijv. BLAKE3) zonder dat elk project ter wereld hoeft te migreren.

De kosten voor het creëren van deze onafhankelijke handtekening zijn minimaal. In een test met Chromium (het ergste scenario: 35GB working tree met 2,1 miljoen bestanden) genereerde de tool een checksum in 5 seconden op een M5 Mac. De Linux tree (1,5GB) kostte 257ms en het Git project slechts 17ms.

Conclusie en NIST-compliance

Deze benadering lost ook het probleem van NIST-compliance op. De deadline van NIST voor 2030 gaat over het gebruik van SHA-1 voor "cryptografische bescherming", niet over het bestaan van SHA-1 in je stack. Als elke signatuur ook een SHA-256 content-hash dekt, beschermt SHA-1 niets meer; het is dan slechts een content-adresseerbare sleutel.

We kunnen stoppen met het gebruiken van SHA-1 voor het vertrouwen van content, zonder SHA-1 weg te gooien en het hele ecosysteem kapot te maken.