GnuPG may downgrade digest algorithm to SHA1 during key signature checking

GnuPG may downgrade the message digest algorithm to insecure SHA1 algorithm during signature checking due to reading from uninitialized memory.

Impact

check_signature_over_key_or_uid can read from uninitialized memory which may lead to setting the message digest algorithm to SHA1. This reduces the security of User ID Certification Signatures to that of SHA1. SHA1 suffers from known cryptographic weaknesses like chosen prefix attacks.

This affects the following calls to check_signature_over_key_or_uid:

Details

Cryptographic signatures are not computed over the message itself, but over an aggregated cryptographic hash of the message. This hash is known as the digest (of the message). At the core of checking many kinds of signatures in GnuPG is the function check_signature_over_key_or_uid. For the aforementioned signature types a call to check_signature_over_key_or_uid with both the parameters is_selfsig and signer being NULL is possible. This can lead to the following:

/* Returns whether SIGNER generated the signature SIG over the packet
 * PACKET, which is a key, subkey or uid, and comes from the key block
 * KB.  (KB is PACKET's corresponding keyblock; we don't assume that
 * SIG has been added to the keyblock.)
 *
 * If SIGNER is set, then checks whether SIGNER generated the
 * signature.  Otherwise, uses SIG->KEYID to find the alleged signer.
 * This parameter can be used to effectively override the alleged
 * signer that is stored in SIG.
 *
 * KB may be NULL if SIGNER is set.
 *
 * Unlike check_key_signature, this function ignores any cached
 * results!  That is, it does not consider SIG->FLAGS.CHECKED and
 * SIG->FLAGS.VALID nor does it set them.
 *
 * This doesn't check the signature's semantic mean.  Concretely, it
 * doesn't check whether a non-self signed revocation signature was
 * created by a designated revoker.  In fact, it doesn't return an
 * error for a binding generated by a completely different key!
 *
 * Returns 0 if the signature is valid.  Returns GPG_ERR_SIG_CLASS if
 * this signature can't be over PACKET.  Returns GPG_ERR_NOT_FOUND if
 * the key that generated the signature (according to SIG) could not
 * be found.  Returns GPG_ERR_BAD_SIGNATURE if the signature is bad.
 * Other errors codes may be returned if something else goes wrong.
 *
 * IF IS_SELFSIG is not NULL, sets *IS_SELFSIG to 1 if this is a
 * self-signature (by the key's primary key) or 0 if not.
 *
 * If RET_PK is not NULL, returns a copy of the public key that
 * generated the signature (i.e., the signer) on success.  This must
 * be released by the caller using release_public_key_parts ().  */
gpg_error_t check_signature_over_key_or_uid(ctrl_t ctrl, PKT_public_key* signer,
                                            PKT_signature* sig, KBNODE kb, PACKET* packet,
                                            int* is_selfsig, PKT_public_key* ret_pk) {
  // ...
  int stub_is_selfsig; // <-- [1.1]

  if (!is_selfsig) is_selfsig = &stub_is_selfsig; // <-- [1.2]

  // ...

  if (signer) {
    // ...
  } else {
    /* Get the signer.  If possible, avoid a look up.  */
    if (sig->keyid[0] == pripk->keyid[0]
      && sig->keyid[1] == pripk->keyid[1]) {
      // ...
    } else { // <-- [2]
      /* See if one of the subkeys was the signer (although this
       * is extremely unlikely).  */
      kbnode_t ctx = NULL;
      kbnode_t n;

      while ((n = walk_kbnode(kb, &ctx, 0))) {
        PKT_public_key* subk;

        if (n->pkt->pkttype != PKT_PUBLIC_SUBKEY) continue;

        subk = n->pkt->pkt.public_key;
        if (sig->keyid[0] == subk->keyid[0]
          && sig->keyid[1] == subk->keyid[1]) {
          /* Issued by a subkey.  */
          signer = subk;
          break;
        }
      }

      if (!signer) {
        // ...
      }
    }
  }

  // ...
  
  if (IS_UID_SIG(sig) || IS_UID_REV(sig)) {
    log_assert(packet->pkttype == PKT_USER_ID);
    if (sig->digest_algo == DIGEST_ALGO_SHA1 && !*is_selfsig // <-- [3]
      && !opt.flags.allow_weak_key_signatures) {
      /* If the signature was created using SHA-1 we consider this
       * signature invalid because it makes it possible to mount a
       * chosen-prefix collision.  We don't do this for
       * self-signatures, though.  */
      print_sha1_keysig_rejected_note();
      rc = gpg_error(GPG_ERR_DIGEST_ALGO);
    } else {
      hash_public_key(md, pripk);
      hash_uid_packet(packet->pkt.user_id, md, sig);
      rc = check_signature_end_simple(signer, sig, md, NULL, 0);
    }
  }
  
  // ...
  
  return rc;
}

Since the issue is obvious from the code itself, we do not write a proof of concept for it.

Recommendations

  1. Always initialize memory before reading from it.
  2. Avoid code paths where variable initialization depends on complex control flow.
  3. Thoroughly test the program with memory sanitizer enabled
  4. Look through static analysis results that point out potential uses of uninitialized memory

Finder credits: 49016

Disclosure Timeline:

Upcoming Timeline:

Please note: While we might be able to offer some flexibility, our plan is to adhere to the above stated upcoming timeline, regardless of the availability of patches or fixes.

We kindly request allocation of a CVE number to track this issue. Please keep us updated regarding your remediation efforts.

Thank you

Best, Liam