This work is specifically about digital signatures in WebAssembly modules, not about package/OCI signatures.
The goal is to converge on requirements that can be used to build out an initial and extensible specification.
The proposal from Appendix 2 has documentation and implementations to experiment with:
- WebAssembly module signatures document for implementers.
- wasmsign2 rust library and CLI tool
- WasmSign - a tool to embed signatures in WebAssembly modules
- wasm-sign - another tool to sign WebAssembly modules
- Istio/Envoy example load-time check of WasmSign signatures
- Validation in Lucet
- WAPM package manager signature verification
- Sigstore transparency log
-
Signatures may implement multiple signature algorithms
- Compliance requirements
- Public key size/signature size/verification CPU cost tradeoffs (ex: post-quantum schemes vs EdDSA)
- Code and key reuse (ex: ECDSA-SHA3 for blockchains already leveraging SHA3, EdDSA to use existing SSH keys)
-
It should be possible to verify a module file before execution
-
It should be possible to add signed custom sections to a potentially signed module so that one can verify the original module and the additional custom section independently
See Appendix 1. A user may want to add additional signed information (debug data, precompiled headers) to a signed module and then re-distribute. Users may or may not trust and choose to consume the additional information.
-
Signatures should support streaming compilations
-
A module may contain multiple signatures possibly with key identifiers for each signature
- Required for key rotation
- Keys may be short-term
- There can be a signature for sections 1, 2, 3 and another one for sections 1, 2, 3, 4 in order to make section 4 optional, yet still verifiable when needed
- Oak use case, where each signature represents a property
- The ability to require multiple signers to trust a module (ex: CI system + module maintainer)
- Key identifiers can be used by verifiers to map to public key identifiers they have, and/or quickly reject a signature if it does not match.
-
The format used to encode signatures and related should be extensible
-
Arbitrary sections may not be signed (partial signature)
-
Arbitrary sections may be ignored during verification (partial verification)
-
(a) Sign complete bytecode
Append “signature” section at the end of the file.
-
(b) Sign all bytecode preceding the “signature” Section
This allows adding new Custom Sections after the signature was created. See Appendix 1.
-
(c) Sign all bytecode since the previous “signature” section
Sign bytecode since the previous signature section or start of the module if there wasn’t any (i.e. signing consecutive groups of sections).
This allows adding new custom sections after the signature was created, and removing consecutive groups of sections along with their signatures.
-
(d) Sign hashes of consecutive sections
Split sections into parts (consecutive sections, delimited by a delimiter) that can be signed and verified independently, and sign the concatenation of their hashes.
This allows complete and partial verification of a module using a single signature, as well as adding new custom sections with their signatures.
See Appendix 2.
-
(e) Include manifest in the “signature” section
In the signature section, include a manifest which describes which sections are signed using a given signature. This allows signing arbitrary sets of sections.
- Should signatures have associated metadata/annotations?
- Should the signature section include a timestamp, a module version, or more generally, should we define a minimal set of optional/required metadata that will be signed along with the rest of the module?
- We may want the set of signature algorithms we support and the ones required by the WASI-crypto proposal to overlap.
- Add new “description” and “usage” Custom Sections, to be defined in this working group, then presented to the WebAssembly core as a standalone proposal.
Use case for adding new Custom Sections after the original signature was generated, and the need for signatures to be able to cover only part of the Wasm module.
- Wasm module author compiles Wasm module (bytecode)
- Wasm module author signs Wasm module (
sign(hash(bytecode))
), and appends the signature in a “signature” (A
) Custom Section. - Wasm module author distributes signed Wasm module:
sections | covered by "signature" (A ) |
---|---|
bytecode | yes |
"signature" (A ) |
- Customer uploads Wasm module to the Wasm optimization service, which verifies that the Wasm module is signed by the provided public key.
“Wasm optimization” service compiles uploaded Wasm module (
compile(bytecode)
), appends it in aprecompiled_runtimeX
Custom Section, then signs the complete Wasm module (sign(hash(bytecode+signatureA+precompiled_runtimeX))
), and appends the signature in a “signature” (B
) Custom Section:
sections | covered by "signature" (A ) |
covered by "signature" (B ) |
---|---|---|
bytecode | yes | yes |
"signature" (A ) |
yes | |
precompiled_runtimeX |
yes | |
"signature" (B ) |
This signature format allows full and partial signatures, as well as incremental updates.
Two custom section types are required for this signature format:
- Custom sections named
signature
, storing signature data, when this information is embedded in the module. - Custom sections named
signature_delimiter
, separating consecutive sections that can be signed and verified independently.
Example structure of a module with an embedded signature and delimiters:
sections |
---|
signatures |
part (one or more consecutive sections) |
delimiter |
part (one or more consecutive sections) |
delimiter |
... |
part (one or more consecutive sections) |
delimiter |
Parts and delimiters:
A module can be split into one or more parts (one or more consecutive sections).
Each part is followd by a delimiter. A delimiter is a custom section named signature_delimiter
, containing a 16 byte random string.
sections |
---|
p1 = input part 1 (one or more sections) |
d1 = delimiter 1 |
p2 = input part 2 (one or more sections) |
d2 = delimiter 2 |
... |
pn = input part n (one or more sections) |
dn = delimiter n |
If a signature covers the entire module (i.e. there is only one part), the delimiter is optional.
However, its absence prevents additional sections to be added and signed later.
Signature data:
The signature data is a concatenation of the following:
- An identifier representing the version of the specification the module was signed with.
- An identifier representing the hash function whose output will be signed.
- A sequence of hashes and their signatures.
A hash is computed for all the parts to be signed:
hn = H(pn‖dn)
A signature is computed on the concatenation of these hashes:
hashes = h1 ‖ h2 ‖ … ‖ hn
s = Sign(k, "wasmsig" ‖ spec_version ‖ hash_id ‖ hashes)
One or more signatures can be associated with hashes
, allowing multiple parties to sign the same data.
The signature data can either be stored in the payload of a custom section named signature
, or provided separately.
If embedded in a module, the signature data must appear only once, and be the first section of the module.
It contains a sequence of signatures, where the end of the last signature must coincide with the last byte of the signature data.
Field | Type | Description |
---|---|---|
spec_version | byte |
Specification version (0x01 ) |
hash_fn | byte |
Hash function identifier (0x01 for SHA-256) |
hashes_len | varuint32 |
Length of the concatenated hashes in bytes |
hashes | bytes |
Concatenated hashes of the signed sections |
signature_count | varuint32 |
Number of signatures |
signatures | signature* |
Sequence of signature_count signature records |
where a signature
is encoded as:
Field | Type | Description |
---|---|---|
key_id_len | varuint32 |
Public key identifier length in bytes (can be 0 ) |
key_id | bytes |
Public key identifier |
signature_len | varuint32 |
Signature length in bytes |
signature | bytes |
Signature for hashes that can be verified using key_id |
Signature verification algorithm for an entire module:
- Verify the presence of the signature section, extract the specification version, the hash function to use and the signatures.
- Check that at least one of the signatures is valid for
hashes
. If not, return an error and stop. - Split
hashes
(included in the signature) intoh1 … hn
- Read the module, computing the hash of every
(pi, di)
tuple withi ∈ {1 … n}
, immediately returning an error if the output doesn't matchhi
- Return an error if the number of the number of hashes doesn't match the number of parts.
- Verify that the signature is valid for
hashes
.
Partial signatures:
The above format is compatible with partial signatures, i.e. signatures ignoring one or more parts. In order to do so, a signer only includes the hashes of relevant parts.
By default, partial signatures must be ignored by WebAssembly runtimes. An explicit configuration is required to accept a partially signed module.
Partial verification:
The format is also compatible with partial verification, i.e. verification of an arbitrary subset of a module:
- Verify the presence of the header, extract the specification version, the hash function to use and the signatures.
- Check that at least one of the signatures is valid for
hashes
. If not, return an error and stop. - Split
hashes
(included in the signature) intoh1 … hn
- Read the module, computing the hash of every
(pi, di)
tuple to verify, immediately returning an error if the output doesn't matchhi
- Return an error if the number of the number of hashes doesn't match the number of parts to verify.
- Verify that the signature is valid for
hashes
.
Notes:
- Subset verification doesn't require additional signatures, as verification is always made using the full set
hashes
. - Verifiers don't learn any information about removed sections due to delimiters containing random bits.
Multiple signatures:
The format also supports:
- Multiple signatures for a given section set (
hashes
). Signatures can be added incrementally, without any overhead beyond the signature sizes. - Arbitrary section subsets and signatures combinations.
- Signature verification even if sections have been reordered.
Equivalence between embedded and detached signatures:
Signatures can be embedded in a module, or detached, i.e. not stored in the module itself but provided separately.
A detached signature is equivalent to the payload of a signature
custom section.
Given an existing signed module with an embedded signature, the signature can be detached by:
- Copying the payload of the
signature
custom section - Removing the
signature
custom section.
Reciprocally, a detached signature can be embedded by adding a signature
custom section, whose payload is a copy of the detached signature.
Implementations should accept signatures as an optional parameter. If this parameter is not defined, the signature is assumed to be embedded, but the verification function remains the same.
Implementation complexity:
We expect the most common scenario to be entire modules being signed and verified, using one or more signatures.
Supporting additional use cases introduces implementation complexity, that can be summarized as follows:
Complexity | Signatures | Signed sections | Verified sections | Arbitrary combinations | Reordering |
---|---|---|---|---|---|
1 | 1 | all | all | no | no |
2 | 1+ | all | all | no | no |
3 | 1+ | any subset | signed subset | no | no |
4 | 1+ | any subset | any subset | no | no |
5 | 1+ | any subset | any subset | yes | no |
6 | 1+ | any subset | any subset | yes | yes |
The specification will define what implementations must, should and may implement based on real-world requirements.
All support levels share the same signature format, so "must" and "should" feature sets can be updated incrementally.
Algorithms and identifiers:
Identifier for the current version of the specification: 0x01
A conformant implementation must include support for the following hash functions:
Function | Identifier |
---|---|
SHA-256 | 0x01 |
Signature algorithms and key serialization:
For interoperability purposes, a conformant implementation must include support for the following signature systems:
- Ed25519 (RFC8032)
Public and private keys must include the algorithm and parameters they were created for.
Key type | Serialized key size | Identifier |
---|---|---|
Ed25519 public key | 1 + 32 bytes | 0x01 |
Ed25519 key pair | 1 + 64 bytes | 0x81 |
Representation of Ed25519 keys:
- Ed25519 public key:
0x01 ‖ public key (32 bytes)
- Ed25519 key pair:
0x81 ‖ secret key (32 bytes) ‖ public key (32 bytes)
Implementations may support additional signatures schemes and key encoding formats.