I wanted to have a personal Certificate Authority (CA) to sign TLS certificates for my home network. Once the certificate for the the CA was installed in my browsers, I'd be able to securely trust my local services.
Since I have my GPG subkeys on a YubiKey, I wanted to re-use those private keys
for the CA root so that I didn't have to manage yet another master key. After
much fiddling, I figured out how to make
gpgsm do what I wanted.
Included in the GPG suite is a command called
gpgsm which works on X.509
certificates, like those used in TLS. It is able to access the private keys in
your main GPG keyring—including ones that are actually on a smartcard
like a YubiKey—but keeps certificates and their associated public keys in
a separate database since the formats involved are quite different.
There are several steps to the certificate signing flow:
- Generate a private key and certificate request (
- Sign the certificate (usually done by a third party CA, but we're the CA!)
- Import the certificate into the public keyring (
- Use/export the private key (
In our case, we'll be combining steps 1 and 2 by generating the certificate and signing it at the same time. However, even when doing this we still need to explicitly import the certificate into the keyring (step 3) to be able to access the generated private keys (step 4).
We'll be using
--batch mode which reads a parameter file and
does the signing flow with minimal further input. The parameter file format
is partially documented in GPG's manual, but there are some
additional features that are best found by reading its
gpgsm knows a lot about the X.509 format, it doesn't have first-class
support for every kind of certificate extension we may need for our purposes.
Thankfully, it has an escape hatch in the form of the
However, it requires us to encode the extensions manually. For example, the
Extension: 18.104.22.168 c 30060101ff020100 encodes a
constraints extension and can be understood as three
- the OID of the extension,
- whether the extension is critical or not (if critical, the client must fail validation if it doesn't understand the extension),
- and the ASN.1 value of the extension DER encoded in hex.
The clearest way I found to generate these values is a tiny bit of Python
pyasn1_modules libraries, for example:
from pyasn1.codec.der.encoder import encode from pyasn1_modules.rfc5280 import BasicConstraints from pyasn1_modules.rfc5280 import id_ce_basicConstraints bc = BasicConstraints() bc["cA"] = True bc["pathLenConstraint"] = 0 print(id_ce_basicConstraints, "c", encode(bc).hex())
which prints out the
22.214.171.124 c 30060101ff020100 we saw above.
Creating a CA certificate
The first thing we need to do is generate our root CA certificate. This certificate is what we will install in the trust roots of browsers etc. to allow secure access to the resources we sign with the CA's private key.
--generate-key command's name, we won't be generating a new
private key during this step. Instead, we tell it to use the signing key
[S]) already in our keychain by specifying its
keygrip which is
used to identify keys. To get the keygrip, run the following:
$ gpg --with-keygrip --list-keys pub rsa3072 2018-06-30 [C] [expires: 2021-05-02] 5B5A70814529DDD6F45995A9A1986BFD48E8FD1E Keygrip = E804A37B63A55D6C86E852F1CB7B2173EDF2A398 uid [ultimate] Neil Williams <email@example.com> uid [ultimate] Neil Williams <firstname.lastname@example.org> sub rsa4096 2019-05-02 [S] [expires: 2021-05-02] Keygrip = A2038400550F36E022A149FF6569993C517A591C sub rsa4096 2019-05-02 [A] [expires: 2021-05-02] Keygrip = 16C410601F49FFFCE22D369095B88440FAF5365B sub rsa4096 2019-05-02 [E] [expires: 2021-05-02] Keygrip = C38659A0687A0DF8458FF607B34F0A96EB4F5FC0
The signing key in this case has
A2038400… as its keygrip. With the
keygrip determined, we can make a parameters file for our new self-signed CA
# this must be the first line Key-Type: RSA # the keygrip of our extant private key Key-Grip: A2038400550F36E022A149FF6569993C517A591C # and sign the certificate immediately with the same key Signing-Key: A2038400550F36E022A149FF6569993C517A591C # using SHA-256 Hash-Algo: SHA256 # this is the name that will be shown for your certificate Name-DN: CN=spladug home network # creation and expiration Creation-Date: 2020-01-01 00:00 Expire-Date: 2021-01-01 00:00 # should increase when you renew. Serial: 1 # extensions! Authority-Key-Id: A2038400550F36E022A149FF6569993C517A591C Subject-Key-Id: A2038400550F36E022A149FF6569993C517A591C Extension: 126.96.36.199 c 30060101ff020100
Put that into a file named
ca.params and then generate the certificate with
$ gpgsm --armor --generate-key --batch ca.params > ca.crt gpgsm: about to sign the certificate for key: &A2038400550F36E022A149FF6569993C517A591C gpgsm: certificate created
The output should be a valid CA root certificate that you can import into
browsers and other trust roots. You can inspect it with
$ openssl x509 -text -in ca.crt Certificate: Data: Version: 3 (0x2) Serial Number: 1 (0x1) Signature Algorithm: sha256WithRSAEncryption Issuer: CN = spladug home network Validity Not Before: Jan 1 00:00:00 2020 GMT Not After : Jan 1 00:00:00 2021 GMT Subject: CN = spladug home network Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (4096 bit) Modulus: ... snip ... Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical CA:TRUE, pathlen:0 X509v3 Subject Key Identifier: A2:03:84:00:55:0F:36:E0:22:A1:49:FF:65:69:99:3C:51:7A:59:1C X509v3 Authority Key Identifier: keyid:A2:03:84:00:55:0F:36:E0:22:A1:49:FF:65:69:99:3C:51:... ... snip ...
Since we don't need to export the private key for the CA to send it elsewhere (and can't if it's on a smartcard) we're done here.
Making endpoint certificates
For each endpoint certificate, we'll go through roughly the same flow except that we'll be generating a new private key each time and instead of self-signing we'll sign with the CA's private key.
To start out, we'll make a new parameter file for the endpoint certificate:
# again, this must be the first line Key-Type: RSA # instead of Key-Grip we specify how large we want a newly generated # private key to be Key-Length: 2048 # then sign the certificate immediately with the CA key Signing-Key: A2038400550F36E022A149FF6569993C517A591C # using SHA-256 Hash-Algo: SHA256 # leave the creation date as "now" and set an expiration Expire-Date: 2021-01-01 00:00 # set the serial to a random value Serial: random # what purposes are we certifying this key good for Key-Usage: sign, encrypt # identify the CA certificate Issuer-DN: CN=spladug home network Authority-Key-Id: A2038400550F36E022A149FF6569993C517A591C # the name gets more important for endpoint certs Name-DN: CN=example.com # Name-DNS translates to subject alternative names Name-DNS: foo.example.com Name-DNS: bar.example.com # this is the basic constraints extension with CA=false Extension: 188.8.131.52 n 3000
Just like before, put this in a file like
endpoint.params and use it to
generate a key and signed certificate:
$ gpgsm --armor --generate-key --batch endpoint.params > endpoint.crt gpgsm: about to sign the certificate for key: &A2038400550F36E022A149FF6569993C517A591C gpgsm: certificate created
You will be prompted for a passphrase for protecting (encrypting) the private key. You can leave it blank to leave the key unencrypted if desired.
Like before, you can inspect the certificate with
$ openssl x509 -text -in endpoint.crt Certificate: Data: Version: 3 (0x2) Serial Number: 4291214621359795366 (0x3b8d74f6591ceca6) Signature Algorithm: sha256WithRSAEncryption Issuer: CN = spladug home network Validity Not Before: May 29 05:54:37 2020 GMT Not After : Jan 1 00:00:00 2021 GMT Subject: CN = example.com Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: ... snip ... Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Alternative Name: DNS:foo.example.com, DNS:bar.example.com X509v3 Basic Constraints: CA:FALSE X509v3 Authority Key Identifier: keyid:A2:03:84:00:55:0F:36:E0:22:A1:49:FF:65:69:99:3C:51:... X509v3 Key Usage: critical Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment ... snip ...
This time, we're not done. We need to extract the private key to use in the endpoint. First off, import the newly generated certificate:
$ gpgsm --import endpoint.crt gpgsm: total number processed: 1 gpgsm: imported: 1
Then get the ID of the key (not keygrip, but we'll use that later):
$ gpgsm --list-keys --with-keygrip /home/spladug/.gnupg/pubring.kbx -------------------------------------------- ID: 0x2FD3341C S/N: 01 Issuer: /CN=spladug home network Subject: /CN=spladug home network validity: 2020-01-01 00:00:00 through 2030-01-01 00:00:00 key type: 4096 bit RSA chain length: 0 fingerprint: E1:D0:9E:5E:D0:88:A3:78:AF:49:9D:9D:B0:F9:73:43:2F:D3:34:1C keygrip: A2038400550F36E022A149FF6569993C517A591C ID: 0xA0CBFE60 S/N: 3B8D74F6591CECA6 Issuer: /CN=spladug home network Subject: /CN=example.com aka: (dns-name foo.example.com) aka: (dns-name bar.example.com) validity: 2020-05-29 05:54:37 through 2021-01-01 00:00:00 key type: 2048 bit RSA key usage: digitalSignature nonRepudiation keyEncipherment dataEncipherment fingerprint: 23:11:53:73:83:48:0A:00:4C:C8:E1:A9:6D:08:26:82:A0:CB:FE:60 keygrip: 03AC0260D114811D59215DE7126D448E510C1186
In this example, there are two keys: the CA and the new endpoint certificate.
If you have more endpoints or are doing other things with
gpgsm there will
be more. The key we just made is
0xA0CBFE60 so that's the value we care
With the key ID figured out, we can export the private key:
$ gpgsm --armor --export-secret-key-raw 0xA0CBFE60 > endpoint.key
We now have a certificate (
endpoint.crt) and private key (
that can be installed in our endpoint.
As a final step, clean up the private key from our local keychain so the key
only exists on the endpoint going forward. To do this, manually delete the
private key file from the GPG private key directory using the keygrip listed
03AC0260D114811D59... in this case).
$ rm $GNUPGHOME/private-keys-v1.d/03AC0260D114811D59215DE7126D448E510C1186.key
And we're done!
Other reading and prior art
The following, in addition to things linked above, were both extremely useful to me in figuring out how all of this worked.