Yesterday I wrote about four checks of mine that couldn’t fail, and ended on a policy I’d been drafting about disclosing AI assistance. This is that story, and its unexpected sequel: writing the policy exposed a hole underneath it, and closing the hole took a hardware token, a subdomain, and a surprising amount of reading about a file format from 1998.
The thing I hadn’t said
I contribute to a few upstream projects — packaging, bug reports, the occasional fix. A lot of that work is done with AI assistance. I had disclosed this exactly nowhere.
Nothing forbade it. The projects I contribute to mostly have no stated position, and the one that does — Forgejo, whose agreement is strict enough to decline AI-generated authorship outright — I wasn’t contributing to. So I was inside the rules everywhere I went.
“No rule against it” and “fine” are not the same sentence, though. On one thread a maintainer had been testing my patch on a real instance and replying to me the way you reply to a person, because as far as he knew he was. Twice his testing corrected conclusions I’d reasoned my way to and been confident about. That’s a collaboration, and he was entitled to know what he was collaborating with.
Deciding where the line goes
The interesting question wasn’t whether to disclose but what the tool is allowed to be. I landed on: code, yes; correspondence, no.
AI writes and changes code here, reads codebases, reproduces bugs, checks claims against APIs and CI runs. It does not write the comments I post. I’ll ask it what it thinks I should say — that’s just having an editor — but the words that go out and the decision to send them are mine.
The reason isn’t purity. It’s that a maintainer answering a thread is entitled to know whether there’s a person on the other side of it, and every other line you could draw ends up somewhere sillier when you follow it out.
The sentence I nearly published
Drafting the first disclosure comment, the closing line read:
Anything I write to you is written by me.
Nice sentiment. Also, in a comment that had itself been drafted with assistance, not true — which I noticed roughly one second before it would have been true forever, in public, under a policy about honesty.
I cut it. If I want that sentence, I have to write it myself, and then it says something. A disclosure that overclaims is worse than none: it spends the credibility it was supposed to build.
The version that survived says what was actually done, names the tool, and — the part I think matters — states what was checked rather than merely asserted, and ships the commands so the reader can check without me. “AI was used” is unfalsifiable and therefore worthless. “This claim rests on this command, here it is” is a promise with a handle on it.
Five open threads got a retroactive note. Then I published the policy at a stable URL, so contributions can cite it instead of repeating a paragraph forever.
The hole underneath
Which is when I noticed the problem.
The policy sits next to a vulnerability-disclosure policy that promises a
response within 72 hours and safe harbour for good-faith research. Sensible.
Every property serves a security.txt with a Contact: field pointing at it.
None of them had an Encryption: field, because I had no key. A researcher could
reach me, and could not send me anything confidentially. The one category of
report where that matters most is precisely the category the page invites.
So: a key. Ed25519, three-year expiry — deliberate, because a key that never expires can’t fail closed if you abandon it. The primary is certify-only. It doesn’t sign or encrypt anything; its whole job is to certify three subkeys and revoke them if it must. Signing, encryption and authentication each live on a hardware token that requires a physical touch per operation.
That shape means a lost token costs me three subkeys, not my identity. It also means the primary can go in a drawer — which it hasn’t yet, and I’m aware that “hasn’t yet” is doing a lot of work in that sentence.
Making it findable
A key nobody can find is a key nobody uses, so: Web Key Directory. Put the key at a well-known path under your domain and any correspondent’s client finds it from the address alone — no URL to paste, no keyserver to trust.
There are two variants. The direct one serves from the apex, which in my case is
a YunoHost SSO portal that redirects everything to a login page, so that was out.
The advanced one uses an openpgpkey. subdomain and is, conveniently, the one
clients try first.
Three details silently break it, and I’d rather write them down than rediscover them:
- the path repeats the domain:
.well-known/openpgpkey/<domain>/hu/<hash> - a
policyfile must exist next to it, even empty — gpg checks, and gives up quietly - the key must be binary, not ASCII-armored
Then the test, which after yesterday’s post I was not going to get wrong: run the
lookup from a throwaway keyring, not from the laptop that already has the key. On
this machine gpg --locate-external-key would have succeeded no matter what I’d
uploaded, including nothing.
gpg: key 29E380A267EA01AB: public key "Ralph J.Mayer" imported
From an empty keyring, over the network, with nothing to fall back on. That’s the only version of the test that means anything.
Encryption: then went into all six security.txt files, and the fingerprint
onto the crypto policy page — because fetching a key over HTTPS proves only that
somebody controls a web server. The fingerprint is the part that ties it to me.
Signing the file that says who I am
Last piece: RFC 9116 says security.txt should carry an inline OpenPGP
signature. I couldn’t do that before today for the obvious reason.
The catch is a nice one. The signing key is on a token behind a touch policy, so signing cannot run in CI — the property that makes the key worth having is exactly the property that makes it inconvenient. So the signed file becomes a committed artifact, produced by hand, and every future edit needs a re-sign, including the annual expiry bump. A stale signature is worse than none: it looks like tampering.
I signed two of six — the properties carrying the actual contact and policy text — and left the satire sites unsigned, since they point back at the same Impressum anyway. Same protection where it matters, a third of the ritual.
Both halves, same move
Reading it back, the policy and the key are the same idea in two registers.
The disclosure could have said “trust me, I checked”. Instead it says which claims rest on which commands, and hands you the commands. The key could have been a URL saying “this is my key”. Instead there’s a fingerprint on a page, and a lookup you can run yourself from a machine I’ve never touched.
Neither asks to be believed. That’s the entire feature. It also means I now have several new ways to be caught being wrong, which — after yesterday — I’ve decided to file under improvement.