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--