Go Cryptography State of the Union
Mood
thoughtful
Sentiment
positive
Category
tech
Key topics
Go Programming
Cryptography
Security
The author discusses the current state of cryptography in Go, highlighting recent developments and improvements.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
46m
Peak period
51
Day 1
Avg / period
51
Based on 51 loaded comments
Key moments
- 01Story posted
Nov 20, 2025 at 12:07 PM EST
3d ago
Step 01 - 02First comment
Nov 20, 2025 at 12:53 PM EST
46m after posting
Step 02 - 03Peak activity
51 comments in Day 1
Hottest window of the conversation
Step 03 - 04Latest activity
Nov 20, 2025 at 11:26 PM EST
3d ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
These models can get complex quickly, but are nevertheless important to evaluate a system's specified behaviour.
No system is perfect and your mileage may vary.
Go really just needs a few `crypto.Secret` values of various sizes, or maybe a generic type that could wrap arrays. Then the runtime can handle all the best practices, like a single place in memory, and aggressive zeroing of any copies, etc.
I honestly thought it could not be done safely, but the runtime/secret proposal discussion proved me wrong.
Back in the Oak days Sun asked us (I was at RSADSI at the time) to review the language spec for security implications. Our big request was to add the "secure" storage specifier for data. The idea being a variable, const, whatever that was marked "secure" would be guaranteed not to be swapped out to disk (or one of a number of other system specific behaviors). But it was hard to find a concrete behavior that would work for all platforms they were targeting (mostly smaller systems at the time.)
My coworker Bob Baldwin had an existing relationship with Bill Joy and James Gosling (I'm assuming as part of the MIT mafia) so he led the meetings. Joy's response (or maybe Goslings, can't remember anymore) was "Language extension requests should be made on a kidney. Preferably a human kidney. Preferably yours. That way you'll think long and hard about it and you sure as hell won't submit 2."
I assume all process memory may contain residual secrets. As a mitigation in a password manager and an encrypted file editor, I prevent process memory from being swapped to disk with https://pkg.go.dev/syscall#Mlockall.
But we all see different parts of the industry. Happy to hear you're encountering more capable people in that industry.
It all just seems a bit sloppy. Asking for a seed value like `[32]byte` could at least communicate to me that the level of security is at most 256 bits. And removing all dependencies on rand would make it obvious where the entropy must be coming from (the seed parameter). Cloudflare's CIRCL[0] library does a bit better, but shares some of the same problems.
These are actually very deliberate choices, based on maybe unintuitive experience.
We use []byte instead of e.g. [32]byte because generally you start with a []byte that's coming from somewhere: the network, a file format, a KDF.
Then you have two options to get a [32]byte: cast or copy. They both have bad failure modes. If you do a ([32]byte)(foo) cast, you risk a panic if the file/packet/whatever is not the size you expected (e.g. because it's actually attacker controlled). If you do a copy(seed, foo) it's WAY WORSE, because you risk copying only 5 bytes and leaving the rest to zero and not noticing.
Instead, we decided to move the length check into the library everywhere we take bytes, so at worst you get an error, which presumably you know how to handle.
> why we can't just pass in a seed value to a single unambiguous constructor when generating asymmetric keys
I am not sure what you are referring to here. For e.g. ML-KEM, you pass the seed to NewDecapsulationKey768 and you get an opaque *DecapsulationKey768 to pass around. We've been moving everything we can to that.
> Or how the constructor for a key pair could possibly return an error, when the algorithm is supposed to be deterministic.
Depends. If it takes a []byte, we want to return an error to force handling of incorrect lengths. If the key is not a seed (which is only an option for private keys), it can also be invalid, deterministic or not. (This is why I like seeds. https://words.filippo.io/ml-kem-seeds/)
> removing all dependencies on rand would make it obvious where the entropy must be coming from (the seed parameter)
Another place where experience taught us otherwise. Algorithms that take a well-specified seed should indeed just take that (like NewDecapsulationKey768 does!), but where the spec annoyingly takes "randomness from the sky" (https://words.filippo.io/avoid-the-randomness-from-the-sky/) in an unspecified way, taking a io.Reader gave folks the wrong impression that they could use that for deterministic key generation, which then breaks as soon as we change the internals.
There is only one place to get entropy from in a Go program, anyway: crypto/rand. Anything else is a testing need, and it can be handled with test affordances like the upcoming crypto/mlkem/mlkemtest or testing/cryptotest.SetGlobalRandom.
If the caller was expected to provide a duration and your language has a duration type, you presumably wouldn't take a string, parse that and if it isn't a duration return some not-a-duration error, you'd just make the parameter a duration. It seems like this ought to be a similar situation.
Not in the static type signature, but you can do that as a runtime check either by casting and handling the potential panic (as described above) or by checking the size and returning an error if it's not as expected, which is what the library does.
The difference is really that [32]Byte is a single pointer (in compiler hands that you never touch) to a slab of 32 bytes of memory; the []Byte (of internal size 32 use 0-32) has the current allocated size, current used size, and a pointer (none of which you directly touch but two of which are trivial to affect with language semantics) values that point to a 32 Byte backing array.
The only time this matter is timing or performance critical code. With respect to cryptography, timing might be critical for performance, but it's absolutely critical for never taking _variable_ time to perform an operation based on data as well as not on key. In that respect this doesn't matter.
Of course, this isn’t really reasonable given golang’s brain-dead approach to zero values (making it functionally impossible to structurally prevent using zero IVs). But it just serves as yet another reminder that golang’s long history of questionable design choices actively impede the ability to design safe, hard-to-misuse APIs.
Take either of Rust's library types named Ordering - core::cmp::Ordering (is five less than eight or greater?) or core::sync::atomic::Ordering (even if it's on another core this decrement definitely happens before that check) neither of these implements Default because even though internally they both use the zero bit pattern to mean something, that's a specific value, not a default.
Also, "an unambiguous key type that can be constructed from a []byte or responsibly generated on your behalf" is exactly what crypto/mlkem exposes.
Can you give an example of a situation where that is actually a concern? It doesn't really seem like a realistic threat model to me. Knowledge of the key is pretty much how these algorithms define attackers vs. defenders. If the attacker has the key that's gg.
There are lots of things in Go that can panic. Even in syntax, the conversion is very similar to an interface conversion, and those haven't been a problem for me in practice, partly because of good lint rules to force checking the "okay" boolean.
(But also, it's easy to see how this is a problem for public keys and ciphertexts, and it would be weird to have an inconsistent API for private keys.)
I’m not a golang programmer, but I find this quite bizarre. Sure, C++ arrays are awful and even std::array may not be able to legally alias a vector. But Rust (no surprise) gets this right — there is a properly fallible conversion from slice reference to array reference.
But I guess Go doesn’t. This seems silly to me.
var x []byte
y, ok := [32]byte(x)
// ...
However, it feels like a relatively significant change to the language just for a niche use. Even the ability to cast from []T to [N]T or *[N]T is actually fairly new (Go 1.20 and 1.17, respectively). I don't think it's that hard to check the length before casting, though a checked cast would be convenient since you wouldn't have to repeat the length between the check and the cast. a, b, rest := strings.Split(somestr, "/")
Which would be conventional for the whole thing, and the check would be for an empty type after rest.I usually wind up using something like the samber/lo library to reduce the noise here. You wind up doing this all the time.
msg, ok := <-ch // ok means "channel ch is not closed"
val, ok := m[key] // ok means "map m contains key"
dyn, ok := i.(T) // ok means "interface i's dynamic type is T or implements T"
This new operation would be similar: arr, ok := [N]T(s) // ok means "slice s has len(s) == N"
For all of these cases, ok being true indicates that the other value is safe to use.4 more comments available on Hacker News
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.