Notizen zur Entwicklungsaufgabe

Git-Signaturen in einer Cloud-Mac-CI prüfen

Git-Signaturen in einer Cloud-Mac-CI prüfen

Nachdem Code auf einen Cloud Mac übertragen wurde, checkt die CI ihn in der Regel sofort aus, baut ihn und führt Tests aus. Das Problem: Name und E-Mail-Adresse des Autors in einem Git-Commit sind lediglich editierbare Textfelder. Sie geben an, von wem der Commit angeblich erstellt wurde, belegen aber nicht, dass das Objekt tatsächlich von diesem Entwickler signiert wurde. Zuverlässiger ist es, Commits mit SSH zu signieren und die CI anschließend den gesamten Bereich von der Merge-Basis bis zum aktuellen Commit anhand einer kontrollierten Liste öffentlicher Schlüssel prüfen zu lassen.

Diese Schranke ersetzt kein Code-Review und beurteilt auch nicht, ob der Code sicher ist. Sie beantwortet eine enger gefasste, aber wichtige Frage: Sind die Git-Objekte unverändert, sind ihre Signaturen gültig und gehören die Signaturschlüssel Personen, die aktuell Code einreichen dürfen?

Zuerst die Vertrauensgrenzen der Prüfung festlegen

Die Verifikation eines Commits umfasst mindestens drei Ebenen:

  1. Stimmen der Inhalt des Git-Objekts und die Signatur überein?
  2. Ist der öffentliche Signaturschlüssel in der Liste der freigegebenen Signierenden enthalten?
  3. War diese Identität zum Zeitpunkt des Commits noch berechtigt, Code einzureichen?

Die erste Ebene wird durch die kryptografische Signatur abgedeckt, die zweite durch eine im Repository gepflegte Liste. Für die dritte Ebene bleibt ein funktionierender Prozess zum Entzug von Berechtigungen innerhalb des Teams erforderlich. Es genügt nicht, lediglich git log --show-signature auszuführen und „Good signature“ zu sehen: Auch ein gültiger, aber nicht autorisierter Schlüssel kann eine technisch gültige Signatur erzeugen.

Die E-Mail-Adresse dient der Anzeige und Benachrichtigung, der öffentliche Schlüssel der Verifikation und die Liste zugelassener Signierender der Autorisierung. Diese drei Funktionen dürfen nicht gleichgesetzt werden.

Die Liste sollte in einem separaten Verzeichnis liegen, dessen Änderungen streng kontrolliert werden, beispielsweise .ci/trusted_signers. Änderungen an der Liste selbst sollten ein zusätzliches Review erfordern. So wird verhindert, dass jemand im selben Änderungssatz den eigenen öffentlichen Schlüssel hinzufügt und damit zugleich den eigenen Code freigibt.

Git für SSH-signierte Commits konfigurieren

Ab Git 2.34 können SSH-Schlüssel direkt zum Signieren verwendet werden. Entwickler legen zunächst lokal das Signaturformat und den Pfad zum öffentlichen Schlüssel fest:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgSign true

Konfiguriert wird hier die Datei mit dem öffentlichen Schlüssel; die eigentliche Signatur wird mit dem zugehörigen privaten Schlüssel erzeugt. Nach dem Erstellen eines Commits lässt sich zunächst prüfen, ob das Objekt tatsächlich eine Signatur enthält:

git cat-file commit HEAD | sed -n '/^gpgsig /,/^[^ ]/p'

Jede Zeile der Datei mit den zugelassenen Signierenden enthält eine Identität, optionale Einschränkungen und einen öffentlichen Schlüssel. Für die Identität sollte eine stabile Teamkennung verwendet werden, nicht ein Anzeigename, der sich häufig ändern kann:

ci-release namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
ios-team namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5BBBB...

Nachdem die CI das Repository ausgecheckt hat, wird Git auf diese Datei verwiesen:

git config --local gpg.format ssh
git config --local gpg.ssh.allowedSignersFile .ci/trusted_signers
git verify-commit HEAD

Private Schlüssel gehören nicht in das Repository. Für die Verifikation in der CI werden ausschließlich öffentliche Schlüssel benötigt. Ein privater Schlüssel ist nur in der Umgebung erforderlich, in der signierte Commits oder Tags erstellt werden.

Den vollständigen Commit-Bereich absichern

Nur HEAD zu prüfen, ist ein häufiger Fehler. Ein schädlicher oder nicht autorisierter Änderungssatz kann in einem vorherigen Commit verborgen und durch einen abschließenden signierten Commit oberflächlich kaschiert werden. Die Prüfung muss daher alle Commits nach der vertrauenswürdigen Basis durchlaufen.

#!/bin/bash
set -euo pipefail

base="${MERGE_BASE_SHA:?missing MERGE_BASE_SHA}"
head="${HEAD_SHA:?missing HEAD_SHA}"

git cat-file -e "${base}^{commit}"
git cat-file -e "${head}^{commit}"

count=0
while IFS= read -r commit; do
  git verify-commit "$commit"
  count=$((count + 1))
done < <(git rev-list --reverse "${base}..${head}")

printf 'verified_commits=%s\n' "$count"

MERGE_BASE_SHA sollte von der CI anhand des Ziel- und des einzubindenden Branches berechnet oder aus einer verlässlichen Quelle übergeben werden. Der Wert darf nicht einfach auf HEAD~1 gesetzt werden. Bei Merge-Commits muss außerdem geprüft werden, ob die gewählte Basis der Teamrichtlinie entspricht. Andernfalls könnten Objekte unberücksichtigt bleiben, die über den zweiten Parent eingebracht wurden.

Die Ausgabe der Prüfung sollte den Commit-Hash und die Fehlerphase protokollieren. Vollständige Commit-Nachrichten oder Umgebungsvariablen gehören jedoch nicht in öffentlich einsehbare Logs. Bei einem Fehler die Erstellung abzubrechen, ist leichter zu auditieren, als weiterhin ein Artefakt mit ungeklärter Herkunft zu erzeugen.

Flache Klone, Tags und Schlüsselwechsel behandeln

Flache Klone führen häufig zu irreführenden Fehlermeldungen: Fehlt das Basisobjekt lokal, kann rev-list den vollständigen Bereich nicht ermitteln. Vor der Verifikation muss deshalb die benötigte Historie des Ziel-Branches nachgeladen werden. Anschließend ist mit git cat-file -e ausdrücklich zu prüfen, ob die Basis vorhanden ist. Wenn sie nicht gefunden wird, darf die Prüfung nicht auf ausschließlich HEAD zurückfallen.

Szenario Korrekte Vorgehensweise Unzulässige Abkürzung
Die Basis fehlt im flachen Klon Benötigte Historie nachladen und den Bereich neu berechnen Nur den letzten Commit prüfen
Release-Tag Ein signiertes annotiertes Tag verwenden und git verify-tag ausführen Nur den Namen des Tags prüfen
Übergang vom alten zum neuen Schlüssel Beide öffentlichen Schlüssel vorübergehend beibehalten Den Schlüssel sofort ersetzen und dadurch historische Jobs fehlschlagen lassen
Kompromittierter Schlüssel Schlüssel sofort entfernen und den damit signierten Bereich erneut prüfen Nur die angezeigte Identität ändern
Ausgeschiedenes Teammitglied Den Eintrag aus der Liste der zugelassenen Signierenden entfernen Nur den regulären Anmeldezugang sperren

Ob historische Commits nach dem Entfernen eines Schlüssels weiterhin als gültig gelten sollen, muss im Voraus festgelegt werden. Der einfachste strikte Modus prüft gegen die jeweils aktuelle Liste und eignet sich für Merge-Schranken. Wenn historische Releases langfristig verifiziert werden müssen, sollten Vertrauensdatensätze mit Gültigkeitszeiträumen geführt werden. Zusätzlich sind das Release-Tag, der Commit-Hash und die damals verwendete Version der Liste gemeinsam zu archivieren.

Vorabprüfung und Fehlerdiagnose

Bevor fehlerhafte Prüfungen Merges tatsächlich blockieren, kann zunächst eine Beobachtungsphase eingerichtet werden. In dieser Phase werden Fehler lediglich protokolliert; nicht signierte Releases dürfen dennoch nicht freigegeben werden. Dabei sollten insbesondere folgende Punkte geprüft werden:

Führen Sie bei der Fehlersuche zunächst git verify-commit --raw <hash> aus, um zwischen „Objekt ist nicht signiert“, „Signatur ist beschädigt“ und „öffentlicher Schlüssel fehlt in der Liste“ zu unterscheiden. Prüfen Sie danach die Repository-Konfiguration für gpg.format und gpg.ssh.allowedSignersFile sowie Namespace, Schlüsseltyp und Zeilenumbrüche in der Liste. So lassen sich Autorisierungsprobleme von einer unvollständigen Git-Historie trennen, statt denselben Commit immer wieder neu zu signieren.

Sobald die Prüfung jedes einzelnen Commits, die Tag-Verifikation und das Review von Listenänderungen Bestandteil der Pipeline sind, verfügt das Build-System über eine nachvollziehbare Herkunftskette. Sie beweist nicht, dass der Code fehlerfrei ist. Sie beantwortet jedoch eindeutig, welche Objekte mit welchem zugelassenen Schlüssel signiert wurden und warum nicht bestätigte Objekte nicht in nachgelagerte Build-Schritte gelangt sind.

Häufig gestellte Fragen

Reicht eine korrekte Autorenadresse aus, um einem Commit zu vertrauen?

Nein. Name und Adresse sind frei wählbare Textfelder. Die Signatur belegt den Besitz eines privaten Schlüssels; erst die Liste zulässiger Unterzeichner ordnet diesen Schlüssel einer freigegebenen Identität zu.

Genügt es, nur den letzten Commit eines Branches zu prüfen?

Nein. Jeder Commit nach der vertrauenswürdigen Basis kann den Quellstand verändern. Deshalb muss git verify-commit über den gesamten Bereich laufen; signierte Release-Tags benötigen zusätzlich git verify-tag.

Wie lässt sich ein Signaturschlüssel ohne CI-Ausfall wechseln?

Fügen Sie zuerst den neuen öffentlichen Schlüssel zur versionierten Liste hinzu, erlauben Sie während des Übergangs beide Schlüssel und entfernen Sie den alten erst nach erfolgreicher Prüfung.

Dedizierte Entwicklungsumgebung

Konfigurieren Sie für die nächste Entwicklungsaufgabe einen Cloud-Mac

Wählen Sie Oak M4, Oak M4 Plus oder Oak M4 Pro und konfigurieren Sie ein Mietmodell an einem von fünf physischen Standorten entsprechend dem Standort Ihres Teams.

Cloud-Mac konfigurieren