A standard cryptography API for WebAssembly
It’s no secret that we’re fans of WebAssembly. With WebAssembly, applications can run at near-native speed, on any platform, with strong security guarantees. However, WebAssembly programs can’t do much without accessing APIs.
In this post we’ll describe the motivations for a standard WebAssembly cryptography API, the challenges of designing such an API for this programming language, and the work we’ve contributed so far.
Why WebAssembly needs a WASI cryptography API
While implementations of cryptographic primitives can be compiled into WebAssembly in each application, a standard native host implementation may be desirable for multiple reasons:
Many modern CPUs include cryptographic extensions that can greatly improve performance over pure software implementations while also mitigating side channels. These are not directly accessible from WebAssembly programs. Some functions also require specific CPU instruction sequences for optimal performance, and an optimizing WebAssembly compiler may not be able to generate these.
Resistance against side-channel attacks
Side channels can be exploited to leak secrets between different programs. Cryptographic implementations must do their best to mitigate this while manipulating any secret-dependent data. However, WebAssembly programs need a translation layer before they can be executed on a CPU, and these programs don’t have any control over what optimizations will be made by the compiler or runtime. As a result, code originally written with some of these protections, such as running in constant time, may lose that property and introduce severe vulnerabilities.
Put simply, code directly compiled for the target CPU is easier to verify.
Certifications such as PCI-DSS may not be easy to obtain for WebAssembly modules that will be executed on arbitrary runtimes. However, a WASI cryptography module can be built on top of existing certified libraries and hardware.
Instead of requiring individual modules to be possibly certified, a cloud provider implementing these standard APIs may be able to offer access to certified implementations to all its clients.
As long as standard interfaces exist, WebAssembly modules can be written in multiple programming languages and can safely share data.
Having a standard interface for cryptography means the same set of algorithms are available in all languages targeting WebAssembly, and that they work exactly the same way, with the same performance and the same security guarantees.
The host can protect sensitive material and secret keys by isolating them from WebAssembly programs. They will be securely stored and managed by the host, and will not be visible by guest programs, except as opaque references that can still be used for cryptographic operations.
This greatly mitigates the implications of possible vulnerabilities in WebAssembly programs.
Challenges with WASI
WASI APIs have some unique features and constraints. First, they are not written for a particular programming language. They should be usable from any language, including those that don’t support WebAssembly yet.
Next, these APIs are very low-level. WASI APIs sit between a runtime (such as Lucet and Wasmtime) and a library exposing idiomatic bindings for a language (such as as-wasi for AssemblyScript). To some extent, they are akin to system calls. Unlike most APIs that are designed for applications, WASI APIs must be made for bindings, which have architectural implications.
Finally, this all means that they have to be stable. A breaking change must not break a single application. It cannot break a single library or its dependencies either. As the base layer all bindings depend on, a change in API would potentially break similar libraries written for all languages, cascading to all dependent applications.
Proper versioning can help control the breakage as the API evolves. However, having stable APIs is the best way to gain developers trust, to make it easy to write and maintain bindings, and to grow a new ecosystem.
But considering that WebAssembly is not itself a programming language, how are interfaces defined?
The WebAssembly Interface Types answer this question: They define a standard way to document how to access functions and types implemented in a module, and allow programs written in any language to safely access these.
However, the WebAssembly memory model creates some implications on API design:
A guest cannot access the host memory space. If the host needs to send data to the guest (beyond integers/floats), it has to copy it to guest-allocated space. The required size has to be predictable, or a streaming interface must be implemented.
Host pointers are irrelevant to guests. A data structure holding pointers, such as a simple linked list, must be serialized in order to be sent to a guest.
As an alternative, pointers or objects containing pointers can be represented as opaque handles to the guest.
Once the guest creates or acquires a handle, individual fields of the corresponding object can be accessed only by using dedicated functions.
But with that scheme, having many object types, or complex object types can quickly lead to an explosion of the number of functions. Managing and accessing handles also has a runtime cost.
Challenges with a new cryptography API
As a WASI submodule, a cryptography API has to share the same goals and constraints as other WASI APIs: stability, targeting libraries, and being able to take advantage of the WebAssembly isolation model.
Avoiding breaking changes is critical. However, new cryptographic algorithms are constantly being adopted. Others get deprecated. New usages require new kinds of functions.
Keeping that in mind, we have to create an API that can support both conservative and widely used algorithms and recent (and future!) algorithms that may have different requirements.
For example, a traditional hash function takes a variable-sized input, and returns a fixed-size digest. However, modern hash functions support an optional key, have a variable output length, accept additional parameters such as salts and contexts, blurring the line between hash functions and other kinds of primitives. Some functions even support multi-threading, incremental updates and verified streaming. These are all very useful features, but they can be difficult to expose in an API designed for more conventional functions.
Similarly, the recently introduced permutation-based constructions (as in many candidates of the ongoing NIST lightweight cryptography competition) allow a single core function to be used both for hashing and encryption, while maintaining a rolling state for an entire session. This doesn’t fit at all in APIs having a strong separation between different operations.
Finally, even though the standardization process is not finished yet, post-quantum signature and key exchange mechanisms cannot be ignored either. They also come with new requirements.
This is why, in addition to supporting conservative algorithms, and in order to avoid breaking changes, a cryptography API designed today and aimed at stability has to be ready for all these new paradigms.
Targeting bindings, not applications
The WASI cryptography API is somewhat unique as it is not a library, but a set of functions to build libraries. This includes two classes of libraries:
Bindings specifically made around the WASI cryptography submodule, and
Existing libraries adapted for a WebAssembly environment.
For the first class of libraries, we want the underlying API to have enough structure to provide guidance to implementers. This will ensure safety and consistency across all bindings.
Existing libraries have each been created for a different purpose, so the WASI cryptography API must not be limited to a fixed set of algorithms. In particular, and even though their presence may be optional, it shouldn’t prevent legacy or proprietary algorithms from being implemented.
Illustrated by NaCl and Google Tink, recent cryptography APIs aim at being “easy to use, hard to misuse.” Even though the WASI cryptography API cannot be as opinionated as these libraries, there are still ways to design a cryptography API that can help avoid bugs and encourage safe practices.
This document presents some of the design choices we made for the WASI cryptography API to reduce misuse and encourage safe practices.
Taking advantage of the WebAssembly isolation model
One of the main goals of the WASI cryptography API is to perform operations involving secret keys while keeping the actual secret material in host memory only where it is inaccessible from WebAssembly programs.
This matches the WebAssembly isolation model, where a guest program can only access its own memory.
The API can thus be designed so that secrets must always be wrapped inside a host object before a cryptographic operation using them can be made.
Wrapping objects can be created either by importing raw keying material, or by delegating the key creation to the host. Given only a host-generated handle the only way a WebAssembly program can inspect the corresponding object is by calling a host function.
According to the local policy or key type, the host can then refuse an export operation, keeping secret keys secure.
Building the WASI cryptography API
Work on the WASI cryptography API was initiated a few weeks before this post was written, by founding members of the Bytecode Alliance Fastly, Mozilla, and RedHat, as well as Tobias Nießen of the NodeJS Technical Steering Committee. Ideas are discussed in a dedicated GitHub repository.
We’re very early in the process and are still discussing design choices. We’re optimistic, but a lot of work remains to be done before we have a complete specification because we want this API to be good. We want an API that developers are going to love.
The scope of the WASI cryptography module has been limited to primitives that cannot be securely or efficiently made by a guest WebAssembly program.
This greatly reduces the attack surface; complex parsers and protocols that can be implemented by composing these primitives can benefit from the security benefits of running in a WebAssembly sandbox, reaching out to host functions only when necessary.
While the API is still a moving target, a look at actual code a library implementer would write can illustrate how WebAssembly interfaces are specified, how they can be implemented, and how an API addresses the challenges above.
Example code is written in Rust, but the same patterns would apply to any other programming language.
Key creation - Unifying managed and unmanaged keys
let key_handle = symmetric_key_generate("HMAC/SHA-512", None)?;
The returned value is not the key itself, but an opaque identifier. The secret is securely kept in host memory, and will be automatically erased as soon as the handle is closed. However, it can be exported as raw bytes if necessary.
An alternative way to create a key is to use key management services provided by the host:
let key_manager_handle = key_manager_open(None)?;
let key_handle = symmetric_managed_key_generate(None)?;
In all the examples above, None stands for a set of parameters to override. More on that later.
Managed keys may not be exportable. The host can automatically revoke or rotate them. And guest programs can refer to a previously generated managed key using an identifier and a version number (or “latest” for the current version):
let mut key_id = [0u8; KEY_ID_LEN];
symmetric_key_id(key_handle, &mut key_id)?;
let previous_key_handle = symmetric_key_from_id(key_id, Version::LATEST)?;
Symmetric operations - Keeping the API small
The following examples illustrate how common symmetric operations can be implemented using the same proposal:
let mut out = [0u8; 64];
let st = symmetric_state_open("SHAKE-128", None, None)?;
symmetric_state_squeeze(st, &mut out)?;
let buffer = vec![0u8; 1_000_000_000];
let options = symmetric_options_open()?;
symmetric_options_set_guest_memory(options, "memory", &mut buffer)?;
symmetric_options_set_u64(options, "opslimit", 4)?;
symmetric_options_set_u64(options, "parallelism", 8)?;
let st = symmetric_state_open("Argon2id", None, Some(options))?;
let pw_str = symmetric_state_squeeze_tag(st)?;
AEAD encryption with automatic nonce generation
let mut automatic_nonce = [0u8; 12];
let mut ciphertext = vec![0u8; 20];
let st = symmetric_state_open("AES-256-GCM-SIV", Some(key), Some(options))?;
symmetric_state_options_get(st, "nonce", &mut automatic_nonce)?;
symmetric_state_absorb(st, "additional data")?;
symmetric_state_encrypt(st, &mut ciphertext, b”test”)?;
You may notice how similar these code snippets are; to keep the API small, all symmetric operations share a unique, small function set loosely modeled after Strobe, SHO, Xoodyak and other recent systems using a rolling state for multiple operations.
This part of the API was not designed for specific algorithms. Instead, it provides a dozen functions that can be composed in order to support virtually any symmetric construction. We hope to be able to extend that concept to the entire API.
And in order to take advantage of specific parameters an algorithm may require, arbitrary options can be supplied in addition to the algorithm identifier. This ensures that the API will remain stable as new primitives get added over time.
In addition to compile-time and run-time type checks, we are also taking precautions to prevent creation of insecure contexts. For example, nonces are automatically generated when it is safe to do so, and cannot be used twice in the same session.
In order to encourage experimentation and evaluate its usability on real-world applications, we maintain an example implementation of the proposed APIs, AssemblyScript bindings, as well as a version of the Wasmtime runtime that exposes it to WebAssembly modules.
Beyond the API
Thanks to interface definitions, a WebAssembly module can safely call functions and exchange data with the host or different modules. However, this is not sufficient to guarantee deterministic behavior, which is absolutely critical for security-related applications.
Therefore, to make the cryptography API something developers can trust and rely on, the specification needs to provide all the required details to ensure the absence of undefined behavior. This includes documenting the behavior of common cryptographic primitives on edge cases, with appropriate test vectors.
A reference implementation will also be made available, as well as an extensive compatibility test suite in order to encourage alternative implementations.
Stay tuned, this is just the beginning! WebAssembly opens a whole new world of possibilities, and we are always thrilled to help bring a new essential piece to its growing ecosystem.