What do LLMs mean for GnuPG?

Robert J. Hansen rjh at sixdemonbag.org
Mon Mar 30 05:28:05 CEST 2026


I am not a GnuPG developer. I am, at best, some sort of semiofficial 
GnuPG mascot. Please don't mistake this for anything official. :)

I've heard a lot of people talking about the advent of LLM-assisted 
coding. There seem to be as many ways to approach this as there are 
individual developers. I've read a great number of whitepapers on this 
(and have talked with some genuine Ph.Ds in artificial intelligence: 
thanks, Dr. Ezra Sidran of Riverview AI) and recently had cause to put 
them to the test for a piece of code that needed to be, if not 
bulletproof, hardened. From this, we can hopefully learn some lessons 
about LLMs in security-sensitive code.

Werner and the rest of g10 Code can (and will!) do what they want, of 
course. I'm talking about my experiences only and what, based on those 
experiences, I imagine g10 Code would consider reasonable. If you have 
strong thoughts on the matter, here is the thread to comment on.

1. SO WHERE DO WE BEGIN?

I needed to change my UNIX password.

As I've done for the last quarter-century (yes, I'm old), I broke out 
Ted Ts'o's excellent pwgen tool. One invocation of 'pwgen -s 8 1' later 
and I had a new password. Bam, eight random characters, I'm done.

Then I got to thinking, you know, who's maintaining this package 
nowadays? When was the last time someone gave it an overhaul? How sturdy 
is it?

I wasn't able to quickly find the maintainer, nor a definitive code 
repo. Ted maintains a GitHub repo at https://github.com/tytso/pwgen but 
nothing's been touched in the last seven years, minimum.

pwgen is a good tool. It also deserves new eyes and maybe an overhaul. 
So I decided to use it as an excuse to teach myself Rust.

2. RUST?

Rust is emerging as an excellent general-purpose language for systems 
programming.

3. BUT I THOUGHT ...

If you're thinking "but Sequoia forked from GnuPG over Rust!", well, no, 
it didn't. The division happened because of interpersonal reasons, not 
some simplistic "this side only loves Rust and that side only loves C." 
I am absolutely convinced that Werner enthusiastically supports 
well-working FOSS software written in any language, even C++.

Let's put language choice aside and go forward to rpass. :)

4. RPASS

rpass (https://github.com/rjhansen/rpass) is a reimplementation of pwgen 
in Rust. There are a couple of minor differences: namely, pwgen's -s 
flag is now mandatory and strong guarantees are made about per-glyph 
entropy, what random number generator is used, and so on.

5. ORIGINAL IMPLEMENTATION

I firmly believe that as soon as code is usable for its chosen purpose 
and has no obvious defects, stamp it 1.0 and get it out there for the 
world to see. Every coder I know is afraid to do this because we all 
know the 1.0 codebase is utter crap.

My advice to all of us: get over it. Nobody cares if your 1.0 is crap. 
It's expected. Release and start getting better quickly.

I started by imagining a clean architecture: construct a configuration 
object from the command-line parameters, ensure it was internally 
self-consistent (no contradictory commands, etc.), and then use the 
configuration object to create a set of closures:

* one set of closures to securely generate high-entropy passwords,
* one set of closures to print them.

Once these closures are in place, the actual heart of the code becomes 
ridiculously simple. It's literally,

fn main() {
     let config = get_config_object();
     let (mut generate, mut finalize) = make_genfin_closures(&config);
     let write = make_writer(&config);
     for _ in 0..config.password_count() {
         write(generate());
     }
     finalize();
}

This isn't ... quite ... ideal. Since the generate and finalize closures 
share sensitive state (the buffer of random data) the two closures have 
to share access via Rust's equivalent of a pointer. It has a bad code 
smell. The closures themselves also became large and kind of ugly. I 
didn't like it.

But it worked! So, 1.0, here we go!

6. BRINGING IN CLAUDE

6a. EPISODE IV: CLAUDE.AI

The first thing I told Claude.ai was, "Reimplement pwgen in Rust, paying 
close attention to modernization issues."

Claude disappointed me... a lot. Although in many ways it faithfully 
performed the task, _how_ it performed the task was utterly incompetent. 
Instead of choosing a modern cryptographically secure pseudorandom 
number generator from one of the well-known packages on crates.io or the 
Rust standard library, it insisted on rolling its own 
*non-cryptographically secure* linear feedback shift register.

Not only did it roll its own LFSR, it rolled its own LFSR that already 
existed in the Rust standard library!

Likewise, when it came to pwgen's SHA-1 hash feature, Claude didn't 
notice that SHA-1 has been deprecated for pretty much every purpose and 
probably shouldn't be used in any new security-aware code as of 2026. 
Nope, it blithely went ahead and implemented the feature, and even 
rolled its own SHA-1 implementation instead of using Rust's built-in SHA-1.

When determining the column width ('pwgen -C' gives columns of 
passwords), it insisted on checking for the existence of a COLUMNS 
environment variable and defaulting to 80 if it didn't exist. This is, 
to say the least, not the recommended way of discovering terminal width.

rpass has a minimalist core that has a little bit of architectural 
beauty to it. It's kind of like the Picasso drawing of a penguin: it's 
fun because it's so small.

https://www.pablopicasso.net/drawings/ -- look for "Penguin".

I would compare the Claude version to a Jackson Pollack, except that 
Jackson Pollack created art and Claude created a mess.

If I were teaching an undergraduate secure coding course and someone 
turned this in, they would get a very bad grade.

6b. EPISODE V: THE CLAUDE STRIKES BACK

So I burned that one to the ground and felt pretty good about myself. 
Clearly, vibe coding was every bit as awful as I'd feared.

But it deserved a second chance, so ...

"Claude, look at the codebase at this git repo and criticize it on 
security grounds. Pay particular attention to issues of memory safety 
and whether sensitive data is being wiped."

Ow. Ow. Ow. Ow. Ow.

Claude found bugs -- and not a small number of them. Claude found subtle 
ones, like "you're not zeroizing this anonymous temporary variable 
you're implicitly creating", and it also found embarrassing ones, like 
how my finalizer wasn't actually firing.

Yep.

How in the world can this code NOT manage to hit the finalizer?

fn main() {
     let config = get_config_object();
     let (mut generate, mut finalize) = make_genfin_closures(&config);
     let write = make_writer(&config);
     for _ in 0..config.password_count() {
         write(generate());
     }
     finalize();
}

Answer: if the code panics between "let write..." and "finalize()", the 
app will immediately do a controlled crash. The finalizer won't get hit. 
If I were to wrap the finalizer in an RAII block I could get that 
guarantee, but it's not in an RAII block, and...

Etc.

Now, I don't _think_ there's a code path in rpass by which a panic can 
occur. But that's not the same thing as saying I've formally proven 
there is no such code path.

Yow. I have to admit, Claude pointed out a couple of holes in my game. 
Part of this is undoubtedly due to my being new to Rust programming, but 
the bottom line remains: if I can make bugs like this, odds are good you 
can, too -- and Claude can potentially be a useful tool in helping to 
find them before they bite your users.

6c. EPISODE VI: THE RETURN OF THE HACKER

So, armed with this critique I went back to the code and did some 
significant re-work on it. The ultimate architecture changed somewhat, 
so that the fragile generator/finalizer closures with their bad code 
smell and difficult-to-anticipate panic behavior were done away with in 
favor of a lightweight RAII object, but on balance my original 
architecture endured:

fn main() {
     let mut pw = PasswordGenerator::new();
     let mut printer = make_printer();

     for _ in 0..get_count() {
         printer(pw.generate());
     }
}

The basic architecture is intact, there are significantly fewer points 
by which sensitive memory can be returned to the system in a non-zeroed 
state, and on balance the code is a lot stronger.

7. SO WHAT'S THE UPSHOT FOR GNUPG?

Well, based on my experience with Claude so far, here's what I suspect 
about the future of GnuPG development:

* At some point LLMs will be used as part of GnuPG development. Used 
wisely, they offer real gains.

* I am very much opposed to letting LLMs write even one line of code in 
GnuPG.

* If you have any strong feelings about whether GnuPG development should 
embrace LLMs, and if so then how it should embrace them, the time to 
speak up is now. Sooner or later, and I'm betting on sooner, GnuPG will 
need to decide its LLM strategy, and it would be best if we all had a 
discussion about them before the decision needed to be made.

Feel free to ask questions about my experiences with Claude. I'm happy 
to field them. Just remember, I'm not a GnuPG developer. I'm just a guy. :)

-------------- next part --------------
A non-text attachment was scrubbed...
Name: OpenPGP_signature.asc
Type: application/pgp-signature
Size: 236 bytes
Desc: OpenPGP digital signature
URL: <https://lists.gnupg.org/pipermail/gnupg-users/attachments/20260329/b24af97d/attachment-0001.sig>


More information about the Gnupg-users mailing list