16.01.2021 03:25

FreeBSD pkg signing with an agent

Preamble: if you want the code and don't care for my ramblings here you go - http://git.sysphere.org/freebsd-pkgsign/

Coming from GNU/Linux where gpg-agent was available to facilitate key management when signing repositories or packages I missed that feature. FreeBSD however uses SSL not GPG. But those keys can be read by the ssh-agent and we can work with that. Recent SolarWinds supply chain attack is a good reminder to safeguard your software delivery pipeline.

If you announced PUBKEY repositories to your users or customers up until this point you would have to switch to FINGERPRINTS instead, in order to utilize the pkg-repo(8) support for an external signing_command.

The Python Paramiko library makes communication with an agent simple and it is readily available as the py37-paramiko package (or port) so I went with that. There was however a small setback (with RSA sign flags) but more about that at the bottom of the article. If you would prefer a simpler implementation of the agent protocol and to have a self sufficient tool I found sshovel to be pretty good (and confirmed signing is implemented well enough to work for this purpose). I didn't have time to strip out (now unnecessary) encryption code, and more importantly didn't have time to port sshovel to python3 (as python2 is deprecated in FreeBSD).

We are all used to digests of public keys serving as fingerprints and identifiers. However Paramiko derives fingerprints from the public key in the SSH format. For simplicity I decided to flow with it and reference keys by Paramiko fingerprints. The "--dump" argument is implemented as a helper in pkgsign to list Paramiko fingerprints of all keys found in ssh-agent. But before we dump fingerprints if your key(s) is on the file-system without a passphrase (which they really shouldn't be) it's time to put a passphrase on them now (and don't forget to shred the old ones). Here's a crash course on ssh-agent operation, and how to get pkgsign to connect to it:

$ ssh-agent -t 1200s >~/.ssh/ssh-agent.info
$ source ~/.ssh/ssh-agent.info

$ ssh-add myprivatekey.key.enc
  Enter passphrase: [PASSPHRASE]

$ ./pkgsign --dump
  INFO: found ssh-agent key [FINGERPRINT]
If you wanted to automate key loading through some associative array etc. It would be beneficial to rename your private key to match the fingerprint. But you don't have to. However for the public key it is expected (unless you change the default behavior). This is because converting the public key obtained directly from the agent to the PEM pkcs8 format (that pkg-repo(8) is expecting in return) would be more code than this entire thing. It is much simpler to just read the public key from the file-system and be done with it.
# ln -s /usr/local/etc/ssl/public/mypublickey.pub /usr/local/etc/ssl/public/[FINGERPRINT].pub
The ownership/permissions/chflags scheme on the encrypted private key and parent directories is up to you. Or plug it in on external media, or cryptokey, or scp it only when needed, or shutdown the signing server after signing... This is crucial. Agent availability is an improvement, but don't get complacent because of it.

When pkg-repo(8) is used with signing_command the data for signing is piped to the specified command. In addition to that pkgsign expects a fingerprint passed to it as an argument. Why all this messing around with fingerprints at all? Because the ability to use different keys for different repositories is important. Because it aids automation, and because you don't want your repository signed by some OpenSSH key by mistake. To explore some possibilities let's consider this simplified cog of an imaginary automated system:
#!/usr/bin/env bash

declare -A REPO_KEYS

REPO_KEYS['xfce']=FINGERPRINT11111111111111111111
REPO_KEYS['gnome']=FINGERPRINT22222222222222222222

# /path/to/repos/xfce/FreeBSD:12:amd64/
ARG=$1

SOFTWARE_DISTRIB="${ARG%/*/}"
SOFTWARE_DISTRIB="${SOFTWARE_DISTRIB##/*/}"
SOFTWARE_DISTRIB_KEY="${REPO_KEYS[$SOFTWARE_DISTRIB]}"

/usr/sbin/pkg repo $ARG signing_command: ssh signing-server /path/to/pkgsign ${SOFTWARE_DISTRIB_KEY}
How to bootstrap your users or convert existing ones to the new repository format is explained in the manual very well but let's go over it anyway. Since the command to generate the fingerprint may look intimidating to users you could instead opt to pregenerate it and host it along side the public key:
# mkdir -p /usr/local/etc/pkg/keys
# mkdir -p /usr/local/etc/pkg/fingerprints/YOURORG/trusted
# mkdir -p /usr/local/etc/pkg/fingerprints/YOURORG/revoked
# fetch -o "/usr/local/etc/pkg/keys/YOURORG.pub" https://www2.you.com./YOURORG.pub

# sh -c '( echo "function: sha256"; echo "fingerprint: $(sha256 -q /usr/local/etc/pkg/keys/YOURORG.pub)"; ) \
    >/usr/local/etc/pkg/fingerprints/YOURORG/trusted/fingerprint'

# emacs /usr/local/etc/pkg/repos/YOURORG.conf
  ...
  #signature_type: "PUBKEY",
  #pubkey: "/usr/local/etc/pkg/keys/YOURORG.pub",
  signature_type: "FINGERPRINTS",
  fingerprints: "/usr/local/etc/pkg/fingerprints/YOURORG",
  ...
If you want to evaluate pkgsign with OpenSSL pkeyutl first to confirm all of this is possible you can do so for example like this (but only after patching Paramiko as explained in the paragraph below this one):
$ echo -n "Hello" | \
   openssl dgst -sign myprivatekey.key.enc -sha256 -binary >signature-cmp

$ echo Hello | \
   ./pkgsign --debug [FINGERPRINT] >/dev/null

$ echo -n "Hello" | \
   openssl sha256 -binary | openssl pkeyutl -verify -sigfile signature-Hello \
   -pubin -inkey mypublickey.pub -pkeyopt digest:sha256

  Signature Verified Successfully
Now for the bad news. To make this project happen I had to patch Paramiko to add support for RSA sign flags. I submitted the patch upstream but haven't heard anything back yet. It would be nice of them to accept it, but if it takes a very long time then luckily the changes are very minor. It is trivial to keep moving it forward in a py37-paramiko port.
--- paramiko/agent.py  2021-01-15 23:03:50.387801224 +0100
+++ paramiko/agent.py  2021-01-15 23:04:34.667800388 +0100
@@ -407,12 +407,12 @@
     def get_name(self):
         return self.name
 
-    def sign_ssh_data(self, data):
+    def sign_ssh_data(self, data, flags=0):
         msg = Message()
         msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST)
         msg.add_string(self.blob)
         msg.add_string(data)
-        msg.add_int(0)
+        msg.add_int(flags)
         ptype, result = self.agent._send_message(msg)
         if ptype != SSH2_AGENT_SIGN_RESPONSE:
             raise SSHException("key cannot be used for signing")


Written by anrxc | Permalink | Filed under crypto, code