Vertrauen durch Verifizierung: Commit-Signierung in Git

Git bietet Wege, Commits über Signaturen eindeutig Verantwortlichen zuzuordnen. Auf diese Weise die Security zu verbessern, muss nicht aufwendig sein.

In Pocket speichern vorlesen Druckansicht 29 Kommentare lesen
Elektronische Signatur

(Bild: Thapana_Studio/Shutterstock)

Lesezeit: 10 Min.
Von
  • Janosch Deurer
Inhaltsverzeichnis

Man stelle sich vor, Stefan ist ein aktives und geschätztes Mitglied der Open Source Community und trägt regelmäßig Code zu einem Projekt bei. Aber jetzt findet Petra einen Commit unter seinem Namen, der eine Backdoor enthält. Stefans Ruf ist ruiniert, obwohl er den Commit nicht erstellt hat.

Das Beispiel ist fiktiv, allerdings hat mit der XZ-Hintertür im April ein großer Supply-Chain-Angriff für Wirbel gesorgt. Dabei hat zwar vermutlich niemand Commits unter dem Namen existierender Developer erstellt, aber der wirkliche Autor der Codeeinreichung ist bis heute unbekannt.

Sowohl in Open-Source- als auch geschlossenen Projekten ist das erste Glied in der Supply Chain der Commit. Wenn sich dessen Herkunft nicht nachvollziehen lässt, können Entwicklerinnen und Entwickler Schadcode unter unbekanntem oder falschem Namen ins Projekt einschleusen.

Ein Git-Server wie bei GitLab oder GitHub stellt eine Authentifizierung zur Verfügung, sodass nur berechtigte User pushen können. Warum sollte man Commits zusätzlich signieren? Weil Git erlaubt, sie unter beliebigem Namen zu erstellen. Dazu muss man lediglich den Autor und die E-Mail-Adresse in der lokalen Git-Konfiguration anpassen. Abhilfe schafft die Commit-Signatur. Dank ihr ist kryptografisch sicher nachvollziehbar, wer einen Commit erstellt hat.

Da Git dezentral funktioniert, sind lokal beliebige Operationen möglich. Beim Push auf den Server könnte dieser theoretisch verhindern, dass Commits mit einem anderen Autor gepusht werden. Warum passiert das nicht? Zunächst unterscheidet Git zwischen Autor und Committer. Der Autor hat dabei die Codeinhalte erstellt, während der Committer die Änderung eingereicht hat. Meist sind beide identisch. Wenn allerdings jemand Änderungen über Rebase, Cherry Picking oder Patches übernimmt, ersetzt Git bestehende Commits durch neue. Dabei behält das System den ursprünglichen Autor bei und trägt den aktuellen User als Committer ein. GitHub stellt diese Commits wie in Abbildung 1 gezeigt dar.

GitHub zeigt sowohl den Autor als auch den Committer der Änderung an (Abb. 1).

(Bild: Screenshot (Janosch Deurer))

Damit lässt sich unterscheiden, wer den Code geschrieben und wer den Commit erstellt hat. Das ist wichtig, da der Autor nicht allein für den Code verantwortlich ist. Auch beim Rebase oder ähnlichen Operationen können sich Fehler einschleichen.

Der Committer lässt sich allerdings nicht nur in sinnvollen Fällen wie oben beschrieben ändern, sondern auch fälschen: Den Commit in Abbildung 1 habe ich im Namen meines Kollegen ohne sein Zutun erstellt. Dass der Git-Server solche Commits annimmt, ist beispielsweise für den Push eines bestehenden Repository auf einen neuen Server erforderlich. Der Push enthält die vollständige Historie mit den Committern.

Git kennt mit dem Commit-Hash eine Funktion, um die Integrität der Codebasis und der gesamten Commit-Historie kryptografisch sicherzustellen. Das garantiert, dass bei zwei Commits mit demselben Commit-Hash die Historien ebenfalls identisch sind. Signierung stellt zusätzlich sicher, dass die enthaltenen Commits von den angegebenen Autoren stammen.

Bei der Commit-Signierung dient ein privater Schlüssel dazu, den gesamten Commit inklusive Tree, Parent, Autor, Committer und Commit-Nachricht zu signieren. Die digitale Unterschrift betrifft alle Daten, die der Commit-Hash abbildet. Damit ist bei jedem Push sichergestellt, dass der Commit von demjenigen stammt, der ihn signiert hat.

Zum Signieren von Git-Commits stehen prinzipiell drei Varianten zur Verfügung: über einen GPG-Key, über einen SSH-Schlüssel oder per X.509 mit S/MIME. Da SSH-Schlüssel zur Authentifizierung gegenüber dem Git-Server üblich sind, ist es meist am einfachsten, einen bestehenden SSH-Key zum Signieren zu verwenden. GPG-Schlüssel sind etwas aufwendiger zu verwalten, haben dafür aber ein optionales Ablaufdatum und lassen sich anders als SSH-Keys zudem zurückziehen, wenn sie kompromittiert wurden.

Beim Einsatz im Open-Source-Bereich steht außerdem eine Reihe von GPG-Key-Servern bereit, die ein sogenanntes Web of Trust bilden, um die Identität des Schlüsselbesitzers auch ohne physischen Kontakt sicherzustellen. Für große Organisationen kann es sich lohnen, auf S/MIME in Verbindung mit einem X.509-Zertifikat einer Public-Key-Infrastruktur (PKI) zu setzen.

Die folgenden Befehle erzeugen zunächst einen neuen SSH-Key, stellen dann die Commit-Signierung auf SSH um und geben schließlich den SSH-Schlüssel zum Signieren an:

ssh-keygen -t ed25519 -a 100
git config --global gpg.format ssh
git config --global user.signingkey /PFAD/ZUM/SSH/PUBLIC/KEY

Die folgenden drei Befehle erzeugen einen GPG-Key und geben anschließend die Liste der Schlüssel aus. Schließlich nutzt der dritte Befehl die Schlüssel-ID, um den Key in Git als Standardschlüssel zu konfigurieren:

gpg --full-generate-key
gpg --list-secret-keys --keyid-format LONG <e-Mail>
git config --global user.signingkey <Schlüssel-ID>

Der Parameter -S dient dazu, signierte Commits zu erstellen:

git commit -S

Alternativ lässt sich Git so konfigurieren, dass es alle Commits automatisch signiert:

git config --global commit.gpgsign true

Zunächst ist sicherheitstechnisch damit noch nichts gewonnen, da das Signieren von Commits unter falschem Namen und mit beliebiger Signatur weiterhin möglich ist. Es fehlt noch die Verbindung zwischen dem Schlüssel und der zugehörigen Person. Eine Public Key Infrastructure oder ein Web of Trust erfüllen diese Aufgaben.

Für kleinere Teams ohne eine solche Infrastruktur ist ein manueller Schlüsselaustausch möglich, aber oft zu aufwendig. Am einfachsten ist es in dem Fall, dem Git-Server die Aufgabe zu überlassen. Sowohl GitHub als auch GitLab ermöglichen es, öffentliche Schlüssel im Nutzerprofil zu hinterlegen, um die Signaturen zu überprüfen. Bei GPG und S/MIME vergleichen die Server die im Schlüssel hinterlegte E-Mail mit der des Committer. Bei SSH-Signaturen nutzen die Server die E-Mail-Adresse des GitHub- beziehungsweise GitLab-Accounts zum Vergleich. Wenn ich nun einen signierten Commit unter dem Namen meines Kollegen erstelle, markiert GitHub den Urheber als "Unverified" (siehe Abbildung 2).

GitHub zeigt an, dass es den Commit-Urheber nicht verifizieren kann (Abb. 2).

(Bild: Screenshot (Janosch Deurer))

Wenn ich den Commit unter meinem eigenen Namen erstelle, zeigt GitHub dagegen den Urheber als "Verified" an (siehe Abbildung 3).

GitHub deklariert den verifizierten Urheber (Abb. 3).

(Bild: Screenshot (Janosch Deurer))

GitHub bietet zusätzlich einen sogenannten Vigilant Mode (wachsamer Modus), in dem es jeden Commit als "Unverified" kennzeichnet, der nicht mit der E-Mail des aktuellen Users signiert ist. Bei GitLab verhält sich das Interface ähnlich, aber die Plattform kennt keine Funktion, die dem Vigilant Mode entspricht.

Um sicherzustellen, dass der Server nur Commits mit valider Signatur annimmt, empfiehlt sich ein automatisierter Prozess. Bei selbstverwalteten Git-Servern können Pre-Receive-Hooks beim Push alle Commits prüfen. In GitHub lässt sich ein solcher Hook über eine Branch Protection Rule konfigurieren, die allerdings für private Repositories nur kostenpflichtig verfügbar ist. GitLab-Kunden können ebenfalls in der kostenpflichtigen Version forcieren, dass Commits immer signiert sind. Der Server überprüft dabei allerdings nicht, ob die Signaturen valide sind.

Um die Validität sicherzustellen, lassen sich alternativ die Signaturen in der Pipeline von Merge Requests überprüfen. Um zu verhindern, dass jemand diese einfach umgeht, muss die Konfiguration festlegen, dass Commits nur über Merge Requests, die erfolgreich die Pipeline durchlaufen, auf dem Hauptzweig landen können. Außerdem sind verpflichtende Code Reviews erforderlich, damit keine Einzelperson die Pipeline umgehen kann, indem sie beispielsweise den zugehörigen Schritt in der Pipeline auskommentiert. In GitLab lässt sich in einer Pipeline überprüfen, ob alle zu einem Merge Request gehörigen Commits als "Verified" markiert sind. Folgender Code erledigt die Abfrage über die GitLab-API:

Verify Commits Signature:
  stage: verify-signature
  needs: [ ]
  image: alpine:3.19.1@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
  script:
    - |
      apk add --no-cache curl jq

      # API endpoint to get commits from the merge request
      COMMIT_API_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/commits"
      
      # Making API call to get commits
      COMMITS=$(curl --header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" "$COMMIT_API_URL")
      
      # Get commit SHAS
      COMMIT_SHAS=$(echo "$COMMITS" | jq -r '.[] |  .id')

      # Loop through each commit SHA to check its signature
      for SHA in $COMMIT_SHAS; do
        # API endpoint to get the commit's signature
        SIGNATURE_API_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/repository/commits/$SHA/signature"
        
        # Making API call to get the commit's signature details
        SIGNATURE=$(curl --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" "$SIGNATURE_API_URL")
        
        # Get verification status of commit
        VERIFICATION_STATUS=$(echo "$SIGNATURE" | jq '.verification_status')
        
        # Check if the signature is verified
        if [ "$VERIFICATION_STATUS" != '"verified"' ]; then
          echo "Commit $SHA is not signed or the signature is not verified."
          exit 1
        fi
      done
      
      echo "All commits are signed and verified."

Wer das Signieren für alle Commits forciert, muss dafür sorgen, dass Bots, die Commits erstellen, wie Renovate oder Dependabot ebenfalls mit validen Signaturen arbeiten. Darüber hinaus bereiten manche Funktionen in der Oberfläche von GitHub und GitLab Probleme. Unter anderem führt bei beiden Plattformen der Rebase in der Oberfläche zu unsignierten Commits. Beim Merge signiert GitHub seine Commits automatisch, aber GitLab lässt dabei ebenfalls die Signatur weg.

Teams nutzen Tags meist, wenn sie Versionen der Software veröffentlichen. Damit sind sie ein essenzieller Bestandteil des Release-Prozesses und somit auch der Software Supply Chain. Deshalb erhöht es die Sicherheit, die Tags ebenfalls zu signieren. Das funktioniert analog zum Signieren der Commits mit

git tag -s meinNeuerTag

Git-Commit-Signaturen sichern das erste Glied in der Software Supply Chain ab. Teams können das Signieren mit SSH-Key einfach einrichten. Die Sichtbarkeit der Verantwortlichen auf GitHub und GitLab erhöht die Security. Wie viel Aufwand es ist, jeden Commit mit Signaturen zu versehen und diese zu validieren, um eine deutlich höhere Sicherheitsstufe zu erreichen, hängt von den eingesetzten Tools und Workflows im Team ab.

Wer Codereviews nutzt, kann die Pipeline anpassen. Wer viele Automatisierungstools und Bots wie Renovate verwendet, muss die Konfigurationen anpassen, damit die Bots ebenfalls gültige Signaturen erstellen. Teams müssen evaluieren, wie groß der Gesamtaufwand ist, und den Sicherheitsgewinn in Abhängigkeit zu den Projektanforderungen abwägen.

Janosch Deurer
ist Gründer und CEO der IT Consulting Firma corewire GmbH. Als DevOps- und Cloud-Consultant mit Spezialisierung auf Kubernetes, AWS und CI/CD-Pipelines liegt ihm am Herzen, Deployment- und Infrastruktur-Lösungen zu finden, die sich nahtlos in die Arbeitsabläufe der Teams integrieren lassen. Er blickt dabei gerne über den DevOps-Tellerrand hinaus und sorgt dafür, dass Veränderungen in der gesamten Firma auf Zustimmung stoßen. Zusätzlich engagiert er sich aktiv in der DevOps-Community mit der Veranstaltung des DevOps Meetup Freiburg.

(rme)