Software Development

Schütze deine Travis CI-Releases - Teil 2: Signatur mit OpenPGP

Im ersten Teil unserer Serie, habe ich gezeigt, wie man eine Hash-Code-Datei für ein Travis-CI-Release erzeugt, mit der man auf einfache Weise die Integrität überprüfen kann. In diesem Teil werden wir einen Schritt weitergehen und die Release-Authentizität mit OpenPGP-Signaturen sicherstellen. Dadurch werden Angreifer Probleme bekommen, z. B. wenn sie Malware injizieren möchten.

Release-Authentizität sicherstellen mit OpenPGP

Unser Setup hat einen Nachteil: Ein Angreifer, der die ZIP-Datei verändern kann, kann höchstwahrscheinlich gleichzeitig auch die Hash-Datei verändern, das heißt man würde den Angriff dann nicht mehr bemerken. Um dies zu verhindern, ist es gängige Praxis, entweder die ZIP-Datei oder die Hash-Datei zu signieren. Damit kannst du sicherstellen, dass die Hash-Datei wirklich zu deinem Build gehört und nicht von einem Angreifer stammt. Ein Angreifer mag ZIP-Datei und Hash-Datei fälschen, z. B. durch Umleitung des Datenverkehrs, aber es ist sehr unwahrscheinlich, dass er auch die Signatur fälschen kann.

Eine der am weitesten verbreiteten Technologien zum Signieren von Daten ist OpenPGP. Glücklicherweise ist die bekannte GnuPG-Implementierung dieses Standards im Travis-Minimal-Container enthalten.

Bleibt nur noch ein Problem: Wir benötigen zum Signieren zur Build-Zeit einen privaten PGP-Schlüssel, d.h. er muss im Repository existieren. Weil das Repository öffentlich und damit ein gutes Ziel für Angreifer ist, ist es keine gute Idee, den eigenen privaten Schlüssel dafür zu nutzen. Das Risiko einer Kompromittierung ist einfach zu groß. Und wir müssen den Schlüssel verschlüsseln, damit Nutzer mit Klonen deines Repositorys ihn nicht einfach benutzen können.

Generierung des Schlüsselpaars für die Signierung

Zunächst benötigst du ein PGP-Schlüsselpaar, das als dein Signaturschlüssel fungiert beziehungsweise als der deines Repositorys. Du benötigst einen Hauptschlüssel und einen Unterschlüssel, der ausschließlich die Fähigkeit „sign“ besitzt. Folge einfach den unten stehenden Schritten, oder lies in meiner Blogserie über OpenPGP nach. Lass die Passphrase leer, d.h. keine Passphrase. Wir werden den Schlüssel mit einer anderen Methode schützen.

Haupt- und Unterschlüssel generieren

C:\Users\mosig_user>gpg --full-gen-key --expert
gpg (GnuPG) 2.2.11; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(9) ECC and ECC
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(13) Existing key
Your selection? 8

Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Sign Certify Encrypt

(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished

Your selection? s

Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Certify Encrypt

(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished

Your selection? e

Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Certify

(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished

Your selection? q
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 366
Key expires at 07/19/20 10:27:30 W. Europe Daylight Time
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: John Doe
Email address: john.doe@online.host
Comment:
You selected this USER-ID:
"John Doe <john.doe@online.host>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key 0x0E1C3577DA214C08 marked as ultimately trusted
gpg: revocation certificate stored as 'C:/Users/mosig_user/.gnupg/openpgp-revocs.d\B6C61937861267AB884515420E1C3577DA214C08.rev'
public and secret key created and signed.

pub rsa4096/0x0E1C3577DA214C08 2019-07-19 [C] [expires: 2020-07-19]
Key fingerprint = B6C6 1937 8612 67AB 8845 1542 0E1C 3577 DA21 4C08
uid John Doe <john.doe@online.host>


C:\Users\mosig_user>gpg --edit-key --expert 0x0E1C3577DA214C08
gpg (GnuPG) 2.2.11; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec rsa4096/0x0E1C3577DA214C08
created: 2019-07-19 expires: 2020-07-19 usage: C
trust: ultimate validity: ultimate
[ultimate] (1). John Doe <john.doe@online.host>

gpg> addkey
Please select what kind of key you want:
(3) DSA (sign only)
(4) RSA (sign only)
(5) Elgamal (encrypt only)
(6) RSA (encrypt only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(12) ECC (encrypt only)
(13) Existing key
Your selection? 8

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Sign Encrypt

(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished

Your selection? e

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Sign

(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished

Your selection? q
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 366
Key expires at 07/19/20 10:30:40 W. Europe Daylight Time
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec rsa4096/0x0E1C3577DA214C08
created: 2019-07-19 expires: 2020-07-19 usage: C
trust: ultimate validity: ultimate
ssb rsa4096/0xDDD1175EBF719B4B
created: 2019-07-19 expires: 2020-07-19 usage: S
[ultimate] (1). John Doe <john.doe@online.host>

gpg> quit
Save changes? (y/N) y

Der Unterschlüssel wird später in das Repository gelegt. Wir benutzen die Hauptschlüssel-Unterschlüssel-Mechanik, um den Unterschlüssel zurückrufen und neu generieren zu können, falls er kompromittiert werden sollte. Außerdem schränken wir so den Schaden einer möglichen Offenlegung ein.

Exportiere jetzt die Schlüssel und lösche sie aus dem Schlüsselring:

Schlüssel in Dateien exportieren

C:\Users\mosig_user\>gpg --export-secret-key --armor -o 0E1C3577DA214C08.priv.asc 0E1C3577DA214C08
C:\Users\mosig_user\>gpg --export-secret-subkeys --armor -o 0E1C3577DA214C08.sub_priv.asc 0E1C3577DA214C08
C:\Users\mosig_user\>gpg --export --armor -o 0E1C3577DA214C08.pub.asc 0E1C3577DA214C08
C:\Users\mosig_user\>gpg --delete-secret-keys 0E1C3577DA214C08
C:\Users\mosig_user\>gpg --delete-keys 0E1C3577DA214C08

Den Schlüssel in das Repository legen

Bewahre den Hauptschlüssel an einem sicheren Ort auf. Wir werden im Folgenden nur den Unterschlüssel benutzen. Wenn du den obigen Schritten gefolgt bist, dann wird der Schlüssel nicht von einer Passphrase geschützt. Stattdessen werden wir die Datei symmetrisch verschlüsseln und während des Builds entschlüsseln. Dadurch wird der Umgang mit GPG in den Build-Skripten etwas einfacher. Die Unterschiede bezüglich der Sicherheit zwischen der Nutzung einer Passphrase oder der symmetrischen Verschlüsselung der Schlüsseldatei sind in diesem Fall aus meiner Sicht nicht allzu groß.

Zurück zum Thema. Folge einfach diesen Schritten, um den Schlüssel in das Repository zu legen:

  • Kopiere den öffentlichen Schlüssel von 0E1C3577DA214C08.pub.asc nach <REPO_ROOT>/.ci/pubring.auto
  • Verschlüssele die Privatschlüsseldatei:
    gpg --symmetric --cipher-algo AES256 --sign -output <REPO_ROOT>/.ci/secring.auto.gpg 0E1C3577DA214C08.sub_priv.asc
    • Wähle eine sichere Passphrase und hebe sie für den späteren Gebrauch auf. In unserem Fall fungiert sie als der symmetrische Schlüssel.
    • Unter Windows kann es sein, dass einige Sonderzeichen in der Passphrase nicht akzeptiert werden.
  • Übertrage alles per commit und push in das Repository.

Build-Skripte anpassen

Jetzt brauchen wir ein neues Skript, das den Schlüssel entschlüsselt und in unseren eigenen Schlüsselring legt, bevor der Build startet. Der Schlüsselring wird innerhalb des Build-Containers bei jedem Build erzeugt.

.ci/before_script.sh

#!/bin/bash

end_with_error()
{
echo "ERROR: ${1:-"Unknown Error"} Exiting." 1>&2
exit 1
}

declare -r custom_gpg_home="./.ci"
declare -r secring_auto="${custom_gpg_home}/secring.auto"
declare -r pubring_auto="${custom_gpg_home}/pubring.auto"

echo
echo "Decrypting secret gpg keyring.."
# $super_secret_password is taken from the script's env. See below in the blog.
{ echo $super_secret_password | gpg --passphrase-fd 0 "${secring_auto}".gpg ; } || { end_with_error "Failed to decrypt secret gpg keyring." ; }
echo Success!

echo
echo Importing keyrings..
{ gpg --home "${custom_gpg_home}" --import "${secring_auto}" ; } || { end_with_error "Could not import secret keyring into gpg." ; }
{ gpg --home "${custom_gpg_home}" --import "${pubring_auto}" ; } || { end_with_error "Could not import public keyring into gpg." ; }
echo Success!

Dieses Skript erzeugt einen neuen GPG-Schlüsselring im Verzeichnis .ci und importiert den Schlüssel darin. Der zum Schlüssel gehörende symmetrische Entschlüsselungscode, die „Passphrase“ in der GPG-Welt, wird mittels einer Pipe an GPG übergeben. Damit taucht er nicht in den Logs auf, wird nicht im Repository gespeichert und bleibt geheim.

Das Haupt-Build-Skript muss um einen GPG-Schritt erweitert werden:

.ci/script.sh

export release_gpg="${release}.sha512.gpg"

echo
echo "Signing hash file.."
# DA214C08 are the last 4 bytes of the key id 0E1C3577DA214C08
{ gpg --home ./.ci --clearsign -u DA214C08 -o "${release_gpg}" "${release_hash}" ; } || { end_with_error "Creating signature file failed." ; }
echo Success!

Das „Super Secret Password“ einrichten

Jetzt fragst du dich bestimmt: Wo kommt denn eigentlich das $super_secret_password her? Es handelt sich dabei um eine verschlüsselte Umgebungsvariable, die folgendermaßen in die Datei .travis.yml eingetragen werden muss:

travis encrypt super_secret_password=<passphrase>

Dieses Kommando verschlüsselt den Variablennamen und das Passwort mit dem öffentlichen Schlüssel, der zu jedem Travis-CI-Repository gehört. Der obige Befehl gibt eine verschlüsselte Datenmenge aus, die so in die .travis.yml eingetragen wird:

.travis.yml mit super_secret_password

# Mit dem „Super secret password“ wird der Signierschlüssel entschlüsselt.
env:
global:
secure: <OUTPUT_OF_COMMAND>

Der verschlüsselte Wert wird automatisch entschlüsselt, wenn der Build-Container läuft. Er ist nur verschlüsselt, um eine Offenlegung an Klone des Repositorys zu verhindern.

Download-Authentizität prüfen

Wer die Authentizität der heruntergeladenen Datei überprüfen will, geht folgendermaßen vor:

  1. Public-Key des Signierschlüssels besorgen
    Du musst Nutzer mit dem Public-Key des Signierschlüssels versorgen. Dazu kannst du ihn auf einen Schlüsselserver hochladen:
    gpg --keyserver hkps://hkps.pool.sks-keyservers.net --send-key 0E1C3577DA214C08

    Der Nutzer kann den Schlüssel dann so herunterladen:

    gpg --keyserver hkps://hkps.pool.sks-keyservers.net --recv-key 0E1C3577DA214C08

    Eine andere Möglichkeit wäre es, den Schlüssel an einer öffentlich erreichbaren Stelle abzuspeichern, z. B. einem GitHub Gist.

    Eine Public-Key-Datei wird so importiert:

    gpg --import <KEY_FILE_NAME>
  2. Überprüfen der heruntergeladenen Hash-Datei
    Wenn alles bereit ist, kann man so die Authentizität verifizieren:

    C:\Users\mosig_user\gpg --verify file.sha512.gpg
    gpg: Signature made 07/03/19 18:28:13 W. Europe Daylight Time
    gpg: using RSA key D7AB286D68ABF9BB0EA4E7C75B7CC3CB8C21B4BB
    gpg: Good signature from "Jan Mosig <jan.mosig@itemis.de>" [ultimate]
    gpg: aka "Jan Mosig <mosig@itemis.de>" [ultimate]
    Primary key fingerprint: 6AA5 1DE4 2448 71E8 6C62 3672 37F0 7809 07AB EF78
    Subkey fingerprint: D7AB 286D 68AB F9BB 0EA4 E7C7 5B7C C3CB 8C21 B4BB

Zusammenfassung

Vielen Dank fürs Lesen. Jetzt hast du einen Build, der:

  • die Build-Artefakte zippt,
  • eine Prüfsumme für die ZIP-Datei berechnet,
  • die Prüfsumme signiert,
  • ein Release ergibt, das auf Integrität und Authentizität geprüft werden kann und dadurch sehr viel schwerer zu manipulieren ist.
   
Über Jan Mosig

Jan Mosig arbeitet für die itemis AG am Standort Leipzig. Er beschäftigt sich mit Problemen im Projektalltag und setzt zu deren Lösung auf technische Softwarequalität, Agile und Mut zur Veränderung.