english, Software Development

Secure Your Travis CI Releases - Part 2: Signature with OpenPGP

In the first part of our series, I showed you how to generate a hash code file for your Travis CI release, thus making it possible for clients to check release integrity. In this part we are going to take things one step further by securing release authenticity with OpenPGP signatures, thus giving attackers, e.g., malware injectors, a hard time.

Assuring release authenticity with OpenPGP

There is one catch with the setup from the last article: If an attacker manages to tamper with the ZIP file she could most likely also tamper with the hash file at the same time, i.e., you wouldn't notice the hack. In order to prevent this kind of attack, it is common practice to sign either the ZIP file itself or the hash file. By doing so, it is possible to check whether the hash file really originates from your build and not from somewhere else. An attacker may forge the ZIP file and the hash-file, e.g., with traffic interception, but it is very unlikely that she is also able to forge the signature.

One of the most widespread techniques for data signing is OpenPGP. Lucky for us, Travis' minimal container provides the well-known GnuPG implementation of this standard.

The only problem is: We are going to need a private PGP key at build time in order to sign the hash file. This means, it must exist within the repository. Since the repository is public and thus a good target for attackers, it is not a good idea to use your personal private key for this task. The risk of exposure is simply too high. In addition we will also need to encrypt the key somehow, so that it cannot be compromised by users cloning your repository.

Generating the signing key pair

First of all, you need to generate a PGP key pair that will act as your signing key resp. as that of your repository. You need a master key and one sub key that has the ability to “sign” only. See my blog series on OpenPGP on details about generating master and sub keys, or follow the subsequent steps. Leave the passphrase empty, i.e., no passphrase. We are going to protect the key with a different method.

Generating master key and signing sub key

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

The sub key will later be put into the repository. We do use the master and sub key construction in order to be able to revoke the sub key and generate a new one in case it gets compromised. Also, by doing so, we are able to limit the potential damage resulting from an exposure.
Now export the keys and delete them from your keyring:

Exporting keys to files

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

Putting the key into the repository

Keep the master key somewhere hidden. We are going to use the sub key for signing only. If you followed the steps, the key will not be protected by a passphrase. This is because we are going to encrypt the key file and decrypt it during build only. This makes handling the GPG part in the build scripts a bit easier. The differences between using a passphrase or using an encrypted key file are not that huge. You may try and let me know how it turned out.

Anyway, follow these steps to put the keys into your repository:

  • Copy the file 0E1C3577DA214C08.pub.asc into <REPO_ROOT>/.ci/pubring.auto
  • Encrypt the private sub key file:
    gpg --symmetric --cipher-algo AES256 --sign -output <REPO_ROOT>/.ci/secring.auto.gpg 0E1C3577DA214C08.sub_priv.asc
    • Enter a secure passphrase and store it away, because we are going to need it soon. In our case, it will act as the symmetric key.
    • On Windows, some special characters may not be accepted in the passphrase.
  • Commit everything and push it to the repository.

Adjusting the build scripts

Now we need a new script that decrypts our key and puts it into a custom keyring before the build starts. This keyring will be created inside the build container on each build with this script:

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

This script creates a new GPG keyring inside the .ci directory and imports the keys into it. The key file’s enryption key (passphrase in terms of gpg) is passed to gpg via a pipe. Thus, it does not appear on the logs, will never be stored in the repository, and stays secret.

The main build script must be extended by a GPG step like this:

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

Setting Up The Super Secret Password

You may wonder where the $super_secret_password comes from. This is an encrypted environment variable that needs to be set up in the .travis.yml file like this:

travis encrypt super_secret_password=<passphrase>

This command encrypts the variable name and the password with the public key associated with each Travis CI repository. It outputs a blob of encrypted data which needs to go into the .travis.yml like this:

.travis.yml with super_secret_password

# Super secret password to decrypt the private signing key
env:
global:
secure: <OUTPUT_OF_COMMAND>

The encrypted value will be decrypted on the fly when the build container is run. It is only encrypted in order to prevent exposure to clones of the repository.

Checking download authenticity

If a client wants to verify the authenticity of the downloaded file, he must take the following steps:

  1. Getting the public key of your signing key

    You need to provide clients with the public key of your signing key. You can do this by uploading the key to a keyserver like this:

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

    A client may download the key like this:

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

    Another option is to store the key into a place that can be accessed by clients, e.g., a GitHub Gist.

    A client imports the public key file like this:

    gpg --import <KEY_FILE_NAME>
  2. Verifying the downloaded hash file
    Once everything is in place, verify the hash file like this:

    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

Summing it all up

That's it! Thank you very much for spending time here. You do now have a build that:

  • zips the build artifacts,
  • calculates a hash-file for the ZIP file,
  • signs the hash file with your repository’s signing key,
  • results in a release that may be checked for integrity and authenticity and thus is much more secure against forging by attackers.
   
About Jan Mosig

Jan Mosig works for itemis in Leipzig. He is focussed on solving problems in his daily project routine and relies on technical software quality, Agile and the courage to change.