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