In 2012, when I created my GPG key, I was advised to create a signature subkey by the person helping me to set it up. It was therefore obvious to me that subkeys were part of the deal and quite evident to manage.

It was far later than I realized that it’s not the case at all.

So this article aims to give explanations about subkeys and how to manage them.

Important

This article will contain a lot of long pastes and while I think everything in it is relevant, I’ll also make a cheatsheet regarding the relevant commands in here. As soon as it’ll be done I’ll link it here, and add it to the openpgp series.

Foreword

Good practices

In this article, I took some shortcuts, eg, not defining expiration dates on the keys, not dropping “bad” hash algorithms, etc. This is intentional to keep the focus on the subkeys matter. I’ll definitely write a “good practice” article (that will be linked in this paragraph and in the series). Of course, as usual, anyone should do what they prefer, but it might be useful to read it anyway when it’s out.

GPG/OpenPGP

In the following, I’ll tend to generalize the description by referring to OpenPGP keys, packets, signatures, etc. The main reason is that GnuPG is merely an OpenPGP implementation, and what is stated here could apply to other implementations.

What is an OpenPGP key/signature/…?

One thing that might be useful to keep in mind during this whole OpenPGP track is that what’s actually mostly formalized is the OpenPGP message format (see RFC 4880). A key, a signature (on something) is mostly a sum of records, named packets. An exported public key is essentially a file containing a concatenation of these packets, each having an implication on what the implementation of OpenPGP one uses will see of the key after having read all these records.

As for an example, changing the expiration date of a key is practically putting a signature packet of type 0x13 (Positive certification of a User ID and Public Key) containing a signature subpacket of type sub 9 (key expiration time) containing the number of seconds after the key creation time that the key expires. (of course such a packet is only valid in a self-signature)

Following is an example from my own public key (exported in a file, and read via gpg --list-packets file.asc)

# off=567 ctb=89 tag=2 hlen=3 plen=595
:signature packet: algo 1, keyid 0D442664194974E2
        version 4, created 1616059800, md5len 0, sigclass 0x13
        digest algo 10, begin of digest d5 0b
        hashed subpkt 27 len 1 (key flags: 23)
        hashed subpkt 30 len 1 (features: 01)
        hashed subpkt 23 len 1 (keyserver preferences: 80)
        hashed subpkt 11 len 4 (pref-sym-algos: 9 8 7 3)
        hashed subpkt 21 len 4 (pref-hash-algos: 10 9 8 11)
        hashed subpkt 22 len 3 (pref-zip-algos: 2 3 1)
        hashed subpkt 33 len 21 (issuer fpr v4 9AE04D986400E3B67528F4930D442664194974E2)
        hashed subpkt 2 len 4 (sig created 2021-03-18)
        hashed subpkt 9 len 4 (key expires after 10y14d9h16m)
        subpkt 16 len 8 (issuer key ID 0D442664194974E2)
        data: [4095 bits]

This example is actually old, as a more recent self-signature has superseeded this one. Notice that a lot of subpackets are present here.

GPG Subkeys - What are they

Theoretically, a GPG subkey is a GPG key. And a GPG key is a subkey. Yep. Theres’s no real difference.

A GPG key is actually a GPG keypair, it’s a (private, public) couple represented generally by a fingerprint. Originally, in the early 90’s (yep, I was playing with dirt in my garden at this time), when PGP 2.6 was the norm, a PGP key had only one keypair.

The norm evolved, and now, a GPG key comes with as many keypairs as one wants, each one carrying flags (all currently encoded on 1 byte) specifying what it can do. Amongst the current bits, those are relevant to our matter:

  1. 0x01 - C/Certification - The key may be used to certify other keys, meaning it can put signature packet (packet tag 2) with certification signature type (0x10-0x13) on public keys;
  2. 0x02 - S/Signature - The key may be used to sign data;
  3. 0x04 - E/Encryption - The key may be used to encrypt communications;
  4. 0x20 - A/Authentication - The key may be used for authentication.

While most flags can be enabled and disabled either by creating subkeys (we’ll come to that later), or via the hidden interactive change-usage command, the Certification flag can’t. (also, note that some key generated with specific algorithm can’t wear certaing flags - eg, ECC keys use different algorithm variations for encryption keys)

In general in the remaining part of this article and others, I’ll assume that subkeys only bear one flag.

The first signing keypair is named the public key and bears the Certification flag. It is stored in a tag 6 (public key packet) packet. Other keypairs will all be qualified as subkeys and stored in tag 14 (public subkey packet) packet.

Last but not least, flags are not given to the key packet, but are given through signature packets attached to the primary key/subkey packets (the signature is calculated on the subkey), and the same goes for most attributes given to a key. The key packet itself only contain the cryptographic material and the key creation time.

And that’s pretty it.

Wait! How does an external entity knows a subkey is a subkey of the primary key?

True, without this bit, one would have troubles to give any significant trust to a subkey, because nothing would actually prove that it is, indeed, a subkey. One could create a collection of packets with one public key from a person they want to impersonate and a public subkey they actually own, and try to send these to a key server to trump others.

To solve that issue, a subkey is signed via a 0x18 (Subkey Binding Signature) signature packet, from the primary key, which is computed on both the primary key and the subkey.

A last problem needs to be tackled: a signing subkey generated by a third party could create a confusion by signing with a 0x18 signature packet a public primary key, in which case, this could complicate matters a lot.

So, for signing subkeys, when the primary key puts a 0x18 signing packet on the subkey, the signing packet must contain an embedded signature subpacket that contains a 0x19 signing packet made by the signing subkey on the primary key and the signing subkey.

As soon as a signing subkey has one signature of the kind, it theoretically can’t sign other keys. And that’s pretty it.

Old: Signature Packet(tag 2)(572 bytes)
        Ver 4 - new
        Sig type - Subkey Binding Signature(0x18).
        Pub alg - RSA Encrypt or Sign(pub 1)
        Hash alg - SHA512(hash 10)
        Hashed Sub: key flags(sub 27)(1 bytes)
                Flag - This key may be used to encrypt communications
                Flag - This key may be used to encrypt storage
        Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
         v4 -   Fingerprint - 9a e0 4d 98 64 00 e3 b6 75 28 f4 93 0d 44 26 64 19 49 74 e2
        Hashed Sub: signature creation time(sub 2)(4 bytes)
                Time - Wed Jan 17 14:04:57 CET 2024
        Hashed Sub: key expiration time(sub 9)(4 bytes)
                Time - Thu Jul 10 15:04:57 CEST 2025
        Sub: issuer key ID(sub 16)(8 bytes)
                Key ID - 0x0D442664194974E2
        Hash left 2 bytes - 8a a6
        RSA m^d mod n(4094 bits) - ...
                -> PKCS-1

A Subkey Binding Signature packet on a subkey without signature capability.

Old: Signature Packet(tag 2)(1138 bytes)
        Ver 4 - new
        Sig type - Subkey Binding Signature(0x18).
        Pub alg - RSA Encrypt or Sign(pub 1)
        Hash alg - SHA512(hash 10)
        Hashed Sub: key flags(sub 27)(1 bytes)
                Flag - This key may be used to sign data
        Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
         v4 -   Fingerprint - 9a e0 4d 98 64 00 e3 b6 75 28 f4 93 0d 44 26 64 19 49 74 e2
        Hashed Sub: signature creation time(sub 2)(4 bytes)
                Time - Wed Jan 17 14:05:04 CET 2024
        Hashed Sub: key expiration time(sub 9)(4 bytes)
                Time - Thu Jul 10 15:05:04 CEST 2025
        Sub: embedded signature(sub 32)(563 bytes)
        Ver 4 - new
        Sig type - Primary Key Binding Signature(0x19).
        Pub alg - RSA Encrypt or Sign(pub 1)
        Hash alg - SHA512(hash 10)
        Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
         v4 -   Fingerprint - e4 24 1e b6 1e ee 21 6e de 84 8c fc ee 21 5b 9f b8 c4 5b 0b
        Hashed Sub: signature creation time(sub 2)(4 bytes)
                Time - Fri Feb 25 14:10:17 CET 2022
        Sub: issuer key ID(sub 16)(8 bytes)
                Key ID - 0xEE215B9FB8C45B0B
        Hash left 2 bytes - 6e e1
        RSA m^d mod n(4094 bits) - ...
                -> PKCS-1
        Sub: issuer key ID(sub 16)(8 bytes)
                Key ID - 0x0D442664194974E2
        Hash left 2 bytes - 19 b6
        RSA m^d mod n(4096 bits) - ...
                -> PKCS-1

A Subkey Binding Signature packet on a subkey with signature capability.

How should I manage my subkeys?

By default, subkeys are considered less vital. Still, losing control on an encryption subkey means potential data loss, and losing control on an authentication subkey or on a signature subkey to an attacker means potential impersonation until that subkey is revoked.

The good aspect is that the whole trust built on user IDs and on the primary key won’t be lost in the process, although poor subkey management could lead third parties to place little to no trust on one’s cryptographic identity in general.

The rest depends on how you want to deal with it.

I could personally recommend to have the primary key stored on non-connected devices and to consider using it only when you’re at a place you deem safe enough. Subkeys on the contrary can lie on a laptop (preferably with encrypted disks of course), and be used for routine work. But as I stated, it’s your call.

In the following part, I’ll try to show how to generate a test key and subkeys on it to demonstrate how things are working. In the end I’ll also show how to create a setup where subkeys are available on a laptop without having the primary key available.

Hands on - Creating subkeys

GNUPGHOME

While it’s not mandatory, it can be relevant to work in a dedicated GnuPG home directory for this part. This has three main advantages:

  1. You won’t take the risk of breaking your real .gnupg directory;
  2. Wiping the directory clears anything, and therefore avoids any need to do meticulous cleaning;
  3. It might prove useful in other situations and is therefore a good habit to take.

There are two ways of doing so:

  1. Use the --homedir option of the gpg command every time. It’s a bit tedious, but it has the advantage of being explicit;
  2. export GNUPGHOME="/some/path/for/test" in your terminal. All future commands will use this directory, and therefore you don’t need to bother. As it’s not explicit, it can lead to troubles if you open a new terminal or try to do changes in a terminal with the overloaded environment.

In both cases, you need to create the directory before any of these methods works properly. Mind copying your configuration files if you have some, and, potentially, your public keyring. Take also care to set the proper ACLs on the directory (0700).

Tooling

One only needs gpg and pgpdump tools. In Debian, one can get them via

sudo apt install gpg pgpdump
  1. gpg is merely the binaries and tools to use GPG
  2. pgpdump is a way to inspect GPG packets in a more user-friendly way than what one can do with gpg --list-packets. It’s a convenience tool, that you can decide not to have.

Generating two primary keys

Let’s dive into primary key generation. Most people do this interactively, so let’s start this way. We’ll do a second one non-interactively so that we can do some cross-signing and see the packet magic operate.

❯ gpg --expert --full-generate-key
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
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
  (14) Existing key from card
Your selection?

The --full-generate-key option allows one to get the choice on the algorithms they want to use. The --expert mode allows access to all algorithms. Here to spare plenty time on the key generation, I want to use ECC algorithms. Also, these should no longer be in the expert mode, as they’re no widely supported and backward compatibility issues should not arise.

Note that if you still work on older systems, ECC might not be supported by GPG on these systems.

Note that for ECC I can either chose 9, which will automatically create a primary key and an encryption subkey, or I can chose 10 which will generate only the primary key with signing (and certification capabilities). 11 will also only generate a single primary key, but one will be able to toggle the Authentication flag if needed, and, more interestingly, toggle the Signature flag.

Let’s resume.

Your selection? 9
Please select which elliptic curve you want:
   (1) Curve 25519
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection? 1
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: PEB Test 1
Email address: peb@debian.org
Comment:
You selected this USER-ID:
    "PEB Test 1 <peb@debian.org>"

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.
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: revocation certificate stored as '/home/peb/.gnupg/openpgp-revocs.d/C03561D54A82BA395031954F7474FCA0C147A3BE.rev'
public and secret key created and signed.

pub   ed25519 2024-04-03 [SC]
      C03561D54A82BA395031954F7474FCA0C147A3BE
uid                      PEB Test 1 <peb@debian.org>
sub   cv25519 2024-04-03 [E]

Note: I’ll shred these keys afterwards, so I’m fine using my debian.org email. Even if I were not to shred these, Debian would not trust these keys, obviously.

A fast generation could also be done this way:

 gpg --quick-generate-key 'PEB Test 2 <peb@debian.org>' ed25519 cert never
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: revocation certificate stored as '/home/peb/.gnupg/openpgp-revocs.d/A0716A2F08BD3BF32B9893620997589F14E4717A.rev'
public and secret key created and signed.

pub   ed25519 2024-04-03 [C]
      A0716A2F08BD3BF32B9893620997589F14E4717A
uid                      PEB Test 2 <peb@debian.org>

Note

If you were to wish to be able to sign data with the primary key, the proper command would look like:

gpg --quick-generate-key 'PEB Test 2 <peb@debian.org>' ed25519 'cert sign' never

Inspecting the two keys and changing the capabilities of one

Now we have two keys, created through two different mechanisms, and therefore having different capabilities regarding the primary key.

Let’s see a pgpdump of the first key:

 gpg --armour --export C03561D54A82BA395031954F7474FCA0C147A3BE > pub1.asc
 ls
pub1.asc
 pgpdump pub1.asc
Old: Public Key Packet(tag 6)(51 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:14:05 CEST 2024
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Elliptic Curve - Ed25519 (0x2B 06 01 04 01 DA 47 0F 01)
    EdDSA Q(263 bits) - ...
Old: User ID Packet(tag 13)(27 bytes)
    User ID - PEB Test 1 <peb@debian.org>
Old: Signature Packet(tag 2)(144 bytes)
    Ver 4 - new
    Sig type - Positive certification of a User ID and Public Key packet(0x13).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - c0 35 61 d5 4a 82 ba 39 50 31 95 4f 74 74 fc a0 c1 47 a3 be 
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:14:05 CEST 2024
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to certify other keys
        Flag - This key may be used to sign data
    Hashed Sub: preferred symmetric algorithms(sub 11)(4 bytes)
        Sym alg - AES with 256-bit key(sym 9)
        Sym alg - AES with 192-bit key(sym 8)
        Sym alg - AES with 128-bit key(sym 7)
        Sym alg - Triple-DES(sym 2)
    Hashed Sub: preferred hash algorithms(sub 21)(5 bytes)
        Hash alg - SHA512(hash 10)
        Hash alg - SHA384(hash 9)
        Hash alg - SHA256(hash 8)
        Hash alg - SHA224(hash 11)
        Hash alg - SHA1(hash 2)
    Hashed Sub: preferred compression algorithms(sub 22)(3 bytes)
        Comp alg - ZLIB <RFC1950>(comp 2)
        Comp alg - BZip2(comp 3)
        Comp alg - ZIP <RFC1951>(comp 1)
    Hashed Sub: features(sub 30)(1 bytes)
        Flag - Modification detection (packets 18 and 19)
    Hashed Sub: key server preferences(sub 23)(1 bytes)
        Flag - No-modify
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x7474FCA0C147A3BE
    Hash left 2 bytes - b0 f1 
    EdDSA R(254 bits) - ...
    EdDSA s(254 bits) - ...
Old: Public Subkey Packet(tag 14)(56 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:14:05 CEST 2024
    Pub alg - ECDH Elliptic Curve Diffie-Hellman Algorithm(pub 18)
    Elliptic Curve - Curve25519 (0x2B 06 01 04 01 97 55 01 05 01)
    ECDH Q(263 bits) - ...
    ECDH KDF params(32 bits) - ...
        KDFhashID:      Hash alg - SHA256(hash 8)
        KDFsymAlgoID:   Sym alg - AES with 128-bit key(sym 7)
Old: Signature Packet(tag 2)(120 bytes)
    Ver 4 - new
    Sig type - Subkey Binding Signature(0x18).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - c0 35 61 d5 4a 82 ba 39 50 31 95 4f 74 74 fc a0 c1 47 a3 be 
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:14:05 CEST 2024
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to encrypt communications
        Flag - This key may be used to encrypt storage
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x7474FCA0C147A3BE
    Hash left 2 bytes - 30 98 
    EdDSA R(256 bits) - ...

The first key is created by generating a first keypair for the primary key with Signature and Certification capabilities, and a second keypair which will be an encryption subkey. This means that there are two packets. There’s also an UID packet for my PEB Test 1 <peb@debian.org> identity. Added to these, there is one self-sig from the primary key on itself defining many preferences and the capabilities of the key, and a Subkey Binding Signature from the primary key on the encryption subkey.

Compared to the second key, which only has three packets:

 gpg --armour --export A0716A2F08BD3BF32B9893620997589F14E4717A > pub2.asc
 pgpdump pub2.asc
Old: Public Key Packet(tag 6)(51 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:21:49 CEST 2024
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Elliptic Curve - Ed25519 (0x2B 06 01 04 01 DA 47 0F 01)
    EdDSA Q(263 bits) - ...
Old: User ID Packet(tag 13)(27 bytes)
    User ID - PEB Test 2 <peb@debian.org>
Old: Signature Packet(tag 2)(144 bytes)
    Ver 4 - new
    Sig type - Positive certification of a User ID and Public Key packet(0x13).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - a0 71 6a 2f 08 bd 3b f3 2b 98 93 62 09 97 58 9f 14 e4 71 7a
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:21:49 CEST 2024
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to certify other keys
    Hashed Sub: preferred symmetric algorithms(sub 11)(4 bytes)
        Sym alg - AES with 256-bit key(sym 9)
        Sym alg - AES with 192-bit key(sym 8)
        Sym alg - AES with 128-bit key(sym 7)
        Sym alg - Triple-DES(sym 2)
    Hashed Sub: preferred hash algorithms(sub 21)(5 bytes)
        Hash alg - SHA512(hash 10)
        Hash alg - SHA384(hash 9)
        Hash alg - SHA256(hash 8)
        Hash alg - SHA224(hash 11)
        Hash alg - SHA1(hash 2)
    Hashed Sub: preferred compression algorithms(sub 22)(3 bytes)
        Comp alg - ZLIB <RFC1950>(comp 2)
        Comp alg - BZip2(comp 3)
        Comp alg - ZIP <RFC1951>(comp 1)
    Hashed Sub: features(sub 30)(1 bytes)
        Flag - Modification detection (packets 18 and 19)
    Hashed Sub: key server preferences(sub 23)(1 bytes)
        Flag - No-modify
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x0997589F14E4717A
    Hash left 2 bytes - f0 3b
    EdDSA R(252 bits) - ...
    EdDSA s(256 bits) - ...

Now, let’s show how the hidden command change-usage works, by removing the Signature capability on primary key 1.

 gpg --expert --edit-key C03561D54A82BA395031954F7474FCA0C147A3BE
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
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  ed25519/7474FCA0C147A3BE
     created: 2024-04-03  expires: never       usage: SC
     trust: ultimate      validity: ultimate
ssb  cv25519/BDAB020AF4CCE19D
     created: 2024-04-03  expires: never       usage: E
[ultimate] (1). PEB Test 1 <peb@debian.org>

gpg> change-usage
Changing usage of the primary key.

Possible actions for a ECDSA/EdDSA key: Sign Certify Authenticate
Current allowed actions: Sign Certify

   (S) Toggle the sign capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? S

Possible actions for a ECDSA/EdDSA key: Sign Certify Authenticate
Current allowed actions: Certify

   (S) Toggle the sign capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? Q

sec  ed25519/7474FCA0C147A3BE
     created: 2024-04-03  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  cv25519/BDAB020AF4CCE19D
     created: 2024-04-03  expires: never       usage: E
[ultimate] (1). PEB Test 1 <peb@debian.org>

gpg> save

Note the save call, otherwise the packet will not be saved in the pubring and these changes will be lost. We can see that the primary key lost the Signing capability.

Now let’s inspect the key 1 again:

 gpg --armour --export C03561D54A82BA395031954F7474FCA0C147A3BE > pub1.asc
 pgpdump pub1.asc
Old: Public Key Packet(tag 6)(51 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:14:05 CEST 2024
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Elliptic Curve - Ed25519 (0x2B 06 01 04 01 DA 47 0F 01)
    EdDSA Q(263 bits) - ...
Old: User ID Packet(tag 13)(27 bytes)
    User ID - PEB Test 1 <peb@debian.org>
Old: Signature Packet(tag 2)(144 bytes)
    Ver 4 - new
    Sig type - Positive certification of a User ID and Public Key packet(0x13).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: preferred symmetric algorithms(sub 11)(4 bytes)
        Sym alg - AES with 256-bit key(sym 9)
        Sym alg - AES with 192-bit key(sym 8)
        Sym alg - AES with 128-bit key(sym 7)
        Sym alg - Triple-DES(sym 2)
    Hashed Sub: preferred hash algorithms(sub 21)(5 bytes)
        Hash alg - SHA512(hash 10)
        Hash alg - SHA384(hash 9)
        Hash alg - SHA256(hash 8)
        Hash alg - SHA224(hash 11)
        Hash alg - SHA1(hash 2)
    Hashed Sub: preferred compression algorithms(sub 22)(3 bytes)
        Comp alg - ZLIB <RFC1950>(comp 2)
        Comp alg - BZip2(comp 3)
        Comp alg - ZIP <RFC1951>(comp 1)
    Hashed Sub: features(sub 30)(1 bytes)
        Flag - Modification detection (packets 18 and 19)
    Hashed Sub: key server preferences(sub 23)(1 bytes)
        Flag - No-modify
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - c0 35 61 d5 4a 82 ba 39 50 31 95 4f 74 74 fc a0 c1 47 a3 be
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:37:40 CEST 2024
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to certify other keys
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x7474FCA0C147A3BE
    Hash left 2 bytes - 88 c4
    EdDSA R(256 bits) - ...
    EdDSA s(255 bits) - ...
Old: Public Subkey Packet(tag 14)(56 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:14:05 CEST 2024
    Pub alg - ECDH Elliptic Curve Diffie-Hellman Algorithm(pub 18)
    Elliptic Curve - Curve25519 (0x2B 06 01 04 01 97 55 01 05 01)
    ECDH Q(263 bits) - ...
    ECDH KDF params(32 bits) - ...
        KDFhashID:      Hash alg - SHA256(hash 8)
        KDFsymAlgoID:   Sym alg - AES with 128-bit key(sym 7)
Old: Signature Packet(tag 2)(120 bytes)
    Ver 4 - new
    Sig type - Subkey Binding Signature(0x18).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - c0 35 61 d5 4a 82 ba 39 50 31 95 4f 74 74 fc a0 c1 47 a3 be
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:14:05 CEST 2024
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to encrypt communications
        Flag - This key may be used to encrypt storage
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x7474FCA0C147A3BE
    Hash left 2 bytes - 30 98
    EdDSA R(256 bits) - ...
    EdDSA s(254 bits) - ...

We notice that the signature packet only mention Certification capability. It seems, though, that there are still 5 packets, and that the previous one was deleted. It’s actually kind of the case. In general, self-signature packets from a specific key don’t matter if there’s a more recent one signed from the same key. So any local change can induce the dropping of some packets.

On the opposite side, OpenPGP keyservers do not support packet deletion, and are merge-only. Therefore, all packets one sent there will be forever. This means that a long-lived public keys will have multiple signatures from the same key over time regardless opf the previous paragraph’s statement, and when one fetches the public key from a server, they’ll get all of them.

Signing key 1 with key 2

Let’s do a last thing before working a bit with subkeys : let’s sign key 1 with key 2. When one has more than one secret key, it’s relevant to use the --default-key option to help GPG understanding with what key the following actions will occur. By default, when editing a key without mentioning the default-key parameter GPG will use as the acting key the edited key if the secret key is available, or the first key listed by gpg -K otherwise.

 gpg --expert --default-key A0716A2F08BD3BF32B9893620997589F14E4717A --edit-key C03561D54A82BA395031954F7474FCA0C147A3BE
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
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  ed25519/7474FCA0C147A3BE
     created: 2024-04-03  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  cv25519/BDAB020AF4CCE19D
     created: 2024-04-03  expires: never       usage: E
[ultimate] (1). PEB Test 1 <peb@debian.org>

gpg> sign
gpg: using "A0716A2F08BD3BF32B9893620997589F14E4717A" as default secret key for signing

sec  ed25519/7474FCA0C147A3BE
     created: 2024-04-03  expires: never       usage: C
     trust: ultimate      validity: ultimate
 Primary key fingerprint: C035 61D5 4A82 BA39 5031  954F 7474 FCA0 C147 A3BE

     PEB Test 1 <peb@debian.org>

Are you sure that you want to sign this key with your
key "PEB Test 2 <peb@debian.org>" (0997589F14E4717A)

Really sign? (y/N) y

gpg> save

A pgpdump shows the updated key, with a new signature packet of type 0x10 (Generic certification of a User ID and Public Key packet).

 gpg --armour --export C03561D54A82BA395031954F7474FCA0C147A3BE > pub1.asc
 pgpdump pub1.asc
Old: Public Key Packet(tag 6)(51 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:14:05 CEST 2024
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Elliptic Curve - Ed25519 (0x2B 06 01 04 01 DA 47 0F 01)
    EdDSA Q(263 bits) - ...
Old: User ID Packet(tag 13)(27 bytes)
    User ID - PEB Test 1 <peb@debian.org>
Old: Signature Packet(tag 2)(144 bytes)
    Ver 4 - new
    Sig type - Positive certification of a User ID and Public Key packet(0x13).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: preferred symmetric algorithms(sub 11)(4 bytes)
        Sym alg - AES with 256-bit key(sym 9)
        Sym alg - AES with 192-bit key(sym 8)
        Sym alg - AES with 128-bit key(sym 7)
        Sym alg - Triple-DES(sym 2)
    Hashed Sub: preferred hash algorithms(sub 21)(5 bytes)
        Hash alg - SHA512(hash 10)
        Hash alg - SHA384(hash 9)
        Hash alg - SHA256(hash 8)
        Hash alg - SHA224(hash 11)
        Hash alg - SHA1(hash 2)
    Hashed Sub: preferred compression algorithms(sub 22)(3 bytes)
        Comp alg - ZLIB <RFC1950>(comp 2)
        Comp alg - BZip2(comp 3)
        Comp alg - ZIP <RFC1951>(comp 1)
    Hashed Sub: features(sub 30)(1 bytes)
        Flag - Modification detection (packets 18 and 19)
    Hashed Sub: key server preferences(sub 23)(1 bytes)
        Flag - No-modify
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to certify other keys
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - c0 35 61 d5 4a 82 ba 39 50 31 95 4f 74 74 fc a0 c1 47 a3 be
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:48:00 CEST 2024
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x7474FCA0C147A3BE
    Hash left 2 bytes - 90 35
    EdDSA R(256 bits) - ...
    EdDSA s(256 bits) - ...
Old: Signature Packet(tag 2)(117 bytes)
    Ver 4 - new
    Sig type - Generic certification of a User ID and Public Key packet(0x10).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - a0 71 6a 2f 08 bd 3b f3 2b 98 93 62 09 97 58 9f 14 e4 71 7a
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:59:00 CEST 2024
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x0997589F14E4717A
    Hash left 2 bytes - 92 a4
    EdDSA R(250 bits) - ...
    EdDSA s(256 bits) - ...
Old: Signature Packet(tag 2)(117 bytes)
    Ver 4 - new
    Sig type - Generic certification of a User ID and Public Key packet(0x10).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - a0 71 6a 2f 08 bd 3b f3 2b 98 93 62 09 97 58 9f 14 e4 71 7a
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 18:02:04 CEST 2024
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x0997589F14E4717A
    Hash left 2 bytes - 9d 64
    EdDSA R(254 bits) - ...
    EdDSA s(256 bits) - ...
Old: Public Subkey Packet(tag 14)(56 bytes)
    Ver 4 - new
    Public key creation time - Wed Apr  3 17:14:05 CEST 2024
    Pub alg - ECDH Elliptic Curve Diffie-Hellman Algorithm(pub 18)
    Elliptic Curve - Curve25519 (0x2B 06 01 04 01 97 55 01 05 01)
    ECDH Q(263 bits) - ...
    ECDH KDF params(32 bits) - ...
        KDFhashID:      Hash alg - SHA256(hash 8)
        KDFsymAlgoID:   Sym alg - AES with 128-bit key(sym 7)
Old: Signature Packet(tag 2)(120 bytes)
    Ver 4 - new
    Sig type - Subkey Binding Signature(0x18).
    Pub alg - EdDSA Edwards-curve Digital Signature Algorithm(pub 22)
    Hash alg - SHA256(hash 8)
    Hashed Sub: issuer fingerprint(sub 33)(21 bytes)
     v4 -   Fingerprint - c0 35 61 d5 4a 82 ba 39 50 31 95 4f 74 74 fc a0 c1 47 a3 be
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Wed Apr  3 17:14:05 CEST 2024
    Hashed Sub: key flags(sub 27)(1 bytes)
        Flag - This key may be used to encrypt communications
        Flag - This key may be used to encrypt storage
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x7474FCA0C147A3BE
    Hash left 2 bytes - 30 98
    EdDSA R(256 bits) - ...
    EdDSA s(254 bits) - ...

Actually if you follow, you might see that there are two packets. It’s because I did sign twice, out of curiosity about what would happen in terms of packet “merging”. One can see that non-self signatures are not squashed.

Subkeys management

For the remaining part, let’s work a bit with subkeys.

Encryption subkey

As key 1 already has an encryption subkey, let’s make one on key 2. It’s “trivially” done by the addkey command in interactive edition:

 gpg --expert --edit-key A0716A2F08BD3BF32B9893620997589F14E4717A
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
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  ed25519/0997589F14E4717A
     created: 2024-04-03  expires: never       usage: C
     trust: ultimate      validity: ultimate
[ultimate] (1). PEB Test 2 <peb@debian.org>

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
  (14) Existing key from card
Your selection? 12
Please select which elliptic curve you want:
   (1) Curve 25519
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection? 1
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)
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  ed25519/0997589F14E4717A
     created: 2024-04-03  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  cv25519/C8BF4DAF0F4F6F22
     created: 2024-04-03  expires: never       usage: E
[ultimate] (1). PEB Test 2 <peb@debian.org>

Note

One could also create such a subkey via GPGs --quick-add-key option: gpg --quick-add-key A0716A2F08BD3BF32B9893620997589F14E4717A cv25519 encr never

And that’s it!

From there, as soon as the encryption subkey will be published (or rather, only its public material), others will be able to send encrypted messages to you!

Signing subkey

Both key 1 or key 2 are now rather identical (except that key 2 signed key 1). They both can Certify other keys and receive encrypted data. But they both lack Signature capabilities or Authentication capabilities.

While we could add these capabilities through change-usage on the primary key, the idea here is to use subkeys.

Let’s, this time, use the quick way to add a signing subkey to key 1

 gpg --quick-add-key C03561D54A82BA395031954F7474FCA0C147A3BE ed25519 sign never
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 --list-keys C03561D54A82BA395031954F7474FCA0C147A3BE
pub   ed25519 2024-04-03 [C]
      C03561D54A82BA395031954F7474FCA0C147A3BE
uid           [ultimate] PEB Test 1 <peb@debian.org>
sub   cv25519 2024-04-03 [E]
sub   ed25519 2024-04-03 [S]

We see that a signing subkey is now here.

Let’s try to show the differences we now have between key 1 and key 2. First, let’s create a test.txt file with some content and sign it with key 1.

 echo "Test" > test.txt
 cat test.txt|gpg --clearsign --default-key C03561D54A82BA395031954F7474FCA0C147A3BE
gpg: using "C03561D54A82BA395031954F7474FCA0C147A3BE" as default secret key for signing
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Test
-----BEGIN PGP SIGNATURE-----

iHUEARYIAB0WIQS9L6Cqbi8wFtHsC25MM42gOz5uxgUCZg2B0wAKCRBMM42gOz5u
xlvtAQCxn8tZpohkNWZhg5pXoS5AbntoTFwF21Tuq0PRZWsd3AD+LyNr3LgmNp9X
E6u79cYDba104/q/T82+Eqk4q2QD5AU=
=1SAI
-----END PGP SIGNATURE-----

Everything went smoothly.

And now with key 2:

 cat test.txt|gpg --clearsign --default-key A0716A2F08BD3BF32B9893620997589F14E4717A
gpg: using "A0716A2F08BD3BF32B9893620997589F14E4717A" as default secret key for signing
gpg: no default secret key: Unusable secret key
gpg: [stdin]: clear-sign failed: Unusable secret key

It fails, because no key has the Signature capability in key 2.

Let’s finish with an Authentication subkey. While I’ll explain here how to generate one, I’ll leave to another article the usage of such a key with OpenSSH.

Authentication subkey

An authentication subkey can also be generated the same way as the others:

 gpg --list-keys C03561D54A82BA395031954F7474FCA0C147A3BE
pub   ed25519 2024-04-03 [C]
      C03561D54A82BA395031954F7474FCA0C147A3BE
uid           [ultimate] PEB Test 1 <peb@debian.org>
sub   cv25519 2024-04-03 [E]
sub   ed25519 2024-04-03 [S]
sub   ed25519 2024-04-03 [A]

Their usage will be described in another article that’ll link here when it’s out.

Isolating the primary key and keeping live only subkeys

Now that key 1 has all three subkey types, one can safely isolate the primary key via a rather simple mechanism:

  1. Export all the private key via the export-secret-keys option of GPG, and store it safely offline;
  2. Export only the private subkeys via the export-secret-subkeys option of GPG;
  3. Delete the whole private key from the keyring via the delete-secret-keys option of GPG;
  4. Reimport the subkeys from the exported file.

Let’s demonstrate this method.

First, export the whole private key:

 gpg --armour --export-secret-keys C03561D54A82BA395031954F7474FCA0C147A3BE > key1.priv.asc

Then, export the subkeys:

 gpg --armour --export-secret-subkeys C03561D54A82BA395031954F7474FCA0C147A3BE > key1.sub.priv.asc

Delete the whole private key from your active keyring:

 gpg --delete-secret-keys C03561D54A82BA395031954F7474FCA0C147A3BE
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.


sec  ed25519/7474FCA0C147A3BE 2024-04-03 PEB Test 1 <peb@debian.org>

Delete this key from the keyring? (y/N) y
This is a secret key! - really delete? (y/N) y

Check whether it worked:

❯ gpg --list-secret-keys C03561D54A82BA395031954F7474FCA0C147A3BE
gpg: error reading key: No secret key

Reimport the private subkeys:

 gpg --import key1.sub.priv.asc
gpg: key 7474FCA0C147A3BE: "PEB Test 1 <peb@debian.org>" not changed
gpg: To migrate 'secring.gpg', with each smartcard, run: gpg --card-status
gpg: key 7474FCA0C147A3BE: secret key imported
gpg: Total number processed: 1
gpg:              unchanged: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1

And check that it worked:

 gpg --list-secret-keys C03561D54A82BA395031954F7474FCA0C147A3BE
sec#  ed25519 2024-04-03 [C]
      C03561D54A82BA395031954F7474FCA0C147A3BE
uid           [ultimate] PEB Test 1 <peb@debian.org>
ssb   cv25519 2024-04-03 [E]
ssb   ed25519 2024-04-03 [S]
ssb   ed25519 2024-04-03 [A]

As we can see, the main key is referred to by a sec# entry. The # means that the private part is not accesible. This can be seen by trying to do any action that requires Certification capabilities:

 gpg --edit-key C03561D54A82BA395031954F7474FCA0C147A3BE
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret subkeys are available.

pub  ed25519/7474FCA0C147A3BE
     created: 2024-04-03  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  cv25519/BDAB020AF4CCE19D
     created: 2024-04-03  expires: never       usage: E
ssb  ed25519/4C338DA03B3E6EC6
     created: 2024-04-03  expires: never       usage: S
ssb  ed25519/60448FE27317DCE0
     created: 2024-04-03  expires: never       usage: A
[ultimate] (1). PEB Test 1 <peb@debian.org>

gpg> addkey
Need the secret key to do this.

As we can see, the mention in edit-key mode is “Secret subkeys are available.” in opposition to the classic “Secret key is available”. When trying to addkey, the GPG client tells that the secret key is required to do this.

Wrapup

In this article, we saw how the subkey system in OpenPGP is designed, and how to manage subkeys practically with GPG. It adds an extra layer of security as it allows one to get their primary key offline.

Share on: TwitterFacebookEmail



Published

Last Updated

openpgp

Category

security

Tags

Contact