Today I was looking at Upspin and thinking about private keys. I asked myself, “what would it take to make sure that there was one single copy of the private key in RAM, and that Go and the OS worked together to make sure it never went onto disk?”
(Some other people have talked about this too.)
It turns out the answer to that question is much harder than expected, but doable and I’ll try to do it.
So, the last part first. How do you ask the kernel to help you make sure that a given piece of RAM never ends up on disk? There’s a hint here. I have not looked to see if this technique has been picked up by OpenSSL or BoringSSL. The way Akamai hooked it in to ASN.1 parsing ugly and gross and we won’t be doing that, but the kernel system calls they are using are interesting.
mlock and madvise work at page granularity. Data structures exposed in Go programs are at byte granularity, so we’ll need some kind of technique to convince Go to put our data on a page all to itself. I’m thinking that make(byte, 0, pagesize) will probably do what we want. Once the underlying page is marked correctly, we return byte to the caller. It is their responsibility to (1) never reallocate the underlying storage (i.e. only write into it via copy, never append) and (2) make sure that all of the places in the code where the private key is used do not make copies off of this secure page.
Then we need to go hunting, looking for places that the Go runtime is going to make copies of our private key. This is easy but error-prone work (at least for a Go veteran, who can spot when a copy is happening), and it makes me wonder if undertaking a task like this in Rust would be easier; maybe you’d be able to mark the private key immutable and implement the Copy trait as panic, then see what broke? Although I think you would still need to audit the code, because it would be possible to copy out the bytes in a typesafe way, I think.
Here’s a list of places I’ve already noticed I’d need to work on:
- in Upspin’s factotum.go, the private key comes off disk and goes into memory. That’s in a byte, so we’d be able to put it into our safe byte, no problem. However it is converted to a string and then back into a byte, which would have to be avoided. No biggie, and also some nice cleanup because stores the private key as a string (unused) and also an ecdsaKeypair.
- The ecdsa.PrivateKey ends up with a pointer in it to a big.Int, which has the private key in it. It gets there via UnmarshalText. But UnmarshalText converts to a string too, so it has to get fixed. It seems likely that UnmarshalText should be using Fscanf to read directly from the byte it receives, since big.Int implements fmt.Scanner.
- While constructing the big.Int to hold the private key, there is a risk of leaving parts, or maybe all of the private key around in memory. This is because the final length of the underlying “words” array where the private key is stored is not known ahead of time to the math/big library. However, we do know how long the private key is going to be, so we can store a decoy private key into the big.Int to get it to stretch out it’s words slice, and then scan the real private key in, so that the word slice underlying array is never reallocated while reading the private key. However, there does not appear to be a way to tell big.Int where it should put the Word. Due to type safety, it would require unsafe to do that. But it can be done in package factotum before the first call to Unmarshal.
- The implementation of big.Int.Bytes() is used from time to time, and it causes a copy of the private key to be made. It does not allow us to pass in “please write the bytes here” argument in order to make sure that it is copying the private bytes exactly where we want them to go (i.e. to the protected page). At first glance, this would appear to require a new API, which is too bad.
- The factotum package does not expose the private key, so it cannot be accessed outside of this package. That makes auditing usage of the key easy to make sure that it stays private. Thank you Upspin hackers!
So, in summary, this is probably doable, except that controlling the backing store for the big.Int to be private requires unsafe-ness.