I think this is an understatement, considering that it's a core cryptographic library. It appears to have gone through at least five audits (though none since 2010), and includes integration with hardware cryptographic accelerators.
Suggesting a tabula rasa rewrite of NSS would more likely be met with genuine concern for your mental well-being, than by incredulity or skepticism.
In my experience, porting code more or less directly from one language to another is faster and easier than people assume. Its certainly way faster than I assumed. I hand ported chipmunk2d to javascript a few years ago. Its ~30k LOC and it took me about a month to get my JS port working correctly & cleaned up. I spend up throughout the process. By the end I had a bunch of little regexes and things which took care of most of the grunt work.
If we assume that rate (about 1kloc / person-day) then porting boringssl (at 356kloc[1]) to rust would take about one man-year (though maybe much less). This is probably well worth doing. If we removed one heartbleed-style bug from the source code on net as a result, it would be a massive boon.
(But it would probably be much more expensive than that because the new code would need to be re-audited.)
[1] https://www.openhub.net/p/boringssl/analyses/latest/language...
That's often true right up to the point where you have to be keenly aware of and exceptionally careful with details such as underlying memory management functionality or how comparisons are performed. With this in mind, cryptographic code is likely a pathological case for porting. It would be very easy to accidentally introduce an exploitable bug by missing, for example, that something intentionally reads from uninitialized memory.
On top of the re-audit being expensive.
Sounds terrible. This should never happen in any program, so any behavior relying on it is already broken.
I'm way more concerned by memory safety issues than cryptographic issues. Frankly, history has shown that cryptographic bugs are far easier to shake out and manage than memory safety bugs.
and yet, we had the debian/ubuntu openssl bug of 2008... due to someone not wanting to intentionally read from uninitialized memory. Really, it kind of proved the opposite. Valgrind and other tools can tell you about memory safety bugs. Understanding that the fix would result in a crypto bug was harder.
> Really, it kind of proved the opposite.
Not really. Exploited bugs in cryptographic protocols are extremely rare. Exploited memory safety bugs are extremely common.
> Valgrind and other tools can tell you about memory safety bugs.
Not really.
> Understanding that the fix would result in a crypto bug was harder.
Like I said, OpenSSL's PRNG was brutally flawed already and could have been broken on a ton of machines already without anyone knowing it. A compiler update, an OS update, or just unluckiness could have just as easily broken the PRNG.
Building memory unsafety into the prng was the issue.
Memory safety issues are exploited orders of magnitude more often than crypto bugs.
edit: Also, memory safety bugs typically have higher impact than crypto bugs. An attacker who can read arbitrary memory of a service doesn't need a crypto bug, they can just extract the private key, or take over the system.
Crypto bugs are bad. Memory safety bugs are way, way worse.
Reading uninitialised memory in C/C++ is Undefined Behaviour, plain and simple. That means Nasal Demons, up to and including arbitrary code execution vulnerabilities if you're unlucky enough.
If they had simply removed the offending line (or, indeed, set a preprocessor flag that was provided explicitly for that purpose) it would have been fine. The problem was that they also removed a similar looking line that was the path providing actual randomness.
Converting code to Rust while keeping the logic one-to-one wouldn't work. Rust isn't ensuring memory safety by just adding some runtime checks where C/C++ aren't. It (the borrow checker) relies on static analysis that effectively tells you that the way you wrote the code is unsound and needs to be redesigned.
I agree that this might slow down the process of porting the code though. I wonder how much by?
This is especially true given it’s a security library, which perhaps more than any other category I would want to be stable, compatible, and free of surprises.
“You must upgrade to the latest release the moment we release it or else you risk security vulnerabilities and won’t be able to link against any library that uses a different version of ring. Also, we don’t ‘do’ stable APIs and remove APIs the instant we create a new one, so any given release may break your codebase. Good luck have fun!”
Similarly, while the policy is still that ring only supports the newest rust compiler version, due to the fact that there has been no update for months already, you can use it with older compiler versions.
Last, the author used to yank old versions of its library, which caused tons of downstream pains (basically, if you are a library and are using ring, I recommend you have a Cargo.lock checked into git). This yanking has stopped since 3 years already, too. Don't think this was mentioned in the readme, but I feel it's an important improvements for users.
So a great deal of things has improved, although I'd say only the first bullet point is a permanent improvement, while the second two might be regressed upon. idk.
Why? Cryptographic primitives don’t really have a lot of complexity. It a bytes in/bytes out system with little chance for overflows. The bigger issues are things like side channel leaks or incorrect implementations. The former is where validation helps and the latter is validated by round-tripping with one half using a known-working reference implementation. Additionally, the failure mode is typically safe - if you encrypt incorrectly then no one else can read your data (typically). If you decrypt incorrectly, then decryption will just fail. Ciphers that could encrypt in an unsafe way (ie implementation “encrypts” but the encryption can be broken/key recovered) typically implies the cipher design itself is bad and I don’t think such ciphers are around these days. Now of course something like AES-GCM can still be misused by reusing the nonce but that’s nothing to do with the cipher code itself. You can convince yourself by looking for CVEs of cryptographic libraries and where they live. I’m not saying it’s impossible, but cipher and digest implementations from BoringSSL seem like a much less likely place for vulnerabilities to exist (and thus the security/performance tradeoff probably tilts in a different direction unless you can write code that’s both safer while maintaining competitive performance).
Polynomial hashes, elliptic curves, and anything involving huge numbers however are more delicate. Depending on how you implement them, you could have subtle limb overflow issues, that occur so extremely rarely by chance that random test don't catch them. For those you're stuck with either proving that your code does not overflow, or reverting to simpler, slower, safer implementation techniques.
Anyway, NSS wouldn't necessarily need to be replaced with a Rust component, it could well be an existing lib written in another (possibly GCd) safeish language, or some metaprogramming system or translator that generated safe C or Rust, etc. There might be something to use in Go or .net lands for example.
Why? I don't get it. Maintenance of NSS has to be seriously expensive.
Gotta love the US government’s war against crypto.
SSL is complicated because we didn't understand how to design secure protocols in the 90s. Didn't need help.
I’m reasonably sure it was covered in cryptogram a few decades ago. These days, it’s not really discoverable via internet search, since the EC thing drowned it out.
Edit: Here’s the top secret budget for the program from 2013. It alludes to ensuring 4G implementations are exploitable, and to some other project that was adding exploits to something, but ramping down. This is more than a decade after the SSL standards sabotage campaign that was eventually uncovered:
http://s3.documentcloud.org/documents/784159/sigintenabling-...
With SSL, the moles kept vetoing attempts to simplify the spec, and also kept adding complications, citing secret knowledge. It sounds like they did the same thing to 4G.
Note the headcount numbers: Over 100 moles, spanning multiple industries.
It may be too much work to be worth the time, but that's an entirely different matter.
I wonder, how many work years could be too much
(What would you or sbd else guess)
That’s just it though, it never was. That C/C++ code base is like a giant all-brick building on a fault line. It’s going to collapse eventually, and your users/the people inside will pay the price.
>That’s just it though, it never was. That C/C++ code base is like a giant all-brick building on a fault line. It’s going to collapse eventually, and your users/the people inside will pay the price.
Sure, but everything is a trade-off[1]. In this particular case (and many others) no user appeared to pay any price, which tells me that the price is a spectrum ranging from 'Nothing' to 'FullyPwned' with graduations in between.
Presumably the project will decide on what trade-off they are willing to make.
[1] If I understand your comment correctly, you are saying that any C/C++ project has a 100% chance of a 'FullyPwned' outcome.
I wonder if memory safety had mattered more, whether other languages might have caught on a bit more, developed more etc. Rust is the new kid, but memory safety in a language is not a totally new concept.
The iphone has gone down the memory unsafe path including for high sensitivity services like messaging (2007+). They have enough $ to re-write some of that if they had cared to, but they haven't.
Weren't older language like Ada or Erlang memory safe way back?
AFAIK ADA guarantees memory safety only if you statically allocate memory, and other languages have GC overhead.
Rust is really something new.
GCC has also had an optional bounds checking branch since 1995. [0]
GCC and Clang's sanitisation switches also support bounds checking, for the main branches, today, unless the sanitiser can't trace the origin or you're doing double-pointer arithmetic or further away from the source.
AddressSanitizer is also used by both Chrome & Firefox, and failed to catch this very simple buffer overflow from the article. It would have caught the bug, if the objects created were actually used and not just discarded by the testsuite.
They were only testing with AddressSanitizer, not running the built binaries with it? Doing so is slow to say the least, but you can run programs normally with these runtime assertions.
It even has the added benefit of serving as a nice emulator for a much slower system.
Sorry, that just isn’t the case. It is simple to design an allocator that can detect any double-free (by maintaining allocation metadata and checking it on free), and prevent any use-after-free (by just zeroing out the freed memory). (Doing so efficiently is another matter.) It’s not a language or GC issue at all.
It's not quite that simple if you want to reuse that memory address.
Not reusing memory addresses is a definite option, but it won't work well on 32-bit (you can run out of address space). On 64-bit you may eventually hit limits as well (if you have many pages kept alive by small amounts of usage inside them).
It is however possible to make use-after-free type-safe at least, see e.g. Type-After-Type,
https://dl.acm.org/doi/10.1145/3274694.3274705
Type safety removes most of the risk of use-after-free (it becomes equivalent to the indexes-in-an-array pattern: you can use the wrong index and look at "freed" data but you can't view a raw pointer or corrupt one.). That's in return for something like 10% overhead, so it is a tradeoff, of course.
Rust is a definite improvement on the state of the art in this area.
On the one hand, you might need to care more about what allocators you use for a given use case. On the other hand, you can make the allocator “conform” to a set of constrictions, and as long as it conforms at `comptime`, you can make guarantees downstream to any libraries, with the a sort of fundamental “dependency injection” effect that flows through your code at compile time.
struct AttackerChosenColor {
size_t foreground_color;
size_t background_color;
};
struct Array {
size_t length;
size_t *items;
};
int main() {
// A program creates an array, uses it, frees it, but accidentally forgets that it's been freed and keeps using it anyway. Mistakes happen. This sort of thing happens all of the time in large programs.
struct Array *array = new Array();
...
free(array); // Imagine the allocation is zeroed here like you said. The array length is 0 and the pointer to the first item is 0.
...
struct AttackerChosenColor *attacker = new AttackerChosenColor();
// The allocator can reuse the memory previously used for array and return it to the attacker. Getting this to happen reliably is sometimes tricky, but it can be done.
// The attacker chooses the foreground color. They choose a color value which is also the value of SIZE_T_MAX.
// The foreground_color *overlaps\* with the array's length, so when we change the foreground color we also change the array's size.
attacker->foreground_color = SIZE_T_MAX;
// The background_color overlaps with the array's size, so when we change the background color we also change the array's start.
// The attacker chooses the background color. They choose a color value which is 0.
attacker->background_color = 0; // Now say the attacker is able to reuse the dangling/stale pointer.
// Say that they can write a value which they want to wherever they want in the array. This is
// Like you suggested it was zeroed when it was freed, but now it's been recycled as a color pair and filled in with values of the attacker's choosing.
// Now the attacker can write whatever value they want wherever they want in memory. They can change return addresses, stack values, secret cookies, whatever they need to change to take control of the program. They win.
if (attacker_chosen_index < array->length) {
array->items[attacker_chosen_index] = attacker_chosen_value;
}
}Maybe they meant it zeroes out all the references on free? This is possible if you have a precise GC, although not sure if it's useful.
This is where the GC languages did OK.
Both the array length and the index can be computed at runtime based on arbitrary computation/input, in which case doing bounds checks at compile time is impossible.
After C, alongside TP, got a spot on languages accepted for game development, it took about 5 years for C++ to join that spot, mostly triggered by Watcom C++ on the PC, and PS2 SDK.
There was no GC overhead on the systems programming being done in JOVIAL, NEWP, PL/I,...
Realistically Rust is still the only memory safe language that you could use, so it's not especially surprising that nobody did it 18 years ago.
Just as importantly, there was also a shortage of memory safe languages that had good performance.
Granted those should also be rewritten in safer languages but often they're massive undertakings
Or generate C from a safe GP language, eg C targeting Scheme such as Chicken Scheme / Bigloo / Gambit.
People have been shipping software in memory safe languages all this time, since way before stack smashing was popularized in Phrack, after all.