Software Development

Schütze deine Travis CI-Releases - Teil 1: Prüfsumme mit SHA

Als ich mich zuletzt mit Travis CI beschäftigt habe, einem sehr populären CI-Tool für GitHub-Projekte, kam mir die Frage: Wie schütze ich meine fertigen Release-Artefakte vor Veränderung, z. B. Malware-Injection? Die Lösung lag für mich auf der Hand: Hashcodes mit SHA und Signaturen mit OpenPGP. Aber wie baut man das in einen Travis-CI-Build ein? Zum Signieren muss der private Schlüssel im Repository liegen. Wie aber kann man diesen geheim halten? Ich habe eine Lösung gefunden, die ich in einer kleinen Artikelserie zeigen möchte. Im ersten Teil soll es um die Sicherstellung der Release-Artefakt-Integrität mit SHA-512 gehen.

Ein Release mit dem Travis-GitHub-Release-Provider erstellen

In Yubiset, einer von mir veröffentlichten Sammlung von Shell-Skripten, die die OpenPGP-Schlüsselerzeugung und Yubikey-Manipulation leichter machen sollen, bedeutet „Release“: Alle Skripte in eine ZIP-Datei packen und diese Datei zu GitHub hochladen. Mit Travis CI ist das recht einfach. Wenn man den GitHub-Releases-Provider nutzt, braucht man lediglich folgende Datei in das Root-Verzeichnis eines GitHub-Repositorys zu speichern:

.travis.yml

# Make sure build does only run if a tag is pushed, not if a branch is pushed.
if: NOT tag IS blank
 
# A minimal Linux environment with general tools, not tailored to one specific language.
language: minimal
 
notifications:
  email:
    on_success: never
    on_failure: always
 
# Use Trusty Linux distribution
matrix:
  include:
  - os: linux
    dist: trusty
 
# Skip installation phase. We do not need any dependencies to be installed.
install: true
 
# Zip stuff and provide ZIP file's name in $release_zip.
# This is also where a possible software build, e. g., with mvn, could go into.
script:
    - . ./.ci/script.sh
 
# skip-cleanup: true -> Prevent deletion of created artifacts after install and before deployment
# overwrite: true -> Allow overwriting "old" tags (a.k.a. releases) with the same name.
# prerelease: true -> Mark release as prerelease. Adjust to your needs.
# repo: GitHubAccountName/RepoName
# tags: true -> deploy only on tagged builds
deploy:
  - provider: releases
    api_key:
      secure: $GITHUB_API_KEY
    file:
    - $release_zip
    skip-cleanup: true
    overwrite: true
    draft: false
    prerelease: true
    on:
     repo: JanMosigItemis/yubiset
     tags: true

.ci/script.sh

#!/bin/bash
 
# Prints error message and exit with error code 1.
# Reroutes stderr to stdout.
# Arg 1: Error message - If empty, default message will be used.
end_with_error()
{
    echo "ERROR: ${1:-"Unknown Error"} Exiting." 1>&2
    exit 1
}
 
export TZ=Europe/Berlin
export TIMESTAMP=`(date +'%Y%m%d%H%M')`

# see https://graysonkoonce.com/getting-the-current-branch-name-during-a-pull-request-in-travis-ci/
export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
 
# Guard against usage scenarios where no tag is available, e. g. build of a pushed branch.
if [[ -z "${TRAVIS_TAG}" ]] ; then
    TRAVIS_TAG="no_tag"
fi
 
export release="release_${TRAVIS_TAG}.${TIMESTAMP}"
export release_zip="${release}.zip"
 
echo
echo "Creating ${release_zip}.."
# -r.. Walk through directories (recursive).
# -T.. Test ZIP file integrity at end of process.
# -9.. Use best compression (smallest file size).
# -x.. Ignore
{ zip -rT9 -x".git/*" -x"*.gitignore" -x"*.travis.yml" -x".ci/*" "${release_zip}" * ; } || { end_with_error "Creating ZIP file failed." ; }
echo "Success!"

Wie gelangt man an das $GITHUB_API_TOKEN?

Damit Travis aber überhaupt etwas in dein GitHub-Repository hochladen kann, wird ein $GITHUB_API_KEY benötigt. Dabei handelt es sich um eine Travis-CI-Umgebungsvariable. Sie enthält ein OAuth-Token, mit dem Dritte, wie Travis, auf dein Repository zugreifen können. Dieses Token kannst du folgendermaßen generieren:

      • Installiere die Travis CI CLI (ein Ruby-Gem).
      • Wechsle in den lokalen Clone deines Repositorys.
      • Führe aus: travis setup releases
      • Dadurch wird höchstwahrscheinlich die Datei .travis.yml folgendermaßen abgeändert:

        .travis.yml

        deploy:
          provider: releases
          api_key:
            secure:
        # copy this value
        P9Nvrvch87YjHFOVuKruK9x3oCeGPs7BmLAJllZfw9yMNpoXo3lAQ+EfhrXa4b9v20Ju9516nE7YnloW8kGM77+AVZJ6Xolyo3qwrY1zq7HTC2yN6Hju37PTzPAg0gfsPvAyFSgADMfIR6J9+ocBZP6MTWkwqsWXIPEUNCDKdJKSVeBzM0vrAVV4afNxxejo+u7/DWB3IXYtIKXARDG8lHkxPY+IhL/bsNmurrEziXa/h14eJtG99Yb5IoKlUniH2YlkXs0xgNPLMj0Ilitox/kipLFQhY2oWRzeHmzy3Rf6EomulUc+NOzP6jrMjo+GjQNHOJN14EeCUaTvB4rct0sCIwvbM1ayG/hAuOXFeW+wvZBClt3ri6nDnNCZJbqTuPbgRXDUa6YO37X7Z8XQCewwn+vrHmM5PM6DCHLk7zczEeJPux1PAokYvIuu1C6+0Ao8jiNsQkRjAe8RXzXRJYzim9axlJ5J3x0BJWkwh4x+0ROAgmnPuYritG3ncfRIJL7AkCwqjrU5oz9Maw2gc19V6HhGY8kvYhMafukYbDJf2pA5N7kFopLRCzqBUlwJ6mmx0YO6nPgt6vXt4qzAGn+dFkGDZkBnAoEvu7LYKGkI3IKoWE8QqlIb6hsuYe/gzhT7YqUDAZ+F0YyWkNklWs4rsgDRxzyizhbhUzKPlUw=
          file: $release_zip
          on:
            repo: JanMosigItemis/yubiset
          
      • Kopiere den Wert von secure und erstelle eine neue Umgebungsvariable mit Namen $GITHUB_API_KEY in den Einstellungen deines Travis-Builds. Diese sind unter https://travis-ci.org/GitHubName/RepoName/settings zu erreichen:
        Image_1_Environment_Variables
      • Stelle den Ursprungszustand der Datei .travis.yml mittels git revert wieder her.
      • Dadurch wird ein Zugangstoken in deinem GitHub-Account erstellt (siehe https://github.com/settings/tokens), verschlüsselt und in die Travis-Umgebungsvariable $GITHUB_API_KEY gespeichert. Diese kann aus .travis.yml heraus nur während des Builds gelesen werden.

 

Ein erstes Test-Release

Wenn alles bereit ist, speichere alles und erstelle einen Commit, aber warte noch mit dem Push. Damit Travis alles tatsächlich ausführt, müssen wir noch einen Git-Tag erstellen:

  • git tag -a -m"Tag message"
  • git push origin --follow-tags

Das ist alles, was zum Starten eines Builds und den damit verbundenen automatischen Release benötigt wird. Wir verwenden sogenannte „annotated tags“ (git tag -a), um wertvolle Metadaten, wie zum Beispiel Commit-Nachrichten, Committer-Namen etc., zusammen mit dem Tag zu speichern. Als Resultat erhalten wir ein neues Release in der Releases-Sektion unseres GitHub-Repositorys:

Imgage_2_Release

Release-Integrität sicherstellen mit SHA-512

Jetzt haben wir ein Release. Super! Nun müssen wir sicherstellen, dass Downloader überprüfen können, ob der Download „echt“ ist, das heißt die offiziell veröffentlichte ZIP-Datei und nicht eine, die defekt ist oder böswillig verändert wurde. Diese sogenannte Dateiintegrität kann mit Hilfe eines Hashcodes (Prüfsumme) überprüft werden. Es ist sehr schwer, eine manipulierte ZIP-Datei zu erstellen, die denselben Hashcode, wie die eigentliche ZIP-Datei liefert, vorausgesetzt, man verwendet eine sichere Hash-Funktion. Die Integrität kann man mit einem solchen Code folgendermaßen prüfen:

  • Der Anbieter (in unserem Fall der automatisierte Build) berechnet einen Hashcode für die ZIP-Datei.
  • Der Downloader berechnet den Hashcode nochmals auf seiner lokalen Maschine.
  • Originalcode und lokaler Code werden verglichen. Bei Gleichheit gilt: Die Dateiintegrität ist okay.

Unser Travis-Build läuft auf Linux, und das heißt, wir können das Tool sha512sum nutzen, das viele Distributionen als Teil der GNU-Core-Utilities enthalten. Das Tool ist auch auf Windows über die Git-Bash verfügbar. Es berechnet einen SHA-512-basierten Hashcode.

Erweitere die Datei .ci/script.sh folgendermaßen:

.ci/script.sh erweitert um Hash-Code-Berechnung:

export release_hash="${release}.sha512"
 
echo
echo "Creating ${release_hash}.."
{ sha512sum "${release_zip}" > "${release_hash}" ; } || { end_with_error "Creating hash file failed." ; }
echo Success!

Dadurch erstellt der Buildprozess eine Datei namens release_TAG.TIMESTAMP.sha512, die den folgenden Inhalt hat:

release_TAG.TIMESTAMP.sha512:

E1AAA728E002F462D74DD7E7F2D107AAAF6038C345F5E1F849B6DB8647A15C170F7CC2D65F45E602570A1752C4B0E0D4C9549639DDA8607960A40063AAB84748 *release_TAG.TIMESTAMP.zip

Die Datei enthält den Hashcode in hexadezimaler Form, sowie den Dateinamen, zu dem der Code gehört. In unserem Fall ist das die ZIP-Datei. Der Dateiname wird der neuen Umgebungsvariablen $release_hash zugewiesen. Diese muss jetzt folgendermaßen in die „deploy“-Sektion der Datei .travis.yml eingebunden werden:

„deploy“-Sektion der Datei .travis.yml mit Hashcode-Dateiname

deploy:
  - provider: releases
    api_key:
      secure: $GITHUB_API_KEY
    file:
    - $release_zip
    - $release_hash
    skip-cleanup: true
    overwrite: true
    draft: false
    prerelease: true
    on:
     repo: GitHubName/RepoName
     tags: true

Download-Integrität prüfen

Stell dir vor, du hast die ZIP-Datei und die Hashdatei in das selbe Verzeichnis heruntergeladen. In diesem Fall kannst du die Integrität der ZIP-Datei folgendermaßen prüfen:

Integrität der ZIP-Datei prüfen

# Die Datei, die in der SHA-512-Datei genannt wird (in diesem Fall release_TAG.TIMESTAMP.zip), muss von hier aus erreichbar sein.
# -c Integrität prüfen
sha512sum -c release_TAG.TIMESTAMP.sha512
release_TAG.TIMESTAMP.zip: OK # Erwartete Ausgabe

Natürlich ist es auch einfach möglich, den Hash der heruntergeladenen Datei erneut zu berechnen und beide Werte manuell zu vergleichen.

Für Windows ohne Git-Bash musst du ein Third-Party-Tool benutzen. Eine andere Möglichkeit ist das folgende Batch-Skript, das die PowerShell für die Berechnungen benutzt:

check_hash.bat

@ECHO OFF
SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
 
if [%1] == [] (
    echo ERROR: Please specify SHA512 file to use.
    exit /b 1
)
 
for /F "delims=* tokens=1" %%i in ('type %~1') do set remote_hash=%%i
for /F "delims=* tokens=2" %%i in ('type %~1') do set remote_zip=%%i
for /F %%i in ('powershell -command "(Get-FileHash -Path %remote_zip% -Algorithm SHA512).hash.ToLower()"') do set local_hash=%%i
 
if %local_hash% == %remote_hash% (
    echo %remote_zip%: OK
) else (
    echo %remote_zip%: FAILED
)
 
endlocal

Dieses Skript rufst du in der cmd.exe wie folgt auf:

Dateiintegrität auf Windows prüfen

check_hash release_TAG.TIMESTAMP.sha512
release_TAG.TIMESTAMP.zip: OK # Erwartete Ausgabe

Lass uns zusammenfassen

Okay, das war der erste Teil. Vielen Dank fürs Lesen. Du hast jetzt einen automatisierten Travis-CI-Build, der eine ZIP-Datei und die dazu passende Hashcode-Datei liefert, mit der du oder ein Downloader die Integrität der ZIP-Datei prüfen kann.

Im nächsten Teil der Serie werden wir einen Schritt weiter gehen und unser Release mit OpenPGP-Signaturen gegen Fälschungen des Hashcodes absichern.

   
Ü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.