Re: Account request + libgcrypt security finding

Bert van der Weerd via Gcrypt-devel <[email protected]> Tue, 14 Apr 2026 23:03:21 +0200
Newsgroups gmane.comp.encryption.gpg.libgcrypt.devel
Message-ID <gn6d4ocp4n2eoswf3ybnflskikjvtmulpa2djk6rfscpoiauyc@4n2vdjkdx3cm>
--rhhfaaamnscmtzhx
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: 8bit

> On Mon, Apr 13, 2026 at 05:12:09PM +0200, Werner Koch wrote:
> > On Sat, 11 Apr 2026 18:41, Bert van der Weerd said:
> > 
> > > The first patch is the first high severity issue: GCM silent zero-IV
> > > fallback — patch and demonstration program made by Claude Code.
> > 
> > Please explain the issue here.

Hi Werner, all,

This is an issue with the IV/nonce in the (Chacha20 stream cipher and) GCM encryption mode. The IV/nonce remains zero if set_iv() is not properly called before: _gcry_cipher_gcm_encrypt(), _gcry_cipher_gcm_decrypt(), _gcry_cipher_gcm_authenticate(). This is a known failure mode of these algorithmes, and the proper thing to do is return GPG_ERR_INV_STATE;
 
 

I attached some AI slob:
 
 * cipher-gcm-zero-iv-fallback.readme.md - The full report of the bug, we did some digging in the code and found fips related code, and a function that was marked todo.
 * cipher-gcm-zero-iv-fallback.patch - The actual patch. In many ways the clearest I guess. Handcrafted (yay).
 
 * cipher-gcm-zero-iv-fallback.demo.md - This is the run results of the gcm issue: one patched, one unpatched.



I'm not sending the cipher-gcm-zero-iv-fallback.demo.c file to the list, but I guess one can craft one conceptually. Will send it to [email protected] if you want?
I'm not ready with the Chacha20 patch yet, sorry. I do have two other patches, that I can also just mail to this list, one is a typo in a function name and another is a forgotten wipememory().
 
This AI stuff is something I want to talk about. there are new (unreleased) AI models, from Anthropic and OpenAI. They actually eh... 'hack the planet' so to speak:)
I just wanted to start off with the most pressing ones this generation of AI can already find.

I can create a markdown file of all the bugs, but rather not. Well to be honest this is a lot of extra issues probably for a (short) while and then it cools down.
 
Short version: I basically did a:
 
   for i in crypto/*.c;
   do
     foreach (fallacy in the Wikipedia 'fallacies' page) { check_the_c_file_for_it };
   done;
 
and a heap of markdown came out.

I distrust AI professionally in code like this, so I just wanted to do the most critical then, and it did actually find something here, if I'm correct.
I saw in the code that, yes, that Chacha20 thing might also be a positive. This patch is only about GCM. But I don't know what to do now, really.
 
There also was two other pressing issues, both harder for me to grasp. One is about a missing point in an ECC implementation. The other one is simply just not clear to me yet.


I hope this helps,
--Bert


P.S. Sorry about not signing this email. This is just an email for this list.


--rhhfaaamnscmtzhx
Content-Type: text/markdown; charset=utf-8
Content-Disposition: attachment;
	filename="cipher-gcm-zero-iv-fallback.demo.md"
Content-Transfer-Encoding: 8bit

The attached .c file was compiled with:

```
gcc -o cipher-gcm-zero-iv-fallback.demo cipher-gcm-zero-iv-fallback.demo.c -I $HOME/usr/crypto/include -L $HOME/usr/crypto/lib -lgcrypt -lgpg-error
```

before and after the patch.

-----------------------
```
=== Part 1: no setiv (vulnerability demonstration) ===

  Both sessions encrypted without error and without an IV.
  Both used an all-zero IV — same key + same IV = same keystream.

  ciphertext 1:        c69a31a7609ba69828b7073a36d2b75826a8ff93f8c16df079a372b19663f1f1
  ciphertext 2:        cc903fec7c97e5832eb81d7e3acfe3702badbc9bbac13ee27ba520bc8730b5d0

  ct1 XOR ct2 = pt1 XOR pt2 (keystream cancels):
  ct1^ct2:             0a0a0e4b1c0c431b060f1a440c1d54280d054308420053120206520d11534421

  (ct1^ct2) XOR msg1 recovers msg2:
  recovered msg2:      "Bob's confidential message here!"
  actual    msg2:      "Bob's confidential message here!"

  VULNERABILITY CONFIRMED: msg2 fully recovered from ciphertexts alone.

=== Part 2: correct usage — explicit distinct IVs ===

  Both sessions encrypted successfully with distinct IVs.

  ciphertext 1:        cd599fb47064f000822305c8aaa66eed1848ef18e3343f4f5914185a24512c8e
  ciphertext 2:        fb4a7e920f4c118f542fb422d663e1310b63be5ee84933e84dbaf76d5a432cc9

  ct1 XOR ct2 (should be unintelligible — different keystreams):
  ct1^ct2:             3613e1267f28e18fd60cb1ea7cc58fdc132b51460b7d0ca714aeef377e120047

  XOR of ciphertexts reveals nothing. Correct behaviour.
```
-----------------------
```
LD_LIBRARY_PATH=$HOME/usr/crypto/lib ./cipher-gcm-zero-iv-fallback.demo                                                                                                                                                                                19:41 
=== Part 1: no setiv (vulnerability demonstration) ===

  encrypt session 1: Invalid state — setiv was required, got hard error.
  This is the PATCHED behaviour. Vulnerability is not present.

=== Part 2: correct usage — explicit distinct IVs ===

  Both sessions encrypted successfully with distinct IVs.

  ciphertext 1:        cd599fb47064f000822305c8aaa66eed1848ef18e3343f4f5914185a24512c8e
  ciphertext 2:        fb4a7e920f4c118f542fb422d663e1310b63be5ee84933e84dbaf76d5a432cc9

  ct1 XOR ct2 (should be unintelligible — different keystreams):
  ct1^ct2:             3613e1267f28e18fd60cb1ea7cc58fdc132b51460b7d0ca714aeef377e120047

  XOR of ciphertexts reveals nothing. Correct behaviour.
```
-----------------------

--rhhfaaamnscmtzhx
Content-Type: text/markdown; charset=utf-8
Content-Disposition: attachment;
	filename="cipher-gcm-zero-iv-fallback.readme.md"
Content-Transfer-Encoding: 8bit

# GCM zero-IV silent fallback — security fix

**Affects:** `cipher/cipher-gcm.c`, `cipher/cipher.c`, `cipher/cipher-internal.h`
**Severity:** HIGH
**Reporter:** Bert van der Weerd `<[email protected]>`
**Patch attached:** `cipher-gcm-zero-iv-fallback.patch`

---

## The bug

When a caller performs a GCM encrypt, decrypt, or authenticate operation
without first calling `setiv`, libgcrypt silently substitutes an all-zero
IV via `_gcry_cipher_gcm_setiv_zero` and proceeds.

A zero IV is one of GCM's documented catastrophic failure modes. With a
fixed key and a zero IV, the GHASH authentication key H is directly
computable by any observer. Any two sessions sharing (key, zero-IV) produce
identical keystream, and XOR of their ciphertexts recovers both plaintexts.
There is no error, no warning visible to the caller, and no tag failure —
the library simply does the wrong thing quietly.

The affected call sites are `_gcry_cipher_gcm_encrypt`,
`_gcry_cipher_gcm_decrypt`, and `_gcry_cipher_gcm_authenticate`, all of
which check `c->marks.iv` and branch to `_gcry_cipher_gcm_setiv_zero` on
the missing-IV path.

---

## The fix

Replace the three `_gcry_cipher_gcm_setiv_zero` call sites with
`return GPG_ERR_INV_STATE`. A caller who does not supply an explicit IV
gets a hard error before any data is touched. No silent fallback, no
zero-IV state, no ambiguity.

---

## History and context of the surrounding code

While tracing the fix we found that `_gcry_cipher_gcm_setiv_zero` and its
associated flag `disallow_encryption_because_of_setiv_in_fips_mode` are
the remnants of an unfinished feature — and we want to be transparent
about what we removed and why.

The original design intent, as best we can reconstruct it, was correct and
well-motivated: in FIPS mode, SP 800-38D requires that GCM IVs be generated
by the library using an approved deterministic construction, not supplied
freely by the caller. The scaffolding for this was `_gcry_cipher_gcm_geniv`
in `cipher-gcm.c`, which calls `gcm_generate_iv`, hands the IV back to the
caller, and initialises the context — the right approach.

However, `_gcry_cipher_gcm_geniv` was never more than a design sketch.
It has been sitting behind `#if 0 && TODO` since it was written, and it
could never have compiled: it calls `gcm_generate_iv` and `is_nonce_ok_len`,
neither of which exists anywhere in the codebase. `IVLEN` is similarly
undefined outside the dead block. The structure of the function is sound —
validate inputs, generate IV, initialise context, hand IV back to caller,
wipe local buffer — but the actual IV generation logic was never written.

The FIPS enforcement mechanism — the `disallow_encryption_because_of_setiv_in_fips_mode`
flag — was wired up in anticipation of `geniv` being ready, but with `geniv`
absent the flag has no coherent purpose. In FIPS mode, `setiv` loaded the
caller's IV and then set the flag to disable encryption afterward; the only
escape from this catch-22 was the zero-IV fallback, which is the
vulnerability.

The flag is set in three places across two files (`cipher-gcm.c` and
`cipher.c`) and checked in two places (`_gcry_cipher_gcm_encrypt` and
`_gcry_cipher_gcm_get_tag`). With `setiv_zero` removed, every setter
of the flag is gone, leaving both checks permanently false — dead branches.
We have removed the flag, its struct member in `cipher-internal.h`, all
setters, and all checks as part of this patch.

We have also removed the `#if 0 && TODO` `geniv` block. It calls undefined
functions, references the now-removed flag, and its presence alongside dead
flag infrastructure would only confuse future readers. If `geniv` is
implemented to complete the FIPS IV policy, it should be written fresh
against the cleaned-up context, with `gcm_generate_iv` and friends defined
for real.

The net result of the patch is that GCM FIPS IV policy is currently an
acknowledged open issue: there is no approved IV generation path and no
enforcement of one. We think this is more honest than the previous state,
where the enforcement machinery existed but guarded a door leading nowhere,
and the zero-IV fallback was the de facto behavior for anyone who hit the
wrong path.

---

## Files changed

| File | Change |
|------|--------|
| `cipher/cipher-gcm.c` | Remove forward declaration and body of `_gcry_cipher_gcm_setiv_zero`; replace three call sites with `return GPG_ERR_INV_STATE`; remove `disallow` flag check in `_gcry_cipher_gcm_get_tag`; remove `#if 0 && TODO` `geniv` block |
| `cipher/cipher-internal.h` | Remove `disallow_encryption_because_of_setiv_in_fips_mode:1` bitfield from GCM context struct |
| `cipher/cipher.c` | Remove GCM-specific `disallow` flag logic from `_gcry_cipher_setiv`; function now dispatches directly to `c->mode_ops.setiv` |

---

## Testing

`make check` passes cleanly on a non-FIPS build. No existing test case
relied on the zero-IV fallback behavior. FIPS mode was not tested as the
`geniv` path was never available; the flag removal has no behavioral impact
on FIPS builds beyond making the previously-broken GCM state consistent
with the non-FIPS path.


---

Thank you for maintaining libgcrypt. This audit was conducted as part of
a broader post-Glasswing hardening review of the GnuPG stack. Further
findings will follow as separate reports.

--rhhfaaamnscmtzhx
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="cipher-gcm-zero-iv-fallback.patch"

diff --git a/cipher/cipher-gcm.c b/cipher/cipher-gcm.c
index a9c48551..9134d77b 100644
--- a/cipher/cipher-gcm.c
+++ b/cipher/cipher-gcm.c
@@ -30,8 +30,6 @@
 #include "./cipher-internal.h"
 
 
-static gcry_err_code_t _gcry_cipher_gcm_setiv_zero (gcry_cipher_hd_t c);
-
 /* Helper macro to force alignment to 16 or 64 bytes.  */
 #ifdef HAVE_GCC_ATTRIBUTE_ALIGNED
 # define ATTR_ALIGNED_64  __attribute__ ((aligned (64)))
@@ -979,9 +977,6 @@ _gcry_cipher_gcm_encrypt (gcry_cipher_hd_t c,
     return GPG_ERR_INV_STATE;
 
   if (!c->marks.iv)
-    _gcry_cipher_gcm_setiv_zero (c);
-
-  if (c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode)
     return GPG_ERR_INV_STATE;
 
   if (!c->u_mode.gcm.ghash_aad_finalized)
@@ -1019,7 +1014,7 @@ _gcry_cipher_gcm_decrypt (gcry_cipher_hd_t c,
     return GPG_ERR_INV_STATE;
 
   if (!c->marks.iv)
-    _gcry_cipher_gcm_setiv_zero (c);
+    return GPG_ERR_INV_STATE;
 
   if (!c->u_mode.gcm.ghash_aad_finalized)
     {
@@ -1054,7 +1049,7 @@ _gcry_cipher_gcm_authenticate (gcry_cipher_hd_t c,
     return GPG_ERR_INV_STATE;
 
   if (!c->marks.iv)
-    _gcry_cipher_gcm_setiv_zero (c);
+    return GPG_ERR_INV_STATE;
 
   gcm_bytecounter_add(c->u_mode.gcm.aadlen, aadbuflen);
   if (!gcm_check_aadlen_or_ivlen(c->u_mode.gcm.aadlen))
@@ -1161,55 +1156,6 @@ _gcry_cipher_gcm_setiv (gcry_cipher_hd_t c, const byte *iv, size_t ivlen)
   return _gcry_cipher_gcm_initiv (c, iv, ivlen);
 }
 
-static gcry_err_code_t
-_gcry_cipher_gcm_setiv_zero (gcry_cipher_hd_t c)
-{
-  static const unsigned char zerobuf[MAX_BLOCKSIZE];
-
-  c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode = 0;
-
-  if (fips_mode ())
-    {
-      /* Direct invocation of GCM setiv in FIPS mode disables encryption. */
-      c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode = 1;
-    }
-
-  return _gcry_cipher_gcm_setiv (c, zerobuf, GCRY_GCM_BLOCK_LEN);
-}
-
-
-#if 0 && TODO
-void
-_gcry_cipher_gcm_geniv (gcry_cipher_hd_t c,
-                        byte *ivout, size_t ivoutlen, const byte *nonce,
-                        size_t noncelen)
-{
-  /* nonce:    user provided part (might be null) */
-  /* noncelen: check if proper length (if nonce not null) */
-  /* ivout:    iv used to initialize gcm, output to user */
-  /* ivoutlen: check correct size */
-  byte iv[IVLEN];
-
-  if (!ivout)
-    return GPG_ERR_INV_ARG;
-  if (ivoutlen != IVLEN)
-    return GPG_ERR_INV_LENGTH;
-  if (nonce != NULL && !is_nonce_ok_len(noncelen))
-    return GPG_ERR_INV_ARG;
-
-  gcm_generate_iv(iv, nonce, noncelen);
-
-  c->marks.iv = 0;
-  c->marks.tag = 0;
-  c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode = 0;
-
-  _gcry_cipher_gcm_initiv (c, iv, IVLEN);
-
-  buf_cpy(ivout, iv, IVLEN);
-  wipememory(iv, sizeof(iv));
-}
-#endif
-
 
 static int
 is_tag_length_valid(size_t taglen)
@@ -1302,9 +1248,6 @@ _gcry_cipher_gcm_get_tag (gcry_cipher_hd_t c, unsigned char *outtag,
                           size_t taglen)
 {
   /* Outputting authentication tag is part of encryption. */
-  if (c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode)
-    return GPG_ERR_INV_STATE;
-
   return _gcry_cipher_gcm_tag (c, outtag, taglen, 0);
 }
 
diff --git a/cipher/cipher-internal.h b/cipher/cipher-internal.h
index dc4878bb..7bbf4837 100644
--- a/cipher/cipher-internal.h
+++ b/cipher/cipher-internal.h
@@ -375,7 +375,6 @@ struct gcry_cipher_handle
       unsigned int ghash_aad_finalized:1;
 
       unsigned int datalen_over_limits:1;
-      unsigned int disallow_encryption_because_of_setiv_in_fips_mode:1;
 
       /* --- Following members are not cleared in gcry_cipher_reset --- */
 
diff --git a/cipher/cipher.c b/cipher/cipher.c
index fc130907..a5d59cb3 100644
--- a/cipher/cipher.c
+++ b/cipher/cipher.c
@@ -1328,17 +1328,6 @@ _gcry_cipher_setkey (gcry_cipher_hd_t hd, const void *key, size_t keylen)
 gcry_err_code_t
 _gcry_cipher_setiv (gcry_cipher_hd_t c, const void *iv, size_t ivlen)
 {
-  if (c->mode == GCRY_CIPHER_MODE_GCM)
-    {
-      c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode = 0;
-
-      if (fips_mode ())
-        {
-          /* Direct invocation of GCM setiv in FIPS mode disables encryption. */
-          c->u_mode.gcm.disallow_encryption_because_of_setiv_in_fips_mode = 1;
-        }
-    }
-
   return c->mode_ops.setiv (c, iv, ivlen);
 }
 

--rhhfaaamnscmtzhx
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

_______________________________________________
Gcrypt-devel mailing list
[email protected]
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel

--rhhfaaamnscmtzhx--