macOS-Programme signieren und notarisieren, ohne Mac
Ein Programm aus dem Netz startet auf macOS nur dann ohne Widerspruch, wenn es signiert und von Apple notarisiert ist. Sonst blockiert Gatekeeper es beim ersten Start. Gatekeeper ist die Prüfung, die macOS bei jedem heruntergeladenen Programm vornimmt. Meine Builds laufen aber auf Linux-Runnern, und einen Mac gibt es in der ganzen Kette nicht. Hier steht, wie es trotzdem geht und wo es nicht auf Anhieb klappte.
TL;DR
TL;DR: rcodesign signiert und notarisiert auf Linux, ganz ohne Xcode. Ein einzelnes Kommandozeilen-Programm lässt sich nicht heften, also geht es als ZIP zu Apple. Gatekeeper holt das Ticket dann beim ersten Start online. Im Merge Request probt die Pipeline das Signieren mit einem Wegwerf-Zertifikat. An den echten Schlüssel kommen nur Release-Tags einzelner Projekte. Als ich ein zweites Projekt freischalten wollte, scheiterte das Apply in der CI an einer Sperre, und die ist genau so gewollt.
01 — Signieren auf Linux
Apple liefert seine Werkzeuge zum Signieren nur für macOS. rcodesign ist eine freie Neuimplementierung. Sie läuft auf Linux und schreibt dieselben Signaturen. Damit signiert meine Pipeline zwei Projekte: s3mail und grundschutz-viewer.
s3mail ist eine App mit Fenster und kommt als .app-Bündel, also als Ordner mit Programm und Metadaten. grundschutz-viewer ist ein reines Kommandozeilen-Programm. Es besteht aus einer einzigen Mach-O-Datei, dem Format ausführbarer Programme auf macOS. Signiert werden beide mit demselben Befehl:
rcodesign sign --p12-file id.p12 --p12-password-file id.pw \
--binary-identifier eu.ole-hartwig.grundschutz-viewer \
--for-notarization grundschutz-viewer
--for-notarization erledigt drei Dinge auf einmal. Es verlangt ein echtes Developer-ID-Zertifikat, setzt einen Zeitstempel und schaltet die gehärtete Laufzeit ein. Die gehärtete Laufzeit nimmt dem Programm einige Freiheiten, etwa ungeprüften Code nachzuladen. Fehlt eines der drei, lehnt Apple die Notarisierung ab. --binary-identifier gibt dem Programm eine feste Kennung. Sonst leitet rcodesign sie aus dem Dateinamen ab.
02 — Notarisieren ohne Heften
Signiert ist das Programm jetzt, aber Gatekeeper reicht das nicht. Es will zusätzlich sehen, dass Apple das Programm geprüft hat. Diese Prüfung heißt Notarisierung: Die Pipeline lädt das Programm hoch, Apple untersucht es und stellt ein Ticket aus. Bei einem Bündel heftet rcodesign dieses Ticket direkt an. Das nennt sich Stapling, und Gatekeeper braucht dann beim ersten Start keine Verbindung zu Apple.
An eine einzelne Mach-O-Datei lässt sich kein Ticket heften. Deshalb packe ich das signierte Programm in ein ZIP und reiche das ZIP ein:
zip -q grundschutz-viewer-macos-arm64.zip grundschutz-viewer
rcodesign notary-submit --api-key-file notary.json --wait \
grundschutz-viewer-macos-arm64.zip
Das Ticket bleibt dann bei Apple, und Gatekeeper fragt es beim ersten Start online ab. Für v0.2.0 meldete Apple nach dem Hochladen Accepted. Das Protokoll der Notarisierung führte keine Probleme auf.
Eine Ablehnung, die keine ist
Beim Nachprüfen bin ich über spctl gestolpert, das Werkzeug, mit dem man Gatekeeper von Hand fragt. spctl --assess --type execute lehnte das Programm ab: Der Code sei gültig, aber offenbar keine App. Das sagt es bei jedem Kommandozeilen-Programm, auch wenn alles stimmt.
Mehr sagte ein einfacher Versuch. Ich habe das ZIP entpackt und das Quarantäne-Attribut gesetzt, wie es ein Browser beim Download tut. Dann habe ich das Programm gestartet. Es lief ohne Rückfrage, und codesign -dv zeigte die Developer ID samt Zeitstempel.
03 — Die Probe im Merge Request
Der echte Signierschritt läuft nur auf Release-Tags. Damit wird er auch nur dort getestet. Geht er kaputt, merkt man es ausgerechnet am Tag des Releases.
Deshalb läuft derselbe Job auch in jedem Merge Request. Statt der echten Developer ID nimmt er dort ein Wegwerf-Zertifikat, und die Notarisierung entfällt. Das Zertifikat erzeugt openssl im Job selbst:
openssl pkcs12 -export -inkey throwaway.key -in throwaway.crt \
-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 \
-out id.p12 -passout pass:throwaway
Die drei Schalter sind keine Zierde. rcodesign liest PKCS#12-Dateien nur im alten Format mit SHA1 und 3DES. OpenSSL 3 schreibt standardmäßig ein neueres. Gatekeeper würde ein so signiertes Programm zwar ablehnen. Befehle, Pfade und die Prüfung danach sind aber dieselben wie beim echten Release. Am Ende entpackt der Job jedes ZIP wieder und prüft, ob das Programm ausführbar ist und eine Signatur trägt.
Ein Detail gehört noch dazu. Der Signier-Job läuft auf einem Runner-Pool mit IPv4. Meine übrigen Runner sprechen nur IPv6, und von dort sind Apples Dienste für Notarisierung und Zeitstempel nicht zuverlässig erreichbar.
04 — Zwei Schlüssel, zwei Vertrauensgrenzen
Bleibt die Frage, wer überhaupt an die Schlüssel herankommt. Davon gibt es zwei, und sie brauchen unterschiedlich enge Grenzen.
Für Linux und Windows gibt es keine Signatur im Paket. Dort übernimmt die Prüfsummenliste SHA256SUMS diese Aufgabe. Sie führt jedes Paket auf und trägt eine cosign-Signatur. Deren Schlüssel liegt in KMS und verlässt es nie. Wer die Rolle dafür annimmt, kann KMS nur bitten, etwas zu signieren. Diese Rolle darf deshalb vielen Projekten vertrauen.
Beim Apple-Schlüssel geht das nicht. Eine Developer-ID-Signatur braucht den privaten Schlüssel als Datei, und wer die Datei lesen darf, kann sie in ein Job-Log schreiben. Die Rolle mit dem Signiermaterial vertraut deshalb nur einzelnen Projekten. Das regelt ihre Vertrauensrichtlinie: Die Pipeline weist sich mit einem OIDC-Token aus, und dessen Feld sub nennt Projekt und Ref. Erlaubt sind nur der volle Projektpfad und Release-Tags:
project_path:<gruppe>/<projekt>:ref_type:tag:ref:v*
Ein Platzhalter für eine ganze Gruppe wäre bequemer. Er würde den Schlüssel aber jedem Projekt darin geben. Ein Merge Request kommt an die Rolle gar nicht heran, und auch deshalb braucht er das Wegwerf-Zertifikat.
05 — Grüner Plan, gescheitertes Apply
Für grundschutz-viewer musste ein zweites Muster in diese Vertrauensrichtlinie. Das ist eine Zeile in meinem Infrastruktur-Repository. Der Merge Request lief durch, und der Plan zeigte genau diese Änderung. Alles war grün.
Das Apply auf main scheiterte dann mit AccessDenied auf iam:UpdateAssumeRolePolicy. Der Grund ist eine Rechtegrenze, also eine Obergrenze für das, was eine Rolle überhaupt darf. Die Rolle, mit der die CI Infrastruktur anwendet, hat so eine Grenze. Sie verbietet ausdrücklich, die Vertrauensrichtlinie anderer CI-Rollen zu ändern. Wer das darf, kann eine fremde Rolle auf sich selbst zeigen lassen und bekommt so deren Rechte.
Ein Fehler war das also nicht, sondern die Kontrolle bei der Arbeit. Der Plan prüft solche Sperren nicht, deshalb war er grün. Behoben habe ich es mit einem gezielten Apply von meinem Rechner, das nur diese eine Rolle anfasst. Danach habe ich die Vertrauensrichtlinie gelesen und beide Muster darin gefunden.
06 — Was sonst noch fehlte
Drei kleinere Stellen kamen noch dazu, bevor das erste Release durchlief.
- Das Signier-Image: Der Job läuft in einem eigenen Image mit rcodesign, AWS CLI, openssl und zip. Ein Projekt darf ein fremdes Image nur ziehen, wenn es auf dessen Job-Token-Allowlist steht. Im Merge Request lud der Job das Image auch ohne Eintrag, weil ich die Pipeline selbst ausgelöst hatte. Die Tag-Pipeline startet aber ein Bot mit dem Job-Token, und darauf wollte ich es beim ersten Release nicht ankommen lassen.
- Geschützte Tags: Das Token für den GitHub-Mirror ist eine geschützte Variable. Solche Variablen gibt es nur in Pipelines auf geschützten Branches und Tags. Auf einem ungeschützten Tag fehlt sie, und die Jobs für Mirror und GitHub-Release werden gar nicht erst angelegt. Rot wird dabei nichts, das Release erscheint nur auf einer Seite.
- Kein zweiter Upload: Ein zweiter Lauf der Tag-Pipeline baute neu und wollte hochladen. Das Package-Registry lehnte ab, weil es die Version schon gab. Das ist richtig so, denn die neuen macOS-Pakete trugen einen neuen Zeitstempel. Sie hätten nicht mehr zur veröffentlichten
SHA256SUMSgepasst. Ein solcher Lauf soll seitdem die veröffentlichten Dateien weiterreichen statt neu hochzuladen. Bewähren muss sich das erst beim nächsten Neustart.
Die Lehre
Signieren ist der kleinere Teil. Die eigentliche Arbeit steckt in zwei Fragen: Wer kommt an den Schlüssel heran? Und welche Probe zeigt einen kaputten Signierschritt, bevor ein Release davon abhängt? Ein Schlüssel, der als Datei gelesen werden muss, verdient eine engere Grenze als einer, der nur signieren kann.
Häufige Fragen
Brauche ich dafür ein Apple-Entwicklerkonto?+
Ja. Das Developer-ID-Zertifikat und den API-Schlüssel für die Notarisierung gibt es nur mit einem Konto im Apple-Entwicklerprogramm. Einen Mac brauchst du dafür nicht. Zertifikat, Passwort und API-Schlüssel liegen bei mir als drei getrennte Geheimnisse, die nur die Signier-Rolle lesen darf.
Warum nicht einfach xattr in die Anleitung schreiben?+
Weil das den Schutz abschaltet, statt ihn zu erfüllen. Mit xattr -d com.apple.quarantine startet auch ein unsigniertes Programm. Genau an diesem Schritt geben aber viele auf, die das Programm nicht selbst gebaut haben, und wer ihn gewohnt ist, führt ihn irgendwann auch bei einem fremden Download aus.
Was ist mit Windows?+
Die Windows-Pakete sind bei mir nicht signiert, und SmartScreen sagt das beim Start auch. Prüfen lässt sich ein Download trotzdem: über SHA256SUMS und dessen cosign-Signatur, die für alle Plattformen gleich ist.
Fazit
Für signierte und notarisierte macOS-Programme ist kein Mac nötig. rcodesign auf einem Linux-Runner reicht, und bei einem Kommandozeilen-Programm ersetzt ein ZIP das Heften. Mehr Aufmerksamkeit brauchen die Grenzen drumherum: eine Probe in jedem Merge Request und eine Rolle, die den Apple-Schlüssel nur Release-Tags einzelner Projekte gibt.
Am meisten mag ich an der Geschichte den roten Apply-Job. Er hat mich an einer Sperre aufgehalten, die ich selbst gebaut hatte, und genau dort soll sie halten. Die Pakete liegen im Release von grundschutz-viewer.
Ich bringe deine Programme signiert durch die CI, ohne dass der Schlüssel jedem Projekt offensteht.
Dabei richte ich Signierung und Notarisierung für macOS ein, die Probe im Merge Request und eine signierte Prüfsummenliste für Linux und Windows. Die Rollen mit dem Signiermaterial vertrauen danach nur den Projekten und Tags, die es wirklich brauchen.
Statt Beratung auf Papier begleite ich das erste Release, bis die Pakete beim Nutzer starten.
Über den Autor

Kai Ole Hartwig
Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.
