Go Proposal: Secret Mode
Posted by enz 5 days ago
Comments
Comment by Someone 1 day ago
I think that means this proposal adds a very specific form of finalisers to go.
How is that implemented efficiently? I can think of doing something akin to NSAutoReleasePool (https://developer.apple.com/documentation/foundation/nsautor...), with all allocations inside a `secret.Do` block going into a separate section of the heap (like a new generation), and, on exit of the block, the runtime doing a GC cycle, collecting and clearing every now inaccessible object in that section of the heap.
It can’t do that, though, because the article also says:
“Heap allocations are only erased if the program drops all references to them, and then the garbage collector notices that those references are gone. The program controls the first part, but the second part depends on when the runtime decides to act”
and I think what I am thinking of will guarantee that the garbage collector will eagerly erase any heap allocations that can be freed.
Also, the requirement “ the program drops all references to them” means this is not a 100% free lunch. You can’t simply wrap your code in a `secret.Do` and expect your code to be free of leaking secrets.
Comment by asmor 1 day ago
Comment by tapirl 21 hours ago
AddCleanup might be too heavy, it is cheaper to just set a bit in the header/info zone of memory blocks.
Comment by dpifke 5 days ago
And an in-progress proposal to make these various "bubble" functions have consistent semantics: https://github.com/golang/go/issues/76477
(As an aside, the linked blog series is great, but if you're interested in new Go features, I've found it really helpful to also subscribe to https://go.dev/issue/33502 to get the weekly proposal updates straight from the source. Reading the debates on some of these proposals provides a huge level of insight into the evolution of Go.)
Comment by kmeisthax 1 day ago
I mean, if you're worried about ensuring data gets zeroed out, you probably also don't want to leak it via side channels, either.
Comment by fsmv 5 days ago
Go has the best support for cryptography of any language
Comment by fastest963 5 days ago
Comment by kbolino 1 day ago
// Enabled reports whether Do appears anywhere on the call stack.
In other words, it is just a way of checking that you are indeed running inside the context of some secret.Do call; it doesn't guarantee that secret.Do is actually offering the protection you may desire.Comment by awithrow 5 days ago
Comment by kbolino 1 day ago
Comment by samdoesnothing 5 days ago
Comment by pants2 4 days ago
1. Well-supported standard libraries generally written by Google
2. Major projects like Vault and K8s that use those implementations and publish new stuff
3. Primary client language for many blockchains, bringing cryptography contributions from the likes of Ethereum Foundation, Tendermint, Algorand, ZK rollups, etc
Comment by adastra22 1 day ago
Because there is tremendous support for cryptography in, say, the C/C++ ecosystem, which has traditionally been the default language of cryptographers.
Comment by fsmv 1 day ago
I'm a big fan of the go standard library + /x/ packages.
Comment by Mawr 1 day ago
Comment by drowsspa 1 day ago
Comment by int_19h 1 day ago
Comment by alanfranz 1 day ago
But, this is probably a net improvement over the current situation, and this is still experimental, so, changes can happen before it gets to GA.
Comment by treyd 1 day ago
This isn't true at all.
Writing cryptography code in Go is incredibly annoying and cumbersome due lack of operator overloading, forcinforcing you to do method calls like `foo.Add(bar.Mul(baz).Mod(modulus)).Mod(modulus)`. These also often end up having to be bignums instead of using generic fixed-size field arithmetic types. Rust has incredibly extensive cryptographic libraries, the low-level taking advantage of this operator overloading so the code ends up being able to following the notation in literature more closely. The elliptic_curve crate in particular is very nice to work with.
Comment by oncallthrow 1 day ago
Edit: also, the supported platforms are ARM and x86. If your code isn’t running on one of those platforms, you probably know what you’re doing.
Comment by ctoth 1 day ago
Windows and MacOS?
Go is supposed to be cross-platform. I guess it's cross-platform until it isn't, and will silently change the semantics of security-critical operations (yes, every library builder will definitely remember to check if it's enabled.)
Comment by YesThatTom2 1 day ago
Many advanced Go features start in certain platforms and then expand to others once the kinks are worked out. It’s a common pattern and has many benefits. Why port before its stable?
I look forward to your PR.
Comment by baq 1 day ago
Comment by hypeatei 1 day ago
Which is exactly why it should fail explicitly on unsupported platforms unless the developer says otherwise. I'm not sure how Go developers make things obvious, but presumably you have an ugly method or configuration option like:
dangerousAllowSecretsToLeak()
...for when a developer understands the risk and doesn't want to panic.Comment by kbolino 1 day ago
Comment by satellite2 1 day ago
enclave, err := secret.GetEnclave()
// err contains whether the platform doesn't support it
enclave.Do(f)Comment by voodooEntity 1 day ago
It may sound naive, but packages which include data like said session related or any other that should not persist (until the next Global GC) - why don't you just scramble their value before ending your current action?
And dont get me wrong - yes that implies extra computation yada yada - but until a a solution is practical and builtin - i'd just recommend to scramble such variables with new data so no matter how long it will persist, a dump would just return your "random" scramble and nothing actually relevant.
Comment by raggi 1 day ago
Comment by to11mtm 1 day ago
First one being, it was -very- tricky to use properly for most cases, APIs to the outside world typically would give a byte[] or string or char[] and then you fall into the problem space you mention. That is, if you used a byte[] or char[] array, and GC does a relocation of the data, it may still be present in the old spot.
(Worth noting, the type itself doesn't do that, whatever you pass in gets copied to a non-gc buffer.)
The second issue is that there's not a unified unix memory protection system like in windows; The windows implementation is able to use Crypt32 such that only the current process can read the memory it used for the buffeer.
Comment by evntdrvn 1 day ago
Comment by willahmad 1 day ago
Imagine, 3 level nesting calls where each calls another 3 methods, we are talking about 28 functions each with couple of variables, of course you can still clean them up, but imagine how clean code will look if you don't have to.
Just like garbage collection, you can free up memory yourself, but someone forgot something and we have either memory leak or security issues.
Comment by HendrikHensen 1 day ago
key := make([]byte, 32)
defer scramble(&key)
// do all the secret stuff
Unless I don't understand the problem correctly.Comment by kbolino 1 day ago
1) You are almost certainly going to be passing that key material to some other functions, and those functions may allocate and copy your data around; while core crypto operations could probably be identified and given special protection in their own right, this still creates a hole for "helper" functions that sit in the middle
2) The compiler can always keep some data in registers, and most Go code can be interrupted at any time, with the registers of the running goroutine copied to somewhere in memory temporarily; this is beyond your control and cannot be patched up after the fact by you even once control returns to your goroutine
So, even with your approach, (2) is a pretty serious and fundamental issue, and (1) is a pretty serious but mostly ergonomic issue. The two APIs also illustrate a basic difference in posture: secret.Do wipes everything except what you intentionally preserve beyond its scope, while scramble wipes only what you think it is important to wipe.
Comment by voodooEntity 1 day ago
While in my case i had a program in which i created an instance of such a secret , "used it" and than scrambled the variable it never left so it worked.
Tho i didn't think of (2) which is especially problematic.
Prolly still would scramble on places its viable to implement, trying to reduce the surface even if i cannot fully remove it.
Comment by nemothekid 1 day ago
func do_another_important_thing(key []byte) []byte {
newKey := append(key, 0x0, 0x1) // this might make a copy!
return newKey
}
key := make([]byte, 32)
defer scramble(&key)
do_another_important_thing(key)
// do all the secret stuff
Because of the copy that append might do, you now have 2 copies of the key in data, but you only scramble one. There are many functions that might make a copy of the data given that you don't manually manage memory in Go. And if you are writing an open source library that might have dozens of authors, it's better for the language to provide a guarantee, rather than hope that a developer that probably isn't born yet will remember not to call an "insecure" function.Comment by voodooEntity 1 day ago
Comment by ok123456 1 day ago
Comment by voodooEntity 1 day ago
As another response pointed out, its also possible that said secret data is still in the register, which no matter what we do to the curr value could exist.
Thanks for pointing it out!
Comment by ok123456 1 day ago
This is essentially already the case whenever you use encryption, because there are tell-tale signs you can detect (e.g., RSA S-Box). But this will make it even easier and also tip you off to critical sections that are sensitive yet don't involve encryption (e.g., secure strings).
Comment by compsciphd 1 day ago
secretStash := NewSecretStash()
pString := secretStash.NewString()
....
....
secretStash.Thrash()
yes, you now have to deal in pointers, but that's not too ugly, and everything is stored in secretStash so can iterate over all the types it supports and thrash them to make them unusable, even without the gc running.Comment by mbreese 1 day ago
If you had to prompt a user for a password, you’d read it in, use it, then thrash the value.
read -p “Password: “ PASSWD
# do something with $PASSWD
PASSWD=“XXXXXXXXXXXXXXXXXX”
It’s not pretty, but a similar concept. (I also don't know how helpful it actually is, but that's another question...)Comment by voodooEntity 1 day ago
Comment by compsciphd 1 day ago
Comment by skywhopper 1 day ago
Comment by Thorrez 22 hours ago
That doesn't make sense to me. How can the "offset in an array itself" be "secret" if it's "always" 100? 100 isn't secret.
Comment by stingraycharles 21 hours ago
That’s my hunch at least, but I’m not a security expert.
The example could probably have been better phrased.
Comment by jabedude 1 day ago
Comment by circuit10 1 day ago
Comment by thr0w4w4y1337 1 day ago
1) allocations via memguard bypass gc entirely
2) they are encrypted at all times when not unsealed
3) pages are mprotected to prevent leakage via swap
4) and so on...
Not as ergonomic as OP's proposal, of course.
Comment by robmccoll 1 day ago
Comment by cyberax 1 day ago
Linux has memfd_secret ( https://man7.org/linux/man-pages/man2/memfd_secret.2.html ), that allow you to create a secure memory region that can't be directly mapped into regular RAM.
Comment by cafxx 1 day ago
func Encrypt(message []byte) ([]byte, error) {
var ciphertext []byte
var encErr error
secret.Do(func() {
// ...
})
return ciphertext, encErr
}
As that suggests that somehow for PFS it is critical that the ephemeral key (not the long-term one) is zeroed out, while the plaintext message - i.e. the thing that in the example we allegedly want secrecy for - is totally fine to be outside of the whole `secret` machinery, and remain in memory potentially "forever".I get that the example is simplified (because what it should actually do is protect the long-term key, not the ephemeral one)... so, yeah, it's just a bad example.
Comment by kbolino 19 hours ago
Comment by maxloh 1 day ago
I don't understand. Why do you need it in a garbage-collected language?
My impression was that you are not able to access any register in these language. It is handled by the compiler instead.
Comment by jerf 1 day ago
If you are concerned about secrets being zeroed out in almost any language, you need some sort of support for it. Non-GC'd languages are prone to optimize away zeroing out of memory before deallocation, because under normal circumstances a write to a value just before deallocation that is never effectfully read can be dropped without visible consequence to the rest of the program. And as compilers get smarter it can be harder to fool them with code, like, simply reading afterwards with no further visible effect might have been enough to fool 20th century compilers but nowadays I wouldn't count on my compiler being that stupid.
There are also plenty of languages where you may want to use values that are immutable within the context of the language, so there isn't even a way to express "let's zero out this RAM".
Basically, if you don't build this in as a language feature, you have a whole lot of pressures constantly pushing you in the other direction, because why wouldn't you want to avoid the cost of zeroing memory if you can? All kinds of reasons to try to avoid that.
Comment by er4hn 1 day ago
In practice it provides a straightforward path to complying with government crypto certification requirements like FIPS 140 that were written with languages in mind where this is an issue.
Comment by kbolino 1 day ago
And any language which can call C code that is resident in the same virtual memory space can have its own restrictions bypassed by said C code. This even applies to more restrictive runtimes like the JVM or Python.
Comment by hamburglar 1 day ago
Comment by kittywantsbacon 1 day ago
Comment by vlovich123 1 day ago
Comment by tptacek 1 day ago
Since "safety" is an encompassing term, it's easy to find more rigorous definitions of the term that Go would flunk; for instance, it relies on explicit synchronization for shared memory variables. People aren't wrong for calling out that other languages have stronger correctness stories, especially regarding concurrency. But they are wrong for extending those claims to "Go isn't memory safe".
Comment by vlovich123 1 day ago
It is true that go is only memory unsafe in a specific scenario, but such things aren’t possible in true memory safe languages like c# or Java. That it only occurs in multithreaded scenarios matters little especially since concurrency is a huge selling point of the language and baked in.
Java can have data races, but those data races cannot be directly exploited into memory safety issues like you can with Go. I’m tired of Go fans treating memory safety as some continuum just because there are many specific classes of how memory safety can be violated and Go protecting against most is somehow the same as protecting against all (which is what being a memory safe language means whether you like it or not).
I’m not aware of any other major language claiming memory safety that is susceptible to segfaults.
Comment by Mawr 1 day ago
Also, by your definition, e.g. Rust is not memory safe. And "It is true that Rust is only memory unsafe in a specific scenario, but [...]". I hope you agree.
Comment by tptacek 1 day ago
Comment by vlovich123 1 day ago
Blatantly false. From Ralf’s post:
> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x2a pc=0x468863]
The panic address is 42, a value being mutated, not a nil pointer. You could easily imagine this address pointing to a legal but unintended memory address resulting in a read or write of unintended memory.
Comment by tptacek 1 day ago
Comment by josefx 1 day ago
The 42 is an explicit value in the example code. From what I understand the code repeatedly changes the value assigned to an interface variable from an object containing a pointer to an object containing an integer. Since interface variables store the type of the assigned value, but do not update both type and value atomically a different thread can interpret whatever integer you put into it as a valid pointer. Putting a large enough value into the integer should avoid the protected memory page around 0 and allow for some old fashioned memory corruption.
Comment by vlovich123 1 day ago
> When that happens, we will run the Ptr version of get, which will dereference the Int’s val field as a pointer – and hence the program accesses address 42, and crashes.
If you don’t see an exploit gadget there based on a violation of memory safety I don’t know how to have a productive conversation.
Comment by tptacek 1 day ago
Comment by afdbcreid 1 day ago
Of course, Go allows more than that, with data races it's possible to reach use after free or other kinds of memory unsafety, but just segfaults don't mark a language memory unsafe.
Comment by cyberax 1 day ago
This stems from the fact that Go uses fat pointers for interfaces, so they can't be atomically assigned. Built-in maps and slices are also not corruption-safe.
In contrast, Java does provide this guarantee. You can mutate structures across threads, and you will NOT get data corruption. It can result in null pointer exceptions, infinite loops, but not in corruption.
Comment by tptacek 1 day ago
Serious systems built in memory-unsafe languages yield continual streams of exploitable vulnerabilities; that remains true even when those systems are maintained by the best-resourced security teams in the world. Functionally no Go projects have this property. The empirics are hard to get around.
Comment by cyberax 1 day ago
> Serious systems built in memory-unsafe languages yield continual streams of exploitable vulnerabilities
I'm not saying that Go is as unsafe as C. But it definitely is NOT completely safe. I've seen memory corruptions from improper data sync in my own code.
Comment by tptacek 1 day ago
Comment by cyberax 16 hours ago
Comment by tptacek 16 hours ago
It's not like this is a small track record. There is a lot of Go code, a fair bit of it important, and memory corruption exploits in non-FFI Go code is... not a thing. Like at all.
Comment by cyberax 9 hours ago
I triggered SSM agent crashes while developing my https://github.com/Cyberax/gimlet by doing concurrent requests.
I'm certain that they could have been used to do code execution, but it just makes no real sense given the context.
Comment by gethly 1 day ago
Comment by raggi 1 day ago
Comment by yyyk 18 hours ago
Comment by baq 1 day ago
// Only the ciphertext leaves this closure.
This ideally should be describable by the type system.Comment by hamburglar 1 day ago
Comment by nixpulvis 1 day ago
https://github.com/rust-lang/rust/issues/17046
https://github.com/conradkleinespel/rpassword/issues/100#iss...
Comment by __turbobrew__ 1 day ago
Seems like this should raise a compiler error or panic on runtime.
Comment by teeray 1 day ago
Comment by tjpnz 1 day ago
Comment by _1tan 1 day ago
Comment by burnt-resistor 1 day ago
Big thumbs down from me.
Comment by jeffrallen 1 day ago
Comment by leoh 1 day ago
Comment by IshKebab 1 day ago
Comment by raggi 1 day ago
Comment by purplesyringa 1 day ago
Comment by IshKebab 1 day ago
You could definitely zero registers that way, and a allocator that zeros on drop should be easy. The only tricky thing would be zeroing the stack - how do you know how deep to go? I wonder what Go's solution to that is...
Comment by purplesyringa 1 day ago
And stack's the main problem, yeah. It's kind of the main reason why zeroing registers is not enough. That and inter-procedural optimizations.
Comment by raggi 21 hours ago
In general though getting to a fairly predictable place is possible and the typical case of key material shouldn’t have highly arbitrary stacks, if you do you’re losing (see io comment above).
https://docs.rs/zeroize/1.8.1/zeroize/ has been effective for some users, it’s helped black box tests searching for key material no longer find it. There are also some docs there on how to avoid common pitfalls and links to ongoing language level discussions on the remaining and more complex register level issues.
Comment by IshKebab 23 hours ago
Comment by leoh 1 day ago
Comment by purplesyringa 1 day ago
In a nutshell, if you have a function like
fn secret_func() -> LargeType {
/* do some secret calculations */
LargeType::init_with_safe_Data()
}
...then even if you sanitize heap allocations and whatnot, there is still a possibility that those "secret calculations" will use the space allocated for the return value as a temporary location, and then you'll have secret data leaked in that type's padding.More realistically, I'm assuming you're aware that optimizing compilers often simplify `memset(p, 0, size); free(p);` to `free(p);`. A compiler frontend can use things like `memset_s` to force rewrites, but this will only affect the locals created by the frontend. It's entirely possible that the LLVM backend notices that the IR wants to erase some variable, and then decides to just copy the data to another location on the stack and work with that, say to utilize instruction-level parallelism.
I'm partially talking out of my ass here, I don't actually know if LLVM utilizes this. I'm sure it does for small types, but maybe not with aggregates? Either way, this is something that can break very easily as optimizing compilers improve, similarly to how cryptography library authors have found that their "constant-time" hacks are now optimized to conditional jumps.
Of course, this ignores the overall issue that Rust does not have a runtime. If you enter the secret mode, the stack frames of all nested invoked functions needs to be erased, but no information about the size of that stack is accessible. For all you know, memcpy might save some dangerous data to stack (say, spill the vector registers or something), but since it's implemented in libc and linked dynamically, there is simply no information available on the size of the stack allocation.
This is a long yap, but personally, I've found that trying to harden general-purpose languages simply doesn't work well enough. Hopefully everyone realizes by now that a borrow checker is a must if you want to prevent memory unsoundness issues in a low-level language; similarly, I believe an entirely novel concept is needed for cryptographical applications. I don't buy that you can just bolt it onto an existing language.
Comment by leoh 1 day ago
Comment by leoh 1 day ago
use std::ptr;
struct SecretData {
data: Vec<u8>,
}
impl Drop for SecretData {
fn drop(&mut self) {
// Zero out the data
unsafe {
ptr::write_bytes(self.data.as_mut_ptr(), 0, self.data.len());
}
}
}Comment by steveklabnik 1 day ago
Comment by IshKebab 1 day ago
Comment by rurban 11 hours ago