Upgrading a certification signature from SHA1 to SHA256

A certification signature is a way to store the assertion that a binding between a User ID (like an email address) and a public key is authentic. The certification signature is added to the certificate containing the binding. This augmented certificate can then be published, allowing other users to see and consider this assertion.

The first certification signature is added to a key when it's generated. It selfsigns the key creating a binding between it's public key and it's initial User IDs. Revocation certificates also contain a certification signature for verifying the authenticity of the revocation.

The certification signature is created by hashing the public key and the User ID, and subsequently signing the resulting hash value. Ideally the hash value for the combination of public key and User ID should be unique. Hash values however are of limited size which implies an information loss – there exist other input values, which produce the same hash value. The consequence of these collisions is that a certification signature asserts authenticity of an unlimited amount of public key - User ID pairs. Obviously this is not intended.

One design criteria for a hash function used for the creation of certification signatures is that these collisions are next to impossible to construct making the abuse of a collision infeasible in practice.

SHA1 is one of such hash functions. Over the time careful research concluded that SHA1 was less robust against collision construction than initially thought. Combined with the expanding availability of computing power SHA1 became vulnerable, meaning the construction of a fitting public key – User ID pair for a hash value became a realistic scenario. The remedy is to replace a SHA1 hash with a hash calculated by a stronger hash function: SHA256.

Check for SHA1 signatures in certificates

We use sq cert lint to check for certificates that use SHA1 signatures. sq cert lint takes the certificate(s) to examine either a a file parameter or from stdin.

This example takes the certificates from gpg using a keyring which contains just two vulnerable keys.

$ gpg --export | sq cert lint
Certificate 27E8C659C86F1C42 is not valid under the standard policy: No binding signature at time 2024-04-18T09:26:21Z
Certificate 27E8C659C86F1C42 contains a User ID ("Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>") protected by SHA-1
Certificate 27E8C659C86F1C42, key 739247043F705BAD uses a SHA-1-protected binding signature.
Certificate 9C2437DF50A1F904 is not valid under the standard policy: No binding signature at time 2024-04-18T09:26:21Z
Certificate 9C2437DF50A1F904 contains a User ID ("Alice Example (GnuPG 1.4.18 Testkey) <alice@example.com>") protected by SHA-1
Certificate 9C2437DF50A1F904, key 6FBFA7A845431489 uses a SHA-1-protected binding signature.
Examined 4 certificates.
  0 certificates are invalid and were not linted. (GOOD)
  4 certificates were linted.
  2 of the 4 certificates (50%) have at least one issue. (BAD)
0 of the linted certificates were revoked.
  0 of the 0 certificates has revocation certificates that are weaker than the certificate and should be recreated. (GOOD)
0 of the linted certificates were expired.
4 of the non-revoked linted certificates have at least one non-revoked User ID:
  2 have at least one User ID protected by SHA-1. (BAD)
  2 have all User IDs protected by SHA-1. (BAD)
4 of the non-revoked linted certificates have at least one non-revoked, live subkey:
  2 have at least one non-revoked, live subkey with a binding signature that uses SHA-1. (BAD)
0 of the non-revoked linted certificates have at least one non-revoked, live, signing-capable subkey:
  0 certificates have at least one non-revoked, live, signing-capable subkey with a strong binding signature, but a backsig that uses SHA-1. (GOOD)

Adding --list-keys gives a shorter output, containing only the fingerprints of certificates with issues.

$ gpg --export | sq cert lint --list-keys
F39D47AD38E2FC90D86FB94B27E8C659C86F1C42
000D6166AEC5E2EDA801BC259C2437DF50A1F904
Examined 4 certificates.

Adding User IDs allows to constrain the certificates considered.

$ gpg --export bob@example.com | sq cert lint
Certificate 27E8C659C86F1C42 is not valid under the standard policy: No binding signature at time 2024-04-17T12:10:44Z
Certificate 27E8C659C86F1C42 contains a User ID ("Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>") protected by SHA-1
Certificate 27E8C659C86F1C42, key 739247043F705BAD uses a SHA-1-protected binding signature.
Examined 1 certificate.
  0 certificates are invalid and were not linted. (GOOD)
  1 certificate was linted.
  1 of the 1 certificates (100%) has at least one issue. (BAD)
0 of the linted certificates were revoked.
  0 of the 0 certificates has revocation certificates that are weaker than the certificate and should be recreated. (GOOD)
0 of the linted certificates were expired.
1 of the non-revoked linted certificate has at least one non-revoked User ID:
  1 has at least one User ID protected by SHA-1. (BAD)
  1 has all User IDs protected by SHA-1. (BAD)
1 of the non-revoked linted certificates has at least one non-revoked, live subkey:
  1 has at least one non-revoked, live subkey with a binding signature that uses SHA-1. (BAD)
0 of the non-revoked linted certificates have at least one non-revoked, live, signing-capable subkey:
  0 certificates have at least one non-revoked, live, signing-capable subkey with a strong binding signature, but a backsig that uses SHA-1. (GOOD)

To fix a certification signature a secret key is needed.

Exporting a "secret key" actually exports the keypair - the secret key material and the public certificate.

Passing --fix to sq cert lint causes sq to generate new certifications using SHA256 as hash function. The result is written to stdout and can be passed to gpg for import.

  $ gpg --export-secret-key bob@example.com | sq cert lint --fix | gpg --import
Certificate 27E8C659C86F1C42 is not valid under the standard policy: No binding signature at time 2024-04-17T12:20:50Z
Certificate 27E8C659C86F1C42 contains a User ID ("Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>") protected by SHA-1
Certificate 27E8C659C86F1C42, key 739247043F705BAD uses a SHA-1-protected binding signature.
Examined 1 certificate.
  0 certificates are invalid and were not linted. (GOOD)
  1 certificate was linted.
  1 of the 1 certificates (100%) has at least one issue. (BAD)
0 of the linted certificates were revoked.
  0 of the 0 certificates has revocation certificates that are weaker than the certificate and should be recreated. (GOOD)
0 of the linted certificates were expired.
1 of the non-revoked linted certificate has at least one non-revoked User ID:
  1 has at least one User ID protected by SHA-1. (BAD)
  1 has all User IDs protected by SHA-1. (BAD)
1 of the non-revoked linted certificates has at least one non-revoked, live subkey:
  1 has at least one non-revoked, live subkey with a binding signature that uses SHA-1. (BAD)
0 of the non-revoked linted certificates have at least one non-revoked, live, signing-capable subkey:
  0 certificates have at least one non-revoked, live, signing-capable subkey with a strong binding signature, but a backsig that uses SHA-1. (GOOD)
gpg: key 27E8C659C86F1C42: "Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>" 2 new signatures
gpg: Total number processed: 1
gpg:         new signatures: 2

The output looks similar to the output above. We now have a look at the cert again, expecting it to be linted:

$ gpg --export bob@example.com | sq cert lint

sq cert lint produces no output if no certification with issues is found.

For a closer look sq toolbox packet dump can be used.

$ gpg --export bob@example.com | sq toolbox packet dump
Public-Key Packet, old CTB, 269 bytes
    Version: 4
    Creation time: 2024-04-17 09:46:37 UTC
    Pk algo: RSA
    Pk size: 2048 bits
    Fingerprint: F39D47AD38E2FC90D86FB94B27E8C659C86F1C42
    KeyID: 27E8C659C86F1C42
  
User ID Packet, old CTB, 52 bytes
    Value: Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>
  
Signature Packet, old CTB, 318 bytes
    Version: 4
    Type: PositiveCertification
    Pk algo: RSA
    Hash algo: SHA1
    Hashed area:
      Signature creation time: 2024-04-17 09:46:37 UTC
      Key flags: CS
      Key expiration time: 11months 30days 3h 50m 24s
      Symmetric algo preferences: AES256, AES192, AES128, CAST5, TripleDES
      Hash preferences: SHA256, SHA1, SHA384, SHA512, SHA224
      Compression preferences: Zlib, BZip2, Zip
      Features: SEIPDv1
      Keyserver preferences: no modify
    Unhashed area:
      Issuer: 27E8C659C86F1C42
    Digest prefix: 23A9
    Level: 0 (signature over data)
  
Signature Packet, old CTB, 408 bytes
    Version: 4
    Type: PositiveCertification
    Pk algo: RSA
    Hash algo: SHA256
    Hashed area:
      Signature creation time: 2024-04-17 12:20:50 UTC (critical)
      Key expiration time: 11months 30days 3h 50m 24s
      Symmetric algo preferences: AES256, AES192, AES128
      Issuer: 27E8C659C86F1C42
      Notation: salt@notations.sequoia-pgp.org
        00000000  81 e7 68 6d aa e8 67 8e  68 f9 15 96 fa 79 f1 2a
        00000010  51 54 e4 9a b8 2a 82 4d  0d 47 88 72 e9 44 45 12
      Hash preferences: SHA256, SHA512
      Compression preferences: Zlib, BZip2, Zip
      Keyserver preferences: no modify
      Key flags: CS
      Features: SEIPDv1
      Issuer Fingerprint: F39D47AD38E2FC90D86FB94B27E8C659C86F1C42
    Digest prefix: 9A8C
    Level: 0 (signature over data)
  
Public-Subkey Packet, old CTB, 269 bytes
    Version: 4
    Creation time: 2024-04-17 09:46:37 UTC
    Pk algo: RSA
    Pk size: 2048 bits
    Fingerprint: 509105DD8BB39C3925F939BE739247043F705BAD
    KeyID: 739247043F705BAD
  
Signature Packet, old CTB, 293 bytes
    Version: 4
    Type: SubkeyBinding
    Pk algo: RSA
    Hash algo: SHA1
    Hashed area:
      Signature creation time: 2024-04-17 09:46:37 UTC
      Key flags: EtEr
      Key expiration time: 11months 30days 3h 50m 24s
    Unhashed area:
      Issuer: 27E8C659C86F1C42
    Digest prefix: 882A
    Level: 0 (signature over data)
  
Signature Packet, old CTB, 388 bytes
    Version: 4
    Type: SubkeyBinding
    Pk algo: RSA
    Hash algo: SHA256
    Hashed area:
      Signature creation time: 2024-04-17 12:20:50 UTC (critical)
      Key expiration time: 11months 30days 3h 50m 24s
      Issuer: 27E8C659C86F1C42
      Notation: salt@notations.sequoia-pgp.org
        00000000  b1 73 8c 9b 5c f2 0b d9  69 d7 cb 67 46 b1 ef bb
        00000010  c4 1c 88 23 98 36 06 50  a9 5d ca 35 26 b5 b8 a7
      Key flags: EtEr
      Issuer Fingerprint: F39D47AD38E2FC90D86FB94B27E8C659C86F1C42
    Digest prefix: 5381
    Level: 0 (signature over data)

Note that the old SHA1 certifications are still within the certificate.

Fixing a revocation certificate

First, lets look into a revocation certificate to examine the hash algorithm used. In this example bob.pgp.rev contains the revocation certificate. Use sq toolbox packet dump to have a closer look.

$ sq toolbox packet dump bob.pgp.rev
Signature Packet, old CTB, 302 bytes
    Version: 4
    Type: KeyRevocation
    Pk algo: RSA
    Hash algo: SHA1
    Hashed area:
      Signature creation time: 2024-04-18 09:48:29 UTC
      Reason for revocation: Key is retired and no longer used, Test revocation
    Unhashed area:
      Issuer: 27E8C659C86F1C42
    Digest prefix: 18DB
    Level: 0 (signature over data)

Hash algo tells the used algorithm, SHA1 in this case.

sq cert lint is not meant to fix this issue. Instead simply create a new revocation certificate.

Note: The generation of a revocation certificate only works with keys containing SHA256 certifications. See above how fix keys in case they are missing.

$ gpg --export-secret-key bob@example.com | sq key userid revoke "Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>" 'retired' 'Test revocation with sq' > bob.pgp.rev
$ sq inspect bob_new.pgp.rev
-: Revocation Certificate.

    Fingerprint: F39D47AD38E2FC90D86FB94B27E8C659C86F1C42
                 Invalid: No binding signature at time 2024-05-06T11:50:46Z
Public-key algo: RSA
Public-key size: 2048 bits
  Creation time: 2024-04-17 09:46:37 UTC

         UserID: Bob Example (GnuPG 1.4.18 Testkey) <bob@example.com>
                 Revoked:
                  - User ID information is no longer valid
                    On: 2024-05-06 11:50:46 UTC
                    Message: "Test revocation with sq"
                 Invalid: No binding signature at time 2024-05-06T11:50:46Z