BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Maintaining the Go Crypto Libraries

Maintaining the Go Crypto Libraries

Bookmarks
49:56

Summary

Filippo Valsorda talks about the challenges in maintaining and keeping the cryptographic libraries written in Go secure, safe, useful and modern. In particular, Valsorda discusses how security, scope and maintainer resources are on a balance, and what tools we can deploy to tip the scale.

Bio

Filippo Valsorda works on the Go team at Google, where he owns the cryptographic libraries and acts as the primary security coordinator. Previously, he worked at Cloudflare, where he built their DNSSEC and experimental TLS 1.3 stacks, and maintained their Go DNS server, which was authoritative for 40% of the Alexa top 1M. He built mkcert and the Heartbleed test, and writes at filippo.io.

About the conference

Software is changing the world. QCon empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.

Transcript

Valsorda: Today I'm here to tell you about the Go cryptography libraries. I'm Filippo [Valsorda] and my main job is that I'm the security coordinator for the Go project. I usually start with that because even if most of my time is spent maintaining the crypto libraries, I like to point out that the crypto libraries are just a way to guarantee the security of the Go ecosystem. The job is making sure that Go applications are secured and that job includes, for example, responding to reports to security at golang.org. There's a whole group behind that email but I'm usually the person that will do triage.

Cryptography Is Hard

As for cryptography, you might already know that it’s hard - capital H Hard. These can mean different things depending on who you're talking to because cryptography itself can mean a few different things. Today, we're not going to talk about cryptography design, we're not talking about what academics do when they design the next AES or the next SHA and they have to think about the next hash function and they have to think about things like differential task or anything like that. We're going to talk about what people like us do, we're going to talk about cryptography engineering.

Cryptography engineering is, of course, more fraught than other kinds of engineering. That makes it even more important to always keep an eye on complexity when developing. Cryptography comes with complexity in itself because the subject matter is complex, you are mixing math with dangerous security requirements. What I want to tell you with the talk today is that cryptography engineering, like most engineering though, is an exercise in managing that complexity.

Cryptography engineering, indeed, is special mostly because a single mistake can make your entire system useless. Cryptography is a tool to achieve security but if you get anything wrong in the cryptography implementation, that's going to not deliver the security it was meant to deliver, and it will become useless. This is a bit like saying that if you get the lighting wrong on a bridge, it's going to collapse.

When we have that level of assurance requirements in software engineering, we usually focused on testing as a way to gain confidence in the implementations. Cryptography engineering is particularly tricky there too because you can test everything that your users will do, you can test everything that your application will do, you can test everything that any reasonable member of your ecosystem will do, and still not deliver any security with your cryptographic implementation. The attacker will be more than happy to figure out what things you didn't test because nobody in their right mind would send those inputs and proceed to use those to break whatever security property you are trying to deliver.

The most important tool in cryptography engineering is managing complexity and this complexity can be above your API surface where you're managing the complexity in your APIs, in your documentation, in what you provide to users so that they use your libraries safely and correctly, and below the API surface where you don't want to implement things that will bring in so much complexity that you can effectively guarantee the security of what you're implementing.

These two have a little different dynamics and the failure mode in both is different. If you fail in managing complexity above your API surface, what you get is that users will roll their own crypto.

We've all heard "Don't roll your own crypto" as gospel and while it's a right advice, I often feel like cryptographers like us use it to gain a moral standing over developers that are just trying to roll their own crypto, not because they want to, but because what we provide wasn't easy enough. I don't believe any developer starts the day thinking, "It would be really nice to figure out my own composition of ciphers today." They open maybe the docs of whatever library they have available, decide that this is all way too much, that it's not clear how to use this. Then they search on Stack Overflow, and say, "It seems easy enough to bang together AES and HMAC, it will work out."

Yes, you shouldn't roll your own crypto but in the first place, you shouldn't feel the need to. This is about cryptography library maintainers managing the complexity we expose above the API surface. Below the API surface, the same refrain, "Don't roll your own crypto," sometimes lead to, "But you're not supposed to understand cryptography code. If you read my code and it's all too complex to understand, it's just because you're not a cryptographer, ok?" That's not how it works. Cryptography code can be readable. The cryptography code actually should be more readable than any other code base exactly because the complexity is intrinsic in what we are implementing, we need to strive even more to manage our complexity below the API surface. The job is harder, so we need to do a better job at making it manageable.

The Go Cryptography Libraries

From this understanding of cryptography engineering is how I maintain the Go cryptography libraries. For context, Go has made a different choice compared to most languages, which usually come with links or wrappers for OpenSSL or simply don't provide any cryptography in the standard library. Rust doesn't have standard library cryptography, it has very good implementations in the ecosystem. JavaScript only has web crypto, but it depends on what platform you're running on. Python doesn't come with a crypto standard library. Go does; you'll find anything from a TLS stack implementation for HTTPS servers and clients all the way to HMAC or any other primitive that you might need to make signatures to verify hashes, encrypt messages.

Outside of the Go standard library, we have what we call the supplemental repositories. We usually call them the X repos because you fetch them from golanguage.org/x/crypto, for example, the cryptography one. This brings additional implementations and it has all sorts of things that usually are a bit more modern or a little less often used. The only main difference these days, it used to be that these were allowed to be less stable or less things. That's not true anymore, I honestly gave up on that about a year ago. The distinction these days is just that the standard library gets released along with the language, Go 1.13 will come out soon, it will bring out new rotation of these packages.

These packages are just a Go module that you install an update just like you installed an update on your normal dependencies. I think that the decision to implement cryptography libraries in Go has a number of reasons, some of them are very specific to Go, like cross-compilation requirements. What I want to talk more about is how in the other direction, Go is a good language to implement cryptography. If you know a bit about Go, you might imagine the standard reasons for that. Go is memory safe, so you don't risk buffer overflows and vulnerabilities like that. Heartbleed, the vulnerability in OpenSSL, could not have happened in Go because there is no way for other to run buffer.

Performance, of course - Go is not as performance as your C compiled by LLVM might be, but it can still deliver a level of performance that is usable. While writing the core of your cryptography in Python might not deliver something that you can actually use in production, writing it in Go, many times is sufficient. Go binaries are reproducible, which is a property that I really care about because it means that every time you build the same program in Go twice, even on two different machines, as long as you're targeting the same architecture and operating system, the binary will be identical even if your cross-compiling. It will keep being identical forever, so it doesn't matter when you compile it, as long as you use the same version of the compiler, the same version of all the dependencies. This is important in security because it means that you can verify that a binary comes, indeed, from the source it says it comes from.

Finally, Go is fairly easy to statistically analyze. These are the reasonably obvious reasons for which Go is good for cryptography but the ones I found are more important are the same reasons that Go makes for good language for a number of different users. Again, implementing cryptography is inheriting the early complex, so the fact that Go is a clear language is explicit, it always has explicit control flow, you can always tell what is going on in the language, there is no operator overloading.

When you look at some code, you can be sure what it does. When I look at some signing verification, I don't have to worry that an exception will happen and then get quote somewhere else and skip a verification check or anything like that. Documentation is extremely easy and very well built into the culture. In Go, you just document things by adding comments to the export and symbols, which you might be familiar with if you use tools like Oxygen, but there's also very good centralized tooling for showing this documentation.

Finally, there’s something that you might be confused about, "Wasn't this about cryptography, why are we talking about code for matters?" One of the reasons I enjoy writing cryptography in Go is that there is a standard code for matter, go fmt. Basically, everyone in the community formats their code the exact same way, tabs instead of spaces, which might be controversial. It might not even be what I used to do but at least it's always the same. When you're looking at two pieces of code and trying to work out with the difference is, trying to figure out why one piece of code is working and that one isn't, you always feel you're in unfamiliar territory. All of these things reduce the overhead and the complexity of programming in the language, which is important when the baseline complexity is already so high because you're implementing something complex.

All of this works out, the Go cryptography library has been solid for years now from before my tenure too and it's been used in production in a number of places, it's used in Kubernetes, it's used in Docker. When I was a co-author, I used it to implement the TLS 1.3 prototype that run on the whole CDN for all the clients. There was no NGIX in the middle, there was no OpenSSL, we run Go straight on the edge. This helps the language succeed because some of the most common use case of the Go language. Making microservices and having an easy to use HTTP, TLS, HTTPS library, made it very easy to make self-contained services that work securely on your network.

However, it's important to keep in mind what the goal of implementing this library was. The Go project is not in the business of building a cryptography library. The goal is keeping the ecosystem secured, the goal is providing security for Go programs for Go developers. The cryptography libraries are just a tool for that Goal, so my role is to develop a good cryptography library but that's because I am trying to keep the ecosystem secured. It's important in this to manage complexity on our side so that developers that are implementing something that requires HTTPS, something that requires signing, something that is requires encryption, don't have to manage that complexity themselves.

This is important for anyone depending on the code they make. Any complexity you don't address in your own work is complexity that you are going to delegate to your users. In open source project, it's clear: if I don't take care of complexity and just push it outwards out of the API surface, the millions of Go developers will have to handle that complexity. It scales all the way to work projects where someone will use your library. That someone might be you later, it might be the team next to you, it might be your team. Anytime a library does not actively manage the complexity it's exposing, it's delegating that complexity downstream to whoever using it.

This is, of course, hard to action. This is maybe a lofty goal, but some of the challenges that I encountered when I joined is that the crypto libraries were developed by Adam Langley. He did a fantastic job at building them and I liked that they're not complex, I like that they are safe into how do I decide every day what to do and what not to do.

Until last year, I worked on defining the principles that defined this philosophy more explicitly and I came to a conclusion that the Go crypto library is trying to be secure, safe, practical, and modern. I say them in order because I think it's important to know what the priorities of where you're going with your project are. Secure is obvious, we don't want to have vulnerabilities. Decisions on it are easy; if there's vulnerability, we fix it - easy. Not easy to do maybe, but easy to decide, easy to think about. I like to think of secure also meaning as if we can't guarantee that something is secure, if we can't figure out how to test that, we should not merge it.

Equally important and often overlooked is that they should be safe. Safe here means that they are hard to use incorrectly, that they're not just possible to use securely but that developers will use it securely. You will say, "You can't go and make people use your libraries correctly." That's true, but I also can ask them to learn cryptography to use my libraries. If people need to understand cryptography to use the library, I failed. Safe means that default should be safe, it means that documentation should be clear about what you can and can't do and what security it provides them and what it doesn't. It means that if there is an insecure option, for example, if you really want to turn off certificate validation in TLS, it should be obvious that you're doing that so that you or your code reviewers can spot it. Something that I am applying as a hard rule now is that anything insecure needs to shout it from the API.

I like short names, Go has a tendency to make shorter names than Java or other languages, but I will go out of my way to put insecure, legacy, superfluous even, in the exported APIs when I think you should make an active decision to opt into something that will be in your code. I want everybody who uses a weird version of CATX that was never standardized to have to put a comment about it that says, "Yes, I know it says legacy but I'm sorry, this happened, we have to." Cool, we're all adults, we're all developers, we can make our decisions but I want these decision to be explicit and opt-in. The option to disable TLS certificate validation is called InsecureSkipVerify.

Next, practical, because at some point, you have to acknowledge that you have a job to do, you can't just make the perfectly secure, perfectly safe empty library. Zero lines of code, zero vulnerabilities. I call it dangerous because it's easy to call everything useful or everything practical. If someone comes with a feature request, it's because they need it. Sometimes they might be misguided, and they might need something else but oftentimes when a developer comes to you and wants to implement something, that is a valid use case. That's why they're in order, they're in order because I want to be able to say, "Yes, this sounds useful, but I don't think we can implement it securely with the resources we had," or, "Yes, this sounds useful but it will not at all be safe because people would turn it on without realizing" or "People would have to turn it off to be secured and that's not an option, defaults have to be secured."

We picked practical instead of useful because you have to decide what your use case is at some point and what you're trying to enable. The job is to let Go developers develop secure applications effectively but at some point, there will be something that is too niche. I'm a big fan of providing hooks, callbacks, ways for people to plug in their own weird things when it's safe to do so but in general, you have to decide what your scope is and this will come up again because it's critical to managing complexity.

Finally, modern, I wanted to finish it on a note up because you can keep it very secure and very safe by never moving and stay in the exact same implementations as 15 years ago and then Libsodium happens, as it should. Libsodium is a C library that I truly recommend if you are working in the language that has bindings for Libsodium, which came out as a safer, more modern, more opinionated C cryptography library. I consider my job to make sure that Go never needs Libsodium, that we can keep adding the new things that people need, the more modern, the better alternatives: TLS 1.3, SHA-3 – controversial. Adding the things that people need in order to stay up to date with the state of the art.

This is also, by the way, why the Go cryptography library is not FIPS validated. FIPS is a certification that moves necessarily slowly because it's a government certification and sometimes the cryptography engineering community figure out a way to do something better. For example, ECDSA signatures - it turns out you shouldn't really do them with random IDs with random values. We learned that lesson painfully, lots of bitcoins were stolen, but now we know that it's better to do it differently. FIPS requires a certain way of generating randomness. The problem is that you can't stay modern when you also have to move at the pace of certification which, of course, it's not their fault, certifying something is hard.

How the Go Cryptography Libraries Are Different

You might be noticing a pattern of trying to do something a little different from other cryptography libraries. All the principal sound intrinsically good, nobody will come in and argue, "No, you should not make a safe library," or, "No, security is not that important," or, "I don't know, modern sounds bad to me."

However, in practice, where we are different is how we prioritize things. You might have noticed that nowhere on the principles there was performance, for example. Performance should be sufficient such that it will be practical, because if it's too slow to use it, you're going to roll your own or you're not going to use it, so it's not being practical, so it's not being useful. As long as performance is sufficient, I don't care if we're 20% lower than OpenSSL, that's not a priority of the Go crypto libraries. Universal support is not a target, I don't care if there are some ciphers that we don't support as long as in practice, you get to talk with most of the TLS servers in the world. Yes, you don't get to choose all the different ways to talk to the servers but as long as you can get your job done, once that's taken care of, which, of course, is about practical, it's not a priority to having implemented everything, just like it's not a priority to implement uncommon use cases. Go has good support for modules these days, we're rolling it out. It has a very good ecosystem of really modern software, so you can go and use something more tailored if you have a long tail use case.

Instead, priorities are readability, because the thing we're implementing is complex, so we need to keep the code as readable as possible. We have to reduce the overhead complexity to be able to fight this fight and truly, this is not just about cryptography. You might have noticed by now, this talk is a ruse to convince you to manage complexity in your own code bases because I don't think cryptographers are the only ones that have to implement something complex. Sometimes you have complex rules to interact with legacy clients, sometimes you have complex database algorithms to deliver the performance you need, sometimes you have complex interactions with hardware. Sometimes you just have complexity from the platform you're running on, I find programming for the web extremely complex myself. It's important to fight that complexity by managing it everywhere you can, removing the complexity of how the code reads so that you can focus on what the code is saying.

Safe defaults are important in and out of security context. Then, good guidance, documentation examples. People want to copy-paste, everybody will copy-paste, everybody will copy-paste how to use the crypto libraries and it will either be from the examples I write or from Stack Overflow. That will happen also for your libraries because as long as you are not going to be the only one who ever uses your libraries, somebody will want to copy-paste an example of how to do it. Examples are extremely important.

Now, let me show you an example of what I mean with not implementing everything. This is the list of OpenSSL supported ciphers. There are things in there like Camellia, Idea. There's more that I don't actually remember the names of. Those are names of ciphers that you probably never heard. Who had ever heard of Camellia? One hand? Yes. Right there, implemented, negotiable, when you try to look to a server that has all ciphers enabled, that's in that. These are the cipher suites that crypto TLS implements and six of those are not enabled by default and you have to explicitly go out of your way to get them to your program. Essentially, you just use three variants with a bit of combinations of certificates.

This is to say that most of the value of the Go cryptography libraries is in what we don't ship. We have good implementations, there are people that contributed really fast implementation of a number of things, I'm really happy about it but what I'm really happy about is all the things that are not in there. All the configuration options that are not in there, all the decision you don't have to make when you use the library. Again, any library can benefit from this. A library that when you use it doesn't ask you many questions and does the thing well enough that you can carry on with your day is a delight to use.

In particular, I always call out that I never want to add knobs. We are developers, we always want to solve the problem by adding another argument to that function, add another value to that enum, add another option to that interface. It's an easy way to solve a problem on the spot and if feels good because we are problem solvers. That's what feels good about programming, there's a task and then we get it done. Someone comes and they need a semantic that is slightly different from the one you already have, "Can you just add another option to enable that semantic?" You go in there and you know how to do it, so you just write 10 lines and you're happy and you satisfied the ticket, fixed.

You do it once, twice, thrice, you do it 10 times and then you have a legacy piece of software and now you want to rewrite it because you want to just bring all of that together. Refusing knobs is, I think, the most important baseline for decisions I make in the crypto libraries.

Then, making a curated selection of features. When I implement a cipher, I want it to be the best cipher for you because I don't think you care what cipher you encrypt something with, you care that it's encrypted security. And that it will interoperate well with other things, of course, and that it will be performance. These are decisions that you have to spend a lot of time learning what's in my brain to then make them yourself if I gave you all the options and no guidance.

Instead, I want that if you see something in the libraries, "Ok, if it's not marked as deprecated, I don't have to worry about reading papers to find out if this was broken in the last five years or not." I think that the most value we deliver is by curating. We deliver value by resisting adding things, by implementing 80% of the features with 20%, 10%, 5% of the complexity. To do that, you have to know your community, you have to know your use case, you have to know your downstream. That's true both of, of course, open source projects that need to know who their community are, but also of your work projects, you need to know who is using your library and what for.

How I would like to word it is that maintaining a cryptographic library is an exercise in resisting complexity. Complexity always trends up, new features are added, things get broken and need workarounds. New research is done, new, better, modern things are added. Clients come out that don't really interoperate with your thing, unfortunately. Complexity always turns up, the job of an owner of a library that's used by many downstreams is to resist that complexity.

I like to think in terms of complexity multipliers. Every time I accept some complexity into the libraries, I am removing a bit of complexity from whoever is asking me for that feature that would have had to work around it in Slack or implement it themselves, and I am placing a little complexity on everybody else. This, of course, changes depending how wide everybody else is. For the Go standard library, it's 2 million developers, so every time I take even a little bit of complexity, I am adding complexity below the API surface maybe, but for everybody that uses the library.

How the Go Cryptography Libraries Stay Different

Finally, I want to talk about how we make sure that the crypto libraries stay like they are. I told you what the principles are, I told you what my goal is, I told you how I want to resist complexity and how you should do. Now I'm going to talk a bit about how in practice we fight that. The cool thing we're up against is that there's a fundamental symmetry. It can take 10 times more time to review, maintain, verify all of that, and periodically fix a piece of code than it takes to write.

I'll be honest, I love writing cryptography code because you get to do a thing that, intuitively, you're not even that convinced it should work. There are things in cryptography that I'm still not entirely convinced are real, like zero-knowledge proofs, you can't prove something about a statement without saying it. Ok, I don't entirely have an intuition for the fact that it works but then you write it and it interoperates with the thing, it does the thing and you know from the paper that it has some properties and you can see it and that's a thrill. I'm sure different people like writing different code, but writing code is fun and it's the easy part of it because it has a clear success result, it runs, it passes the tests, it does the thing.

Most of the job is after, most of the job is getting it through code review, most of the job is reviewing it yourself or getting other people to review it and their time in reviewing it, maintaining it later, carrying that complexity for the rest of the life of the project, or until you get to make the much harder push to remove it.

This is especially true for things like assembly code. Sometimes, the Go compiler is not smart enough for delivering the performance we would want. For example, the Go compiler does not have auto-vectorizer and what a vectorizer does is just recognize that you're doing something in a loop over, say, four integers and realize that you could just pack those four integers into a wider register and then just do the thing one quarter of the times over that wider register instead, vectorization.

It gets wider and wider, the newer CPUs make these registers more and more wide and they take more and more power and gets more and more complex to decide whether to use them, but the core point is that they can make your things six times faster. This is a PR to make ECDSA, elliptical implementation six times faster on a specific architecture by rewriting it in Assembly. It's 2,500 lines of handwritten Aassembly. The reason is that when you write in Assembly, you have to do things like unroll loops yourself. Remember that loop we said we can do one quarter of the times? Yes, but you have to write it manual each time because there's no compiler to do that for you when it delivers the performance gain. Frankly, we don't have the maintainer resources to review 2,500 lines of Assembly, or at least not on a regular basis. If it happens once a year, that's doable, but that's not sustainable.

Here is a lesson that is hard to admit because all of us have an idea of what we want our project, our service, our system to look like in our head and we know what we would want it to be if we had unlimited resources. We don't want to admit that it's not perfect, that it's the best it could possibly be because we don't have the resources to make it that good. That PR could probably be made secure if we had more maintainers to review it, if we had people that could specialize in PowerPC architectures to verify it but the reality is that what secure is, is relative to your maintainer resources. Complexity that you can take is relative to the maintainer resources.

What we can secure is not an absolute, a PR is not secure or insecure, there are some MLT or there is not. It depends on whether we have the resources to keep it secure in the future to other than now, to improve it over time so that we fight that trend of complexity that goes up, to fix bugs when they come out and to apply new testing techniques later.

To fight this, I've been working with policies and making, for example, a policy that if you write Assembly, I need you to write as few as you can and explain why you wrote Assembly and to comment it well and make tests. This was a failure, this didn't work. That's because it was not commensurate to our maintainer resources. This made it harder to write Assembly, but it didn't make it easy enough that we could review it confidently.

There is a new version of the Assembly policy that is going to come out after we discuss it with the contributors that do make that work but I'm going to give you an advanced peek. It says things like, "You have to code generate this." I don't want to see just handwritten Assembly, I want to see a tool that generates Assembly so that if you want to write the same thing 15 times, changing only one thing at a time, you can write a program that does that and then I can review the program and say, "Yes, it always prints the same thing and it just changes this register, it sounds good to me." I wanted to have small units so that we can aggressively fuzz each operation.

For example, there was once a bug in an elliptical implementation in Go where subtraction was broken - subtracting two numbers. It took forever to find it because to hit the right inputs to break that function from the way larger dual complete elliptic curve operation function, it would take 2 to 40 operations. We eventually found it, but it would have been found much faster if we had 50 lines for each units to test, so that's going to be a requirement. There's going to be a requirement of reference Go implementation for two reasons: because I want to understand what you are trying to do and, of course, it's easier to read Go than it is to read Assembly, and because it's where you document, "Here is where to Go compiler does not do the thing that I do manually in Assembly."

This is, of course, a lot more work but this is a way to fight complexity and it's a policy which is commensurate to the maintainer resources. Just like what is secure and what is not is relative to the maintainer resources, policies need to be relative to maintainer sources too. Policies are not the only way to approach this. You can often make core changes that reduce complexity in everything else that can use them.

For example, one reason that a lot of implementations were using Assembly was not to do weird vectorization things, it was to multiply two uint64. When you multiply two uint64, you get a uint128, double wide. The Go language does not have a uint128 because our architectures don't have it and so, it's lower to do it in Go in two house and two parts than to do it in Assembly. This is a new intrinsic that is implemented by the Go compiler that just tells it, "Multiply these two uint64 and put in this two output of uint64." That made a bunch of Assembly implementations be not that competitive against the Go implementations that use these.

Then there is tooling; fuzzing, mutation testing, we are working on that soon where since you can't really tell with constant-time Assembly what is covered - code coverage is nice. The problem is that constant-time Assembly, by design, all runs all the time, so you don't know which operations are being done or skipped because they all execute. Instead, mutation testing, you take an Add with Carry, you change it into an Add, and if your tests don't break, you're not testing the Carry.

Then, things like reusable tests, I'm a big fan. If you are the maintainer of a project that requires an interface, defines an interface, like the crypto.Signer. You can write tests that just test the interface behavior and then anybody that implements that interface will have very good tests and you will have a cleaner ecosystem.

The last part about this effort is that this is a community management job. Whether your community is your users, colleagues, customers, or anybody who has requests for your code. In Go, we have a proposal process; you just open an issue and you call it proposal column and it says, "I would like the crypto libraries to do this and that." What's that about is delegating complexity, someone is asking you to take complexity of them into the project. The problem is that now you're taking up that complexity and every other user will suffer at the same time. This is why I like to say that everyone wants their proposal accepted and everybody else's rejected because they understand that their proposal being accepted will reduce their complexity but everyone else's proposal that gets accepted will increase the complexity of what they depend on.

This is always a judgment call, it's extremely hard to make clear cut policies for what goes in and what does not. At the end of the day, a lot of the job is also understanding your community, understanding your users or customers, and understanding what things you do need to take up from them and what things you can justify not doing and justify it, explain to them you do like low complexity, so here's how we maintain it.

Finally, the most satisfying kind of software engineer - removing things. In Go, we can't remove things per se because we have a compatibility promise. When you update Go, nothing breaks, that's our promise. We don't remove APIs, we don't change behaviors of exported APIs. Instead, we deprecate, deprecation are very formal things where you add that comment that is machine readable and a common line in linter tools will warn anybody who is importing this package, "You are using something deprecated." In case of cryptography, I went on a deprecating spree and went to remove a bunch of things that you don't want to use for one reason or the other, whether they are not safe, not secure, or not modern.

Sometimes you can't just deprecate, it's a matter of deprecating and replacing. The curve25519 API we had did not return an error because in most cases, you don't care about malformed inputs, except sometimes you do and that's not safe, that's not a safe default, it's leaving edge case in which it's not secured to use the library. We are deprecating the old API and replacing it with a new one that always returns an error so that by default, it's secured.

There are things that we get to remove like SSLv3 which has been completely broken for 5 years and been deprecated for about 20. This is something that we will remove but we'll remove in stages, it will be first deprecated for a release and then six months later we'll remove after getting that feedback from the community. In its place, you get things like TLS 1.3 because when you recover complexity budget by removing things, you get to spend that on more modern things.

Conclusion

In conclusion, what I want everybody to take away from this talk aside from the fact that Go has a cool set of cryptography libraries, is that every project has a complexity budget. It's how much complexity you are taking up before it becomes an issue whether you acknowledge it or not because, yes, cryptography libraries start with a very complex baseline, but every project has a budget. Our budget is smaller, because we have to spend so much of it on the actual algorithms. Even if you get more slack because you have more understandable things to implement, it's important to make sure that you don't let complexity that trends up overshoot your budget.

You should actively manage your complexity budget, you should know what it is, you should know how much you have left, and you should know how everything that you implement is going to cost you. That famous mythical prototype that we will replace in three months does not exist.

Questions and Answers

Participant 1: As someone who dibble in open source, I mean, [inaudible 00:46:37] Go libraries myself, how much time do you actually spend, because as programmers, we like to learn new things and write them, so what's the ratio or number in actually talking and comment other people's pull request and accepting or rejecting them versus writing your own fixes or new implementation inside the libraries?

Valsorda: The question was, if I understand it correctly, what's the ratio between how much time you spend discussing other people's changes or reviewing versus making your own changes. I think cryptography is a bit particular because I spend a lot of time talking about other people's requirements but not really their code. Assembly implementations - yes, a lot, but aside from that, I write a lot of the code that eventually lands. That's different in other open source projects I manage because they have wildly different maintainer resource budgets and complexity budgets. They can spend more complexity, because they don't have the maintainer resources to reimplement everything or discuss everything at that much length.

Participant 1: Just spend more time at other people's pull request?

Valsorda: Yes.

Participant 1: They can't have that because they don't have their own...

Valsorda: Mostly because they will come with a PR more often than on the crypto libraries, but this is really depends on your project. Something that you might have noticed is that I did not really discuss what complexity is because that's subjective. What I want people to take away is that it's something you should be actively managing, so what I want you to do is ask that question even if I don't have the answer. I want you to think, "Should I be spending more or less time on this or that? Should I be pushing back more or less? Should I be taking these changes?"

Participant 2: Has anyone built a PKI in your language?

Valsorda: What do you mean a PKI?

Participant 2: Like a CA.

Valsorda: TLS in x509 packages support the web PKI, that's their main goal. Let's Encrypt CA, the free automated one, their software called Boulder is written in Go, so that's what powers the biggest CA on the internet. They have HSM for the actual keys but everything built around that is in Go. There are also other PKIs that run on Go, I think that we often see issues with the Estonian ID cards because I think Go is involved in the Estonian election process, which is terrifying. But I'm very happy that it is and I don't have a reason to think it's a bad choice at all, just my job.

 

See more presentations with transcripts

 

Recorded at:

Oct 28, 2019

BT