<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Go-Encryption on PHP Boy Scout</title>
        <link>https://phpboyscout.uk/categories/go-encryption/</link>
        <description>Recent content in Go-Encryption on PHP Boy Scout</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-gb</language>
        <copyright>Matt Cockayne</copyright>
        <lastBuildDate>Tue, 20 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/categories/go-encryption/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>My reason for hand-rolling it was wrong</title>
            <link>https://phpboyscout.uk/my-reason-for-hand-rolling-it-was-wrong/</link>
            <pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate>
            <guid>https://phpboyscout.uk/my-reason-for-hand-rolling-it-was-wrong/</guid>
            <description>&lt;img src=&#34;https://phpboyscout.uk/my-reason-for-hand-rolling-it-was-wrong/cover-my-reason-for-hand-rolling-it-was-wrong_hu_f7d79f9cef462fd.jpg&#34; alt=&#34;Featured image of post My reason for hand-rolling it was wrong&#34; /&gt;&lt;p&gt;One evening I deleted four hundred lines of my own cryptographic packet code, replaced the lot with someone else&amp;rsquo;s library, and ran the tests. They passed, which was not the result I was after.&lt;/p&gt;&#xA;&lt;h2 id=&#34;writing-your-own-is-the-worst-default-there-is&#34;&gt;Writing your own is the worst default there is&#xA;&lt;/h2&gt;&lt;p&gt;The argument starts against me, so I&amp;rsquo;ll start there too. There is a maintained OpenPGP implementation for Go. &lt;a class=&#34;link&#34; href=&#34;https://github.com/ProtonMail/go-crypto&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;ProtonMail/go-crypto&lt;/a&gt; has something like 479 known importers, including go-git, Terraform, OpenTofu, Flux and most of the release-signing tooling in the ecosystem. If you need OpenPGP in Go, that&amp;rsquo;s the answer, and writing your own packet, signature and MPI code instead is close to the worst default a project can pick.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;d written my own anyway, in &lt;a class=&#34;link&#34; href=&#34;https://encryption.go.phpboyscout.uk/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;&lt;code&gt;go/encryption&lt;/code&gt;&lt;/a&gt;, and I had a reason. Our private key lives in a KMS, so it cannot be read, exported or handed to a library. Assembling an OpenPGP certificate needs signatures made &lt;em&gt;with&lt;/em&gt; that key, and my claim was that go-crypto simply couldn&amp;rsquo;t do that, because it wants a private key and I haven&amp;rsquo;t got one to give it. Reasonable, confident, written down in my own docs. And wrong!&lt;/p&gt;&#xA;&lt;h2 id=&#34;test-it-by-deleting-it&#34;&gt;Test it by deleting it&#xA;&lt;/h2&gt;&lt;p&gt;I didn&amp;rsquo;t argue about it, I deleted the package. Actually removed it (not &amp;ldquo;decided not to use it&amp;rdquo;, gone), wired go-crypto in its place, and ran the acceptance tests to see what broke.&lt;/p&gt;&#xA;&lt;p&gt;As far as I could tell, nothing broke.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;packet.PrivateKey&lt;/code&gt; accepts a &lt;code&gt;crypto.Signer&lt;/code&gt; in place of an actual key, so a signature can be made by something that never exposes any key material, which is more or less the definition of a KMS. A spike carried it end to end: a full certificate, both signatures made through a signer, accepted by an independent parser. So the four hundred lines went, because a claim that doesn&amp;rsquo;t hold isn&amp;rsquo;t much of a justification, it&amp;rsquo;s a story I&amp;rsquo;d been telling myself.&lt;/p&gt;&#xA;&lt;p&gt;There was a bonus humiliation buried in the diff, too. Somewhere in that deleted code sat a real bug of mine: an MPI header written &lt;em&gt;before&lt;/em&gt; the leading zero octets were stripped, corrupting roughly one certificate in every hundred and twenty-eight. A one-in-128 bug, in cryptographic code I&amp;rsquo;d written because I didn&amp;rsquo;t trust the alternative!&lt;/p&gt;&#xA;&lt;h2 id=&#34;then-two-things-refused-to-die&#34;&gt;Then two things refused to die&#xA;&lt;/h2&gt;&lt;p&gt;The interesting part is what happened next, because the package came back. Not all of it, and not for the reason I&amp;rsquo;d originally given.&lt;/p&gt;&#xA;&lt;p&gt;The first thing that held is that ECDH decryption has nowhere to plug in. go-crypto&amp;rsquo;s &lt;code&gt;encrypted_key.go&lt;/code&gt; hard-asserts &lt;code&gt;*ecdh.PrivateKey&lt;/code&gt; when it decrypts a PKESK packet, so there is no interface to implement, no &lt;code&gt;crypto.Decrypter&lt;/code&gt; equivalent for ECDH, and no way to hand it a shared secret computed somewhere else. A key that lives in a KMS cannot be used for that at all, and I do mean at all rather than awkwardly or slowly.&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s a real asymmetry worth sitting with: RSA has an interface for this and ECDH hasn&amp;rsquo;t. Signing was fine because signing had &lt;code&gt;crypto.Signer&lt;/code&gt;; decryption wasn&amp;rsquo;t, because the equivalent hadn&amp;rsquo;t been built (yet, I suspect, and there is an open request for it).&lt;/p&gt;&#xA;&lt;p&gt;The second thing that held is the dependency footprint. &lt;code&gt;go/encryption&lt;/code&gt;&amp;rsquo;s build graph contains nothing but the standard library, and &lt;a class=&#34;link&#34; href=&#34;https://gitlab.com/phpboyscout/go/encryption/-/blob/50b0e2b/depfootprint_test.go&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;a test asserts it&lt;/a&gt; so nobody has to remember. Pulling go-crypto in costs &lt;code&gt;golang.org/x/crypto&lt;/code&gt;, &lt;code&gt;github.com/cloudflare/circl&lt;/code&gt; and everything underneath them, and when that dependency went in during the experiment it brought eighteen CVEs along that had to be resolved by raising pins. A consumer whose entire need is &amp;ldquo;decrypt a message addressed to my KMS key&amp;rdquo; would inherit the lot.&lt;/p&gt;&#xA;&lt;h2 id=&#34;this-is-not-go-cryptos-fault&#34;&gt;This is not go-crypto&amp;rsquo;s fault&#xA;&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s &lt;a class=&#34;link&#34; href=&#34;https://github.com/ProtonMail/go-crypto/pull/307&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;an open request&lt;/a&gt; from April asking for exactly the ECDH interface I needed. A maintainer&amp;rsquo;s reply is worth quoting:&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;For RSA decryption with a hardware token, it&amp;rsquo;s possible to set a &lt;code&gt;crypto.Decrypter&lt;/code&gt; as the &lt;code&gt;PrivateKey&lt;/code&gt; […] I would prefer to do the same for ECDH.&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;Agreed in principle, reviewed inside a day, then silence for four months including an unanswered follow-up. A related PKCS#11 request was &lt;a class=&#34;link&#34; href=&#34;https://github.com/ProtonMail/go-crypto/issues/228&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;declared out of scope&lt;/a&gt; outright.&lt;/p&gt;&#xA;&lt;p&gt;That reads like neglect until you remember what Proton actually is. Proton Mail holds its users&amp;rsquo; keys locally, under a passphrase. Externally held keys are not their problem to solve, and a maintainer declining hardware-token support for a product with no hardware tokens is making a perfectly sound call about their own roadmap. That&amp;rsquo;s the thing I&amp;rsquo;d been mis-describing all along: go-crypto isn&amp;rsquo;t an abandoned library so much as a vendor library that the rest of us defaulted to.&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-we-all-ended-up-here&#34;&gt;How we all ended up here&#xA;&lt;/h2&gt;&lt;p&gt;The history is worth the detour, because it explains the incentives. &lt;code&gt;golang.org/x/crypto/openpgp&lt;/code&gt; was &lt;a class=&#34;link&#34; href=&#34;https://github.com/golang/go/issues/44226&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;frozen and deprecated in March 2021&lt;/a&gt;. The proposal is blunt about why: it was &amp;ldquo;effectively unmaintained&amp;rdquo; and &amp;ldquo;the crypto package with the most open CLs&amp;rdquo;. The Go team had already &lt;a class=&#34;link&#34; href=&#34;https://github.com/golang/go/issues/30141&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;tried to recruit a community maintainer in 2019&lt;/a&gt; and no one appeared. The freeze notice suggests using a maintained community fork, and then adds, carefully: &lt;em&gt;we don&amp;rsquo;t endorse any specific one.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Proton had already forked in 2019, because they needed OpenPGP for Proton Mail and upstream had stalled, and the other serious fork, keybase&amp;rsquo;s, was archived in 2021. So the ecosystem converged on go-crypto by attrition, the one still standing after the others had gone.&lt;/p&gt;&#xA;&lt;p&gt;Half the Go release-signing tooling on the planet now depends on a fork maintained, entirely legitimately, for one company&amp;rsquo;s mail product. I can&amp;rsquo;t point at a wrong decision anywhere in that chain. It&amp;rsquo;s just what happens when a language freezes a package and hopes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-assembly-came-back-too&#34;&gt;Why assembly came back too&#xA;&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;d have got this bit wrong if I&amp;rsquo;d stopped at &amp;ldquo;keep decryption, delegate assembly&amp;rdquo;. Assembly and decryption are one problem, not two. The key-derivation function binds the recipient&amp;rsquo;s fingerprint into its parameter block, so a certificate assembled by one implementation and a message decrypted by another have to agree on that value &lt;em&gt;exactly&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Split them across modules and neither half can be tested without the other, and the acceptance test that actually matters (encrypt to a certificate we assembled, then open it) ends up living in a third place dragging both in.&lt;/p&gt;&#xA;&lt;p&gt;Keeping them together costs about three hundred lines and buys a test that runs on every commit with no credentials at all. That&amp;rsquo;s a good trade, and it&amp;rsquo;s a completely different argument from the one I started with.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-it-looks-like-now&#34;&gt;What it looks like now&#xA;&lt;/h2&gt;&lt;p&gt;go-crypto is still in that &lt;code&gt;go.mod&lt;/code&gt;, as a test dependency. Each message the package can open, it opens alongside go-crypto and checks they recover the same session key, and a second suite runs the same thing against &lt;code&gt;gpg&lt;/code&gt; itself, because gpg is what a security researcher is going to have on their machine. Hand-rolled implementation, validated against the library I didn&amp;rsquo;t use and the binary everyone else has.&lt;/p&gt;&#xA;&lt;p&gt;So I&amp;rsquo;ve ended up somewhere I didn&amp;rsquo;t expect. The code survived. My reason for writing it did not, and I only found that out by deleting the lot and watching the tests go green&amp;hellip; four hundred lines gone, three hundred back, and a justification that&amp;rsquo;s now narrow enough to be true.&lt;/p&gt;</description>
        </item></channel>
</rss>
