Signing releases with a GPG project key
Intro
This article presents a step-by-step guide for signing a project’s releases with GPG. It came from my own experiences adding GPG-signing support to vault-token-helper.
My initial thought was to create a signing sub-key from my own personal GPG key and then I came across this much better idea from a StackExchange post:
As an alternative, you might want to consider creating a project key which you sign with your own key. This might have the advantage that other contributors/users could also sign the key (and thus certify that this indeed is the key used for the project), and handing over the project might be easier in case somebody else will take over maintenance.
Great idea! I’ll explain how to create a “project key” in this post.
Why sign releases? The Apache Foundation has an guide rationale already so I’ll just borrow their rationale:
A signature allows anyone to verify that a file is identical to the one created by the Apache release manager. Using a signature:
users can make sure that what they received has not been modified in any way, either accidentally via a faulty transmission channel, or intentionally (with or without malicious intent)
the Apache infrastructure team can verify the identity of a file
OpenPGP signatures confer the usual advantages of digital signatures: authentication, integrity and non-repudiation. MD5 and SHA checksums only provide the integrity part as they are not encrypted
If your project happens to be lucky enough to use goreleaser you can benefit from its built-in support for GPG signing. If you don’t use goreleaser it is still fairly straightforward to sign releases.
Creating the project key
First we will create a new GPG master key. After that we will create a sub-key that will be used for signing releases.
Master key
This guide assumes you have already created a personal GPG key. This is referred to as an “author key” in this guide and will be used to sign the project key, verifying your relationship to the project.
List current secret keys (-K /--list-secret-keys
):
$ gpg --keyid-format long -K
/Users/joe/.gnupg/pubring.kbx
-----------------------------
sec rsa4096/ADC517A11EB0B309 2016-02-09 [SC]
Key fingerprint = CF5C 5827 E7BD 7059 7992 8EC4 ADC5 17A1 1EB0 B309
uid [ultimate] Joe Miller <[email protected]>
ssb rsa2048/3C7B936B8FC96571 2016-02-09 [E] [expires: 2024-02-07]
ssb rsa2048/B0FBB6EC7A273832 2016-02-09 [SA] [expires: 2024-02-07]
ssb rsa2048/4AE3F48180C87370 2019-07-12 [S]
Generate a new master GPG key. This will be the “project key”:
$ gpg --expert --full-generate-key
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? E
Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Sign Certify
(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
(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) 0
Key does not expire at all
Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: vault-token-helper
Email address: [email protected]
Comment: github.com/joemiller/vault-token-helper project key
You selected this USER-ID:
"vault-token-helper (github.com/joemiller/vault-token-helper project key) <[email protected]>"
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 37F9D1272278CD32 marked as ultimately trusted
public and secret key created and signed.
pub rsa4096 2019-07-13 [C]
5EF225507053ACC2728AA51C37F9D1272278CD32
uid vault-token-helper (github.com/joemiller/vault-token-helper project key) <[email protected]>
We now have a new master key with only the [C]
(certify) bit set. This is a type of signing key
that signs sub-keys. We will create a signing sub-key for signing the releases next and
sign it with the master key.
Export the keyid:
export KEYID=5EF225507053ACC2728AA51C37F9D1272278CD32
Signing sub-key
Now that we have established a new master key for the project we will create a sub-key that will be used for signing releases. By creating a sub-key for signing we are able to keep the master key safely offline. The sub-key can be deployed to your CI/CD pipeline. If it is compromised it can be revoked by the master key and replaced with a new sub-key.
Edit the master key:
gpg --expert --edit-key $KEYID
Create a signing key by selecting (4) RSA (sign only):
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? 4
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) 0
Key does not expire at all
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/37F9D1272278CD32
created: 2019-07-13 expires: never usage: C
trust: ultimate validity: ultimate
ssb rsa4096/6720A9FD78AC13F5
created: 2019-07-13 expires: never usage: S
[ultimate] (1). vault-token-helper (github.com/joemiller/vault-token-helper project key) <[email protected]>
Save and exit:
gpg> save
List secret keys. There are now two keys, our author key and a new project key:
$ gpg --keyid-format long -K
sec rsa4096/ADC517A11EB0B309 2016-02-09 [SC]
Key fingerprint = CF5C 5827 E7BD 7059 7992 8EC4 ADC5 17A1 1EB0 B309
uid [ultimate] Joe Miller <[email protected]>
ssb rsa2048/3C7B936B8FC96571 2016-02-09 [E] [expires: 2024-02-07]
ssb rsa2048/B0FBB6EC7A273832 2016-02-09 [SA] [expires: 2024-02-07]
ssb rsa2048/4AE3F48180C87370 2019-07-12 [S]
sec rsa4096/37F9D1272278CD32 2019-07-13 [C]
5EF225507053ACC2728AA51C37F9D1272278CD32
uid [ultimate] vault-token-helper (github.com/joemiller/vault-token-helper project key) <[email protected]>
ssb rsa4096/6720A9FD78AC13F5 2019-07-13 [S]
Sign the project key with your author key:
gpg --sign-key $KEYID
Publish the project key to the key server network. This allows others to easily download the project’s public key to verify releases:
gpg --send-key $KEYID
Signing releases
Now that we have created our project key and a sub-key for signing we will export the sub-key for use in our CI/CD system. Only the sub-key will be deployed to our CI/CD system while the master will be kept offline. If the sub-key is compromised we can use the master key to revoke it and create a new one.
List the master and sub-key ID’s:
$ gpg --keyid-format long -K $KEYID
sec rsa4096/37F9D1272278CD32 2019-07-13 [C]
Key fingerprint = 5EF2 2550 7053 ACC2 728A A51C 37F9 D127 2278 CD32
uid [ultimate] vault-token-helper (github.com/joemiller/vault-token-helper project key) <[email protected]>
ssb rsa4096/6720A9FD78AC13F5 2019-07-13 [S]
Export the sub-key ID:
export SUBKEY=6720A9FD78AC13F5
Export the sub-key - and ONLY the sub-key. The !
is important for exporting only the sub-key:
gpg --armor --export-secret-subkeys $SUBKEY\! >vault-token-helper.signing-key.gpg
Testing: Build and sign a local snapshot release
Add the following in .goreleaser.yaml
to enable signing of the checksum file. You could also
sign each artifact but signing the checksum file is sufficient:
# GPG signing
sign:
artifacts: checksum
Run the following to create a temporary GPG keyring directory and import the signing key:
GNUPGHOME="$PWD/releaser-gpg"
export GNUPGHOME
mkdir -p "$GNUPGHOME"
chmod 0700 "$GNUPGHOME"
cat vault-token-helper.signing-key.gpg | gpg --batch --allow-secret-key-import --import
Run goreleaser
in snapshot mode:
goreleaser --rm-dist --snapshot
Cleanup the temporary GPG key chain when finished:
rm -rf $GNUPGHOME
unset GNUPGHOME
CI/CD
Incorporating signing into your CI/CD pipeline will vary slighty depending on the CI system but the steps are generally similar to the test signing above.
Circle-CI
Store the signing sub-key base64 encoded as an environment variable and then restore it
before executing goreleaser
.
cat vault-token-helper.signing-key.gpg | base64 | pbcopy
Create an environment variable GPG_KEY
in your CI system using the base64 encoded copy of the signing key as the contents.
Make a release.sh
script, Makefile task, or add a step into your .circleci/config.yml
:
release:
docker:
- image: circleci/golang:1.11
steps:
- run:
name: Setup GPG signing key
command: |
GNUPGHOME="$PWD/releaser-gpg"
export GNUPGHOME
mkdir -p "$GNUPGHOME"
chmod 0700 "$GNUPGHOME"
echo "$GPG_KEY" \
| base64 --decode --ignore-garbage \
| gpg --batch --allow-secret-key-import --import
gpg --keyid-format LONG --list-secret-keys
- run: curl -sL https://git.io/goreleaser | bash -s -- --parallelism=2
Azure DevOps Pipelines
See my vault-token-helper repo for an example of intgrating signing in Azure DevOps Pipelines. This is a more complex example due to the project requiring a specialized cross-build environment but the overall mechanics are the same.
One major difference is that the base64 encoded GPG key is stored using Azure’s secure files mechanism because environment variables are limited to 4KB.
Offline the master key
This step is optional but it increases the security of the project master key. In this stage we will delete the master key from our keychain and leave only the signing sub-key. This will allow us to sign builds locally. You could also delete the entire master key and sub-key if you wish.
Make a backup of the master key and sub-key. This should be saved in a safe offline space such as an encrypted USB key.
gpg --armor --export-secret-keys $KEYID >vault-token-helper.gpg
Delete the master key from your keychain:
gpg --delete-secret-keys $KEYID
In the future you will need access to the master key such as to add new sub-keys, revoke existing sub-keys, etc. To do so import the master key from the backup:
gpg --import vault-token-helper.gpg