[HN Gopher] Go Cryptography State of the Union
___________________________________________________________________
Go Cryptography State of the Union
Author : ingve
Score : 110 points
Date : 2025-11-20 17:07 UTC (5 hours ago)
(HTM) web link (words.filippo.io)
(TXT) w3m dump (words.filippo.io)
| edoceo wrote:
| I'm curious about how GC languages handle crypto. Is it a risk
| that decrypted stuff or keys and things may be left in memory
| (heap?) before the next GC cycle?
| Thaxll wrote:
| If you have access to the local machine no language will save
| you.
| edoceo wrote:
| To oversimplifiy, it's like the same-ish risk level as JS or
| PHP or Ruby? (assuming the underlying algorithm is good)
| OhMeadhbh wrote:
| Sure. But there are several graduations of threat between
| "zero access" and "complete access." On the intarwebs, every
| request is from a potential attacker. Attackers are known for
| violating RFC3514, so it is frequently useful to build a
| trust model and use existing access control mechanism to deny
| "sensitive" data (or control functions) to protocol
| participants who cannot verify their identity and/or access
| permission.
|
| 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.
| OhMeadhbh wrote:
| What we did with Java (J/SAFE) was to add explicit methods to
| zero out sensitive info. It was a bit of a PITA because Java's
| never had consistent semantics about when final(ize,ly) methods
| were called. Later we added code to track which objects were
| allocated, but no longer needed, which also wasn't much fun.
|
| 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."
| alphazard wrote:
| It can be, another risk it that a secret value is left on the
| stack, and is never overwritten because the stack doesn't get
| to that memory address again, so it's never overwritten or
| zerod.
|
| 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.
| FiloSottile wrote:
| It's not that simple! What about intermediate values during
| arithmetic computations? What about registers spilling during
| signal handling?
|
| I honestly thought it could not be done safely, but the
| runtime/secret proposal discussion proved me wrong.
| FiloSottile wrote:
| You might find this proposal and the upcoming runtime/secret
| package interesting.
|
| https://github.com/golang/go/issues/21865
| networked wrote:
| I evaluated but didn't adopt
| https://github.com/awnumar/memguard in Go. No matter how well
| it is implemented, I can't pass its secrets to
| https://github.com/FiloSottile/age. Instead, I consider all
| process memory to potentially contain residual secrets. In two
| Go programs, a password manager and an encrypted file editor, I
| prevent process memory from being swapped out to disk with
| https://pkg.go.dev/syscall#Mlockall.
| gethly wrote:
| clear([]byte) if you want to go to the extreme and clean your
| own memory.
| OhMeadhbh wrote:
| I'm more of a C person than a Go person, but I am unbelievably
| happy that someone in that community is using the word
| "cryptography" to mean cryptography and not Bitcoin.
| jsheard wrote:
| Wasn't it just the shorthand "crypto" that got co-opted by the
| Shitcoin Industrial Complex? I think "cryptography" still means
| what it always meant regardless of who you ask.
| OhMeadhbh wrote:
| That's mostly the case, but I've seen job postings for
| "cryptography experts" that are, as best I can tell, looking
| for block chain hucksters. But I'm unlikely to work for
| Microsoft, so I just ignore them.
| jsheard wrote:
| Well yeah, someone hired to work with the low-level nuts
| and bolts of blockchains would ideally need to know their
| way around actual bona-fide cryptography.
| OhMeadhbh wrote:
| Respectfully disagree. My experience is that they've read
| a couple chapters from Applied Cryptography and think
| "IACR" is a router manufacturer.
|
| But we all see different parts of the industry. Happy to
| hear you're encountering more capable people in that
| industry.
| jsheard wrote:
| Oh no I'm not speaking from experience, just saying that
| _in principle_ you would want actual cryptography experts
| in that position, so there 's no contradiction in terms.
| How that tends to play out in practice is another
| question.
| FiloSottile wrote:
| There are definitely better cryptographers than me
| working at Zcash, for example.
| OhMeadhbh wrote:
| I was at Microsoft for meetings once and one of the guys
| asked if I was a cryptographer. I replied that it depends
| on who else is in the room. If it's just me and my boss,
| I'd probably qualify as being a cryptographer. But if I'm
| in the room with Niels Ferguson or Adi Shamir, I should
| be considered a software engineer who is somewhat decent
| at reading math papers. At about the same time, Niels
| Ferguson walked in the room and I quickly commented "you
| should not consider me a cryptographer for the rest of
| this meeting."
| OhMeadhbh wrote:
| Downvoted for mentioning that people confuse cryptography with
| Bitcoin? Good thing I didn't mention I think we're in an AI
| bubble. Or that I prefer emacs to vi.
| kunley wrote:
| Let's revert the self-scrutiny trend and actually enjoy a
| fellow mentioning his downvote. At least I am enjoying, your
| perception is valid.
| alphazard wrote:
| I don't know why the standard library crypto packages insist on
| passing around `[]byte` for things like a seed value, or why we
| can't just pass in a seed value to a single unambiguous
| constructor when generating asymmetric keys. Or how the
| constructor for a key pair could possibly return an error, when
| the algorithm is supposed to be deterministic.
|
| 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.
|
| [0] https://github.com/cloudflare/circl
| FiloSottile wrote:
| > I don't know why the standard library crypto packages insist
| on passing around `[]byte` for things like a seed value
|
| 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.
| tialaramex wrote:
| It's been many years since I wrote any Go for a living, but
| does Go seriously lack a way to say "foo is probably 32
| bytes, give me the 32 byte array, and if I'm wrong about how
| big foo is, let me handle that" ?
|
| 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.
| throwaway894345 wrote:
| > It's been many years since I wrote any Go for a living,
| but does Go seriously lack a way to say "foo is probably 32
| bytes, give me the 32 byte array, and if I'm wrong about
| how big foo is, let me handle that" ?
|
| 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.
| stouset wrote:
| Those aren't arguments for having []byte instead of [32]byte
| like you think they are. They're arguments for having an
| unambiguous IV type that can be constructed from a []byte or
| [32]byte, or responsibly generated on your behalf. The error-
| handling logic can be expressed once in the conversion
| process, and the rest of your crypto APIs can assume the
| happy path.
|
| 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.
| FiloSottile wrote:
| I'm not sure what you are referring to, but we were talking
| about keys, not IVs.
|
| Also, "an unambiguous key type that can be constructed from
| a []byte or responsibly generated on your behalf" is
| exactly what crypto/mlkem exposes.
| kalterdev wrote:
| What's wrong with zero values? They free the developer from
| guessing hidden allocations. IMO this benefit outweighs
| cast riddles by orders of magnitude.
| tialaramex wrote:
| Zero values prioritize implementation convenience (we
| always have a zero value so we don't need to handle any
| cases where we don't have a value, just says those are
| zero) over application convenience (maybe my type should
| not _have_ a default and the situation where it has no
| value is an error)
|
| 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.
| throwaway894345 wrote:
| I'm not nearly as angsty as the parent on this subject,
| but they don't really free the developer from guessing
| about hidden allocations--Go's allocations are still very
| much hidden even if developers can reasonably guess about
| the behavior of the escape analyzer. I think it would
| have been better if Go _required_ explicit assignment of
| all variables. That said, despite not being a big fan of
| this particular decision, Go is still by far the most
| productive language I've used--it strikes an excellent
| balance between type safety and productivity even if I
| think some things could be improved.
| the_gipsy wrote:
| They're like PHP: silent failures that ever push forwards
| through the system.
| alphazard wrote:
| > 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)
|
| 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.
| FiloSottile wrote:
| A cloud service that lets users upload their certificates
| and private keys, to be served by the service's CDN. Here
| the attacker is attacking the system's availability, not
| the key.
|
| (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.)
| amluto wrote:
| _boggle_
|
| 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.
| kbolino wrote:
| Right now, type casts (called conversions in the spec)
| always produce a single value. The idiomatic way to have
| checked casts would IMO be a two-value form, as this would
| be consistent with type assertions, channel receives, and
| map indexing, off the top of my head. End result would be
| something like: 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.
| XorNot wrote:
| But it would be a subvariant of a much more common
| pattern in code - destructuring which is quite noisy now
| but could just be: 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.
| kbolino wrote:
| I must disagree. Destructuring is nifty, but it is almost
| completely unrelated. The second value in any of the
| builtin two-value assignment forms is invariant on the
| type or size of RHS; it's always a boolean, and its
| meaning relates to the "kind" of RHS, not the exact type:
| 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.
| gethly wrote:
| Try creating new AES cypher, you will see that you have to
| provide 16, 24 or 36 bytes in order to get AES-128, AES-192, or
| AES-256. There is no single [32]byte, for example, as the
| length cannot be inherently fixed due to this.
| Aman_Kalwar wrote:
| This is a super helpful overview. Love how Go's crypto ecosystem
| keeps getting more opinionated and safer.
| pregnenolone wrote:
| I agree with the author's sentiment about FIPS 140. I find NIST
| to be incredibly slow. I understand there must be some stability,
| but they are too slow. For example, I think it's horrible that
| they are still recommending PBKDF2 in 2025.
| SAI_Peregrinus wrote:
| A big part of the problem I have with it is that it's a
| "ceiling" on security. Things like electrical code or building
| code are a "floor" on quality, you have to be _at least as good
| as the code requirements_ , but can freely be better. FIPS-140
| bounds you both ways. If you could more easily do better it'd
| be much less of a problem that NIST are slow.
| fl0ki wrote:
| In fairness, it's one thing for an implementation like a
| building to be as over-enginereed as possible in its own
| right, but it's another when a standard has to ensure that
| multiple implementations can interoperate. I'm not saying
| FIPS-140 has only that kind of limitation (far from it), just
| that this isn't the best analogy.
| akerl_ wrote:
| Is any of FIPS about ensuring interoperability?
| dlgeek wrote:
| Yeah, there's a ton of correctness testing involved.
| That's mostly at the algorithm, rather than the module
| level, so it'll fall under CAVP/ACVP rather than CMVP.
| akerl_ wrote:
| That's not for interop, that's for "are you actually
| doing the crypto you said you'd do". It's designed to
| prevent broken crypto, not to ensure coordination between
| parties.
| Analemma_ wrote:
| I don't love FIPS either, but cryptosystems don't work the
| same way as buildings and electrical codes. It's _very easy_
| to have "secure cryptosystem A" and "secure cryptosystem B",
| and then have massive security holes in "cryptosystem A + B".
| This happens all the time, and is one of the main reasons for
| the classic "don't roll your own crypto" admonition. The FIPS
| "whole system" mandate is meant to forestall this failure
| mode.
| leoqa wrote:
| What's up with all these bots posting 3-4 sentence summaries in
| the comment section?
___________________________________________________________________
(page generated 2025-11-20 23:00 UTC)