> 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.
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.
```
-----------------------
# 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.
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);
}
_______________________________________________
Gcrypt-devel mailing list
[email protected]
https://lists.gnupg.org/mailman/listinfo/gcrypt-devel