Part 2 of 4 · Simplicity Is Security
Simplicity Is Security: Less Is More
Choosing what not to build is a security decision.
On May 5, 2014, Ted Unangst removed TLS Secure Remote Password (SRP) support from LibreSSL’s libssl code. He wasn’t fixing a known vulnerability in SRP. He was removing functionality that had become too difficult to justify and audit. On July 2, OpenSSL privately received a report of a crash-causing vulnerability in that same TLS SRP code — the very code LibreSSL had stopped shipping it almost two months earlier.1
Security work can often mean finding weaknesses and adding defenses. But every supported feature, dependency, and operating mode adds something we must understand and defend. Instead of responding only with more tests and more controls, we should also ask which capabilities we can cut.
This is part two of four in Simplicity Is Security. It makes the case for reducing unnecessary behavior, not for counting deleted lines.
A smaller target
OpenBSD’s developers created LibreSSL after the Heartbleed vulnerability exposed serious problems in OpenSSL. Their response wasn’t limited to repairing the famous defect. They examined obsolete compatibility code, optional features, and difficult interfaces, asking what was worth retaining. The SRP implementation illustrates why.
SRP wasn’t sitting in an independent corner of the library where a reviewer could examine it and move on. Conditional-compilation directives and nested branches ran through functions responsible for deciding how client and server exchanged keys. As Unangst described it, auditing those thousand-line functions had become impractical.1
A reviewer couldn’t simply understand the ordinary TLS exchange and then examine SRP separately. The optional feature changed the logic of shared code. Its presence meant considering different compiled configurations and negotiated behaviors, including interactions with functionality unrelated to SRP.
The story didn’t end with the July 2 report. The SRP implementation in libcrypto had initially remained because it wasn’t directly obstructing the cleanup. Unangst removed it on July 28. Three days later, researchers discovered a remotely exploitable buffer overflow in that code too.1 These were distinct vulnerabilities in different portions of the implementation.
Removing SRP didn’t prevent Heartbleed, which had already occurred. Nor did it solve the problem for users who depended on SRP. They still needed a working implementation, and OpenSSL’s first attempt to repair its SRP vulnerabilities broke legitimate connections. A corrective patch followed after users reported the regression. LibreSSL, meanwhile, no longer had to maintain those paths or review their interactions with the remaining code.
Unangst described the aim as presenting a smaller target. That target wasn’t merely a count of source lines. It included the combinations of features the library could execute, the mistakes those interactions might conceal, and the burden of understanding the additional code in order to maintain it securely.
Not quite dead
SRP wasn’t dead code. It was a supported feature, even if relatively few applications needed it. As long as it remained available, the library had to account for its behavior, and attackers could potentially exploit mistakes in its implementation.
Developers frequently use static analysis tools to identify unreachable code. If a function truly cannot execute, defects inside it cannot be triggered through the program’s ordinary execution paths. But determining reachability isn’t always straightforward. Dynamic registration, external entry points, and build configurations can make code reachable through paths an analysis can’t account for. And code that hasn’t executed during testing or normal operation may still be reachable by an attacker under the right &mash; or wrong — circumstances.
Security researchers have examined what happens when we remove code that an application doesn’t need. In 2019, Babak Amin Azad and colleagues studied software debloating in four PHP applications. By removing unused files and functions, they produced applications averaging 46% less code and about half the original cyclomatic complexity. They also removed code associated with dozens of reported vulnerabilities.2
The researchers extended this work in 2023 with AnimateDead, which used program analysis to explore code reachable from observed application entry points, considering different inputs and database states. Their experiments reduced code size by an average of 47%, eliminated 55% of critical API calls, and removed code associated with as much as 65% of historical CVE vulnerabilities.3
The distinction matters. Code that cannot execute through an application’s supported paths presents a different risk from code that simply doesn’t execute very often. Removing the former may make the code easier to reason about. Removing the latter can also eliminate behavior an attacker might exploit.
Of course, deciding that code is unnecessary requires understanding who might still depend on it. A rarely used operation might be essential to someone. Execution logs and static analysis are evidence, not proof that a capability can be removed. But once we establish that the behavior is no longer needed, removing it accomplishes something more than deleting source lines. We eliminate code we have no valid reason to execute. We might even eliminate invalid reasons.
And we make the code easier to reason about.
Security lost behind a switch
Configuration can preserve unnecessary and insecure behavior in a less obvious form.
Suppose a Go client exposes a skip_verify setting wired to tls.Config.InsecureSkipVerify. Without custom verification, enabling it skips verification of the server’s certificate chain and hostname.4 The connection can still be encrypted. What it no longer establishes is whether the peer is the server we intended to contact. An active intermediary may be able to intercept the connection without the client detecting the substitution.
This differs from choosing a server address, timeout, or trusted certificate authority. Those parameters can describe legitimate deployments while preserving the requirement to authenticate the peer. This bypass creates another operating mode, one in which that requirement no longer holds. A successful request in that mode may look reassuring precisely because the check that would have rejected it never ran.
Removing the setting can be stronger than documenting why nobody should use it.
Of course, deleting the option may break installations that depended on it to conceal a missing private CA or a certificate issued for the wrong hostname. Those installations need their trust configuration repaired, not merely their application configuration changed.
The successful migration establishes two things: connections using the intended certificates succeed, and connections with untrusted certificates or incorrect hostnames fail. This is clearly more secure than a fallback that silently retries without verification.
Go does supports custom verification callbacks, so the presence of InsecureSkipVerify alone doesn’t establish a vulnerability.4 The question is whether the connection authenticates its peer, not whether a particular Boolean appears in the source.
Some configurability is necessary. Different deployments have legitimate needs. But a security-bypass switch deserves more scrutiny than an ordinary deployment setting.
Safer APIs remove obligations
Removing a feature is one form of simplification. Removing a decision that every caller must make correctly is another.
Unangst described a related ambition later in his 2014 LibreSSL presentation. He and another developer were working on a proposed replacement for the OpenSSL API called ressl, short for Reimagined SSL.1
The idea was to design the interface around what an application developer wanted to accomplish, rather than expose all the machinery the TLS protocol made available.
Most application developers don’t want to manage X.509 structures or reason about certificate extensions. They want to establish a secure connection to a server, or accept one from a client, and exchange data over it.
One of the proposed interface’s important features was hostname verification. Establishing an authenticated TLS connection requires validating the certificate chain and confirming that the certificate identifies the server being contacted. Applications using older OpenSSL interfaces could perform certificate validation while forgetting the separate hostname check. The proposed ressl interface required the caller to provide a hostname and enabled its verification by default. The ordinary path to a connection therefore included an operation that application developers had frequently omitted.
The proposed interface became libtls, a library included in LibreSSL and used by several OpenBSD applications. It kept the underlying TLS implementation but provided a simpler interface for applications. Certificate-chain and hostname verification became part of the normal connection process rather than separate obligations every caller had to remember. Insecure configurations remained possible, but they required explicit choices.5
The design principle survived the experiment: a developer asking for a secure connection shouldn’t have to assemble one from a series of independently optional security decisions.
The same idea appears in a smaller C library interface. Consider realloc(p, count * size), where count and size are represented as size_t. The multiplication happens before the function is called, so the allocator sees only the result. If the unsigned product overflows, it wraps, and the allocator may receive a size smaller than the caller intended. A successful allocation can then be too small for the writes that follow. Checking that realloc returned a non-NULL pointer doesn’t establish that the multiplication was safe.
OpenBSD’s reallocarray(p, count, size) accepts the operands separately and checks their product for overflow.6 The caller must still supply sensible values and handle allocation failure, but the overflow check no longer depends on every caller remembering to implement it.
This is a useful correction to the idea that programmers simply need to be more careful. They do need care. But when an interface repeatedly invites the same mistake, that tells us something about the interface. We can keep asking each programmer to remember a subtle rule, or we can put responsibility for that rule in the operation that needs it. The implementation may become longer. The application becomes simpler because it has one fewer condition to establish independently.
More code in the allocator reduces the defensive code required, and makes the final software more secure.
Less may be more, but is it better?
Later in his LibreSSL account, Unangst explained that the team had mostly stopped deleting code. There was still difficult code left, but it served functionality they needed.1
The strongest objection to less software is straightforward: removing code can remove protection. Delete a bounds check and the program gets shorter. Add a validator that enforces a necessary rule and it gets longer and safer. Introduce a separate process that confines privileged operations, and the architecture becomes larger while the security boundary becomes stronger. None of those decisions can be settled by measuring the diff.
The examples here show three different ways to simplify a system. Retiring an integration removes a capability and the machinery that supports it. Eliminating a security bypass removes an unsafe operating mode. Designing a safer API removes a recurring obligation from its callers.
All three eliminate code an engineer would otherwise have to understand, test, and defend.
-
Ted Unangst, LibreSSL: More Than 30 Days Later, EuroBSDCon 2014. The presentation discusses the SRP chronology, configuration complexity, the proposed
resslinterface, and the team’s move from large-scale deletion toward incremental maintenance. It dates removal of TLS SRP fromlibsslto May 5 and receipt of the nonpublic crash report to July 2; removal of SRP fromlibcryptoto July 28 and discovery of a remotely exploitable buffer overflow there to July 31. Theresslinterface was still under development at the time of the talk. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ -
Babak Amin Azad, Pierre Laperdrix, and Nick Nikiforakis, Less is More: Quantifying the Security Benefits of Debloating Web Applications, USENIX Security 2019. ↩︎
-
Babak Amin Azad et al., AnimateDead: Debloating Web Applications Using Concolic Execution, USENIX Security 2023. ↩︎
-
Go standard-library documentation for
crypto/tls.Config, particularlyInsecureSkipVerify,RootCAs,ServerName,VerifyConnection, andVerifyPeerCertificate. The bypass example assumes no custom verifier replaces the standard chain and hostname checks. The application setting described here is hypothetical. ↩︎ ↩︎ -
Joel Sing, libtls: Rethinking the TLS/SSL API, linux.conf.au 2017. ↩︎
-
OpenBSD,
reallocarray(3)/malloc(3), especially “Return Values,” “Idioms,” and “Caveats.” The interface checks for overflow in the multiplication of element count and element size. On overflow, it returnsNULLwitherrnoset toENOMEM; the existing allocation is preserved on failure. Availability and other allocator semantics must be checked on the target platform. ↩︎