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.
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:
# 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
#!/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!"
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:
travis setup releases
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
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:git revert
wieder her.$GITHUB_API_KEY
gespeichert. Diese kann aus .travis.yml heraus nur während des Builds gelesen werden.
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:
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:
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:
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:
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:
- 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
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:
# 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:
@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:
check_hash release_TAG.TIMESTAMP.sha512
release_TAG.TIMESTAMP.zip: OK # Erwartete Ausgabe
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.