Hacker News new | ask | show | jobs
by jandrese 1666 days ago
It's hard to fault a project written in 2003 for not using Go, Rust, Haskell, etc... It is also hard to convince people to do a ground up rewrite of code that is seemingly working fine.
6 comments

> It is also hard to convince people to do a ground up rewrite of code that is seemingly working fine.

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.

> 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...

> In my experience, porting code more or less directly from one language to another is faster and easier than people assume.

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.

> for example, that something intentionally reads from uninitialized memory.

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.

> 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.

OpenSSL's use of uninitialized memory to seed entropy was always a terrible idea. The PRNG was fundamentally flawed to begin with.

> 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.

If a programs reads from uninitialised memory, I hope for its sake that it does not do it in C/C++. Setting aside that uninitialised memory is a hopelessly broken RNG seed, or the fact that the OS might zero out the pages it gives you before you can read your "uninitialised" zeroes…

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.

Genuinely curious what the use case(s) of reading from uninitialized are. Performance?
It was used as a source of randomness. Someone blindly fixing a "bug" as reported by a linter famously resulted in a major vulnerability in Debian: https://www.debian.org/security/2008/dsa-1571
This is incorrect.

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.

> In my experience, porting code more or less directly from one language to another is faster and easier than people assume

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.

Sounds like a feature of the porting process. Not a bug. And I’d like to think that BoringSSL would be designed well enough internally to make that less of an issue.

I agree that this might slow down the process of porting the code though. I wonder how much by?

The article says Chromium replaced this in 2015 in their codebase. (With another memory-unsafe component, granted...)
BoringSSL started as a stripped down OpenSSL. That's very different from a ground-up replacement. The closest attempt here is https://github.com/briansmith/ring but even that borrows heavily the cryptographic operations from BoringSSL. Those algorithms themselves are generally considered to be more thoroughly vetted than the pieces like ASN.1 validation.
This sounds like a nightmare for any downstream users of this library. Any one of those bullet points in that section would be a major concern for me using it in anything other than a hobby project, but all of them together seem almost willfully antagonistic to users.

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!”

Note that the readme is outdated. With the upcoming 0.17 release, which is in the making for almost a year already, you can link multiple versions of ring in one executable: https://github.com/briansmith/ring/issues/1268

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.

Yeah, that is pretty wild. Total prioritization of developer convenience over actual users of the library.
Or rust-crypto
nss was also generally considered to be thoroughly vetted though
There’s a world of difference between ASN.1 validation and validation of cryptographic primitives. The serialization/deserialization routines for cryptographic data formats or protocols are where you typically get problems. Things like AES and ECDSA itself, less so, especially when you’re talking about the code in BoringSSL. Maybe some more obscure algorithms but I imagine BoringSSL has already stripped them and ring would be unlikely to copy those.

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).

For symmetric cryptography (ciphers & hashes), I agree. I'd say as far as to say they're stupidly easy to test.

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.

The Ring readme doesn't really cover its functionality but sounds like it may be a lower level crypto lib than NSS? And it also seems to be partly written in C.

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.

ring incorporates a barebones ASN.1 parser that also webpki uses, which is probably the crate you want to use if you want to do certificate verification in Rust. webpki is C-free but it does use ring for cryptographic primitives so that will have to be replaced if you don't like ring. More generally, I think Firefox wants to have great control over this specific component so they likely want to write it themselves, or at least maintain a fork.
Perl generated assembly. Hehh…
> 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.

Why? I don't get it. Maintenance of NSS has to be seriously expensive.

The NSA was caught intentionally complicating that spec. The idea was to ensure it was impossible to implement correctly, and therefore be a bottomless well of zero days for them to exploit.

Gotta love the US government’s war against crypto.

Sorry for sounding like a broken record, but source please?
Can you provide more information on this? I'd be interested to read about this topic.
He's confused it with Dual_EC_DRBG, a backdoored random number generator in a different non-international standard.

SSL is complicated because we didn't understand how to design secure protocols in the 90s. Didn't need help.

No; this predated that by about a decade. They had moles on the committees that codified SSL in the 90’s. Those moles added a bunch of extensions specifically to increase the likelihood of implementation bugs in the handshake.

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.

To be fair you don't need to rewrite the whole thing at once. And clearly the audits are not perfect, so I don't think it's insane to want to write it in a safer language.

It may be too much work to be worth the time, but that's an entirely different matter.

> may be too much work

I wonder, how many work years could be too much

(What would you or sbd else guess)

>seemingly worked fine

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.

>>seemingly worked fine

>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.

What's somewhat interesting is memory safety is not a totally new concept.

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?

Memory safe language that can compete with C/C++ in performance and resource usage is a new concept.

AFAIK ADA guarantees memory safety only if you statically allocate memory, and other languages have GC overhead.

Rust is really something new.

There's different classes of memory un-safety: buffer overflow, use after free, and double free being the main ones. We haven't seen a mainstream language capable of preventing use and free and double free without GC overhead until Rust. And that's because figuring out when an object is genuinely not in use anymore, at compile time, is a really hard problem. But a buffer overflow like from the article? That's just a matter of saving the length of the array alongside the pointer and doing a bounds check, which a compiler could easily insert if your language had a native array type. Pascal and its descendants have been doing that for decades.
> That's just a matter of saving the length of the array alongside the pointer and doing a bounds check, which a compiler could easily insert if your language had a native array type. Pascal and its descendants have been doing that for decades.

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.

[0] https://gcc.gnu.org/extensions.html

> 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.

> We haven't seen a mainstream language capable of preventing use and free and double free without GC overhead until Rust.

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.

> prevent any use-after-free (by just zeroing out the freed memory)

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.

One of the things I like about Zig is that it takes the memory allocator as a kind of “you will supply the correct model for this for your needs/architecture” as a first principle, and then gives you tooling to provide guarantees downstream. You’re not stuck with any assumptions about malloc like you might be with C libs.

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.

Zig is, however, not memory safe, which outweighs all of those benefits in this context.
I mean, if you don't care about efficiency, then you don't need any fancy mitigations: just use Boehm GC and call it a day. Performance is the reason why nobody does that.
Zeroing out freed memory in no way prevents UAFs. Consider what happens if the memory which was freed was recycled for a new allocation? Maybe an example will help make it clearer? This is in pseudo-C++.

    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;
    }
    }
> Zeroing out freed memory in no way prevents UAFs.

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.

The trick is not that the language support a safe approach (C++ has smart pointers / "safe" code in various libraries) in my view but simply that you CAN'T cause a problem even being an idiot.

This is where the GC languages did OK.

As far as I know, nothing in the C/C++ standard precludes fat pointers with bounds checking. Trying to access outside the bounds of an array is merely undefined behavior, so it would conform to the spec to simply throw an error in such situations.
There's address-sanitizer, although for ABI compatibility the bound is stored in shadow memory, not alongside the pointer. It is very expensive though. A C implementation with fat pointer would probably have significantly lower overhead, but breaking compatibility is a non-starter. And you still need to deal with use-after-free.
I believe that's how D's betterC compiler [0] works, whilst retaining the memory safe features.

[0] https://dlang.org/spec/betterc.html

> But a buffer overflow like from the article? That's just a matter of saving the length of the array alongside the pointer and doing a bounds check, which a compiler could easily insert

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.

What new concept? When C and C++ appeared they were hardly usefull for game development, hence why most games were coded in Assembly.

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,...

There were OSe written in memory safe languages before two persons decided to create a toy OS in Assembly for their toy PDP-7.
The issue isn't really that there was a shortage of memory safe languages, it's that there was a shortage of memory safe languages that you can easily use from C/C++ programs. Nobody is going to ship a JVM with their project just so they can have the "fun" experience of using Java FFI to do crypto.

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.

> The issue isn't really that there was a shortage of memory safe languages, it's that there was a shortage of memory safe languages that you can easily use from C/C++ programs.

Just as importantly, there was also a shortage of memory safe languages that had good performance.

AFAIK the issue with messaging isn't that the core app itself is written in an unsafe language , but that many components it interacts with are unsafe. E.g file format parsers using standard libraries to do it.

Granted those should also be rewritten in safer languages but often they're massive undertakings

In 2003 you could have generated C from a DSL, for one. Like yacc and lex had been standard practice (although without security focus) since the 80s.

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.

I think a good approach could be what curl is doing. AFAIK they are replacing some security-critical parts of their core code with Rust codebases, importantly without changing the API.
Modula-2 (1978), Object Pascal (1980), .....