L4Re binary protocol
This page describes, byte for byte, every message the L4Re form accepts and sends. It applies to protocol
version 1. The header file l4re/pub/include/wvds_crypto.h is authoritative; this page summarizes it and
adds service behavior that the header alone doesn't tell you.
Transport
Requests and responses travel through DataspaceEndpoint. The service registers under the IPC name
CRYPTO_SVC and expects the caller under CRYPTO_CLI; each side's shared region is 64 KiB.
| Step | Who | Call |
|---|---|---|
| Send request | client | add_new_req(id, type, address, length); by convention, type is the request type from the header |
| Read and release request | service | reads the message and calls free_peer_req for every request, accepted or rejected |
| Send response | service | add_new_req with the same id and IPC type 0x80 |
| Read and release response | client | evaluates the header and calls free_peer_req |
The id matches a response to its request. You choose it; keep it unique among outstanding requests.
The IPC type of a response is always 0x80, errors included. Whether a response is an error is
determined only by the req_type field of the message header.
Message header
Every message starts with 16 bytes. All numbers are little-endian.
| Offset | Length | Field | Content |
|---|---|---|---|
| 0 | 4 | magic | 0x43525950, on the wire 50 59 52 43 |
| 4 | 1 | version | 1 |
| 5 | 1 | req_type | request or response type, see below |
| 6 | 2 | flags | 0; the service ignores this field |
| 8 | 4 | key_id | key ID, 0 if the request needs no key |
| 12 | 4 | payload_len | length of the payload that follows the header |
Before processing anything, the service checks that:
- The message is at least 16 bytes and at most 16 bytes plus 64 KiB long.
magicandversionmatch.payload_lenis no more than 65,536 and equals the message length minus 16.
The service drops an empty or oversized message without a response. If the message is shorter than the
header, the header is wrong, or payload_len doesn't match, it responds with WVDS_ERR_INVALID_HEADER.
Request types
| Value | Name | key_id | Served |
|---|---|---|---|
0x01 | WVDS_REQ_AES_GCM_ENCRYPT | AES key | yes |
0x02 | WVDS_REQ_AES_GCM_DECRYPT | AES key | yes |
0x03 | WVDS_REQ_CHACHA20_ENCRYPT | ChaCha20 key | only with the chacha20 build feature |
0x04 | WVDS_REQ_CHACHA20_DECRYPT | ChaCha20 key | only with the chacha20 build feature |
0x10 | WVDS_REQ_ML_DSA_SIGN | ML-DSA key | yes |
0x11 | WVDS_REQ_ML_DSA_VERIFY | 0 | yes |
0x12 | WVDS_REQ_ML_DSA_KEYGEN | 0 | yes |
0x20 | WVDS_REQ_ML_KEM_ENCAP | 0 | yes |
0x21 | WVDS_REQ_ML_KEM_DECAP | ML-KEM key | yes |
0x22 | WVDS_REQ_ML_KEM_KEYGEN | 0 | yes |
0x30, 0x31 | WVDS_REQ_SLH_DSA_SIGN, …_VERIFY | – | reserved |
0x40, 0x41 | WVDS_REQ_LMS_SIGN, …_VERIFY | – | reserved |
0x50 | WVDS_REQ_DERIVE_KEY | 0 | yes |
0x51 | WVDS_REQ_IMPORT_KEY | – | reserved |
0x52 | WVDS_REQ_EXPORT_KEY | key pair | yes |
0x80 | WVDS_REQ_RESPONSE | as in the request | response |
0xFF | WVDS_REQ_ERROR | 0 | error response |
The service answers reserved and not-built-in types with WVDS_ERR_INVALID_REQ_TYPE.
A request that needs a ChaCha20 key can't succeed today: no request type creates such a key. The same applies to AES; see Keys and key storage.
Payload by request
Length fields inside the payload are 4 bytes, little-endian. “Rest” means all bytes up to the end of the payload.
AES-GCM
| Type | Request | Response |
|---|---|---|
0x01 | nonce (12) · AAD length (4) · AAD · plaintext (rest) | nonce (12) · tag (16) · ciphertext (rest) |
0x02 | nonce (12) · tag (16) · AAD length (4) · AAD · ciphertext (rest) | plaintext (rest) |
The caller supplies the nonce. A nonce the service has already seen for this key among the last 1024
results in WVDS_ERR_ENCRYPTION_FAILED. ChaCha20 (0x03, 0x04) uses the same layouts.
ML-DSA
| Type | Request | Response |
|---|---|---|
0x12 | level (1) | key ID (4) · public key (rest) |
0x10 | level (1) · message (rest) | signature (rest) |
0x11 | level (1) · key length (4) · public key · signature length (4) · signature · message (rest) | result (1): 1 valid, 0 invalid |
Levels: 2 = ML-DSA-44, 3 = ML-DSA-65, 5 = ML-DSA-87. For 0x10, the service signs only the
message; the level byte isn't part of it, and the service takes the level from the key. For 0x12, the
response header's key_id field also carries the new key ID.
An invalid signature isn't an error response; it's a response with result 0. If the public key can't
be used, the service responds with WVDS_ERR_VERIFICATION_FAILED.
ML-KEM
| Type | Request | Response |
|---|---|---|
0x22 | level (1) | key ID (4) · public key (rest) |
0x20 | level (1) · public key (rest) | shared secret (32) · ciphertext (rest) |
0x21 | ciphertext (rest) | shared secret (32) |
Levels: 1 = ML-KEM-512, 3 = ML-KEM-768, 5 = ML-KEM-1024.
Key derivation and public keys
| Type | Request | Response |
|---|---|---|
0x50 | salt length (4) · salt · info length (4) · info · output length (4) · IKM (rest) | derived key (output length) |
0x52 | no payload | public key (rest) |
For 0x50, key_id must be 0, the IKM must not be empty, and the output length must not exceed 1024
bytes. 0x52 returns the public key of an ML-DSA or ML-KEM key; for any other key, the service responds
with WVDS_ERR_KEY_NOT_FOUND.
Sizes
| Parameter set | Public key | Signature or ciphertext |
|---|---|---|
| ML-DSA-44 | 1312 | 2420 |
| ML-DSA-65 | 1952 | 3309 |
| ML-DSA-87 | 2592 | 4627 |
| ML-KEM-512 | 800 | 768 |
| ML-KEM-768 | 1184 | 1088 |
| ML-KEM-1024 | 1568 | 1568 |
All values are in bytes; the header defines them as constants, such as WVDS_ML_DSA_65_SIG_SIZE.
Error response
A header with req_type = 0xFF and key_id = 0, followed by:
| Offset | Length | Content |
|---|---|---|
| 0 | 4 | error code |
| 4 | 4 | message length |
| 8 | rest | message, UTF-8, for diagnostics only |
For the error codes, see Error codes. Evaluate the code, not the message; the wording can change.
Requests that get no response
If the caller exceeds 60,000 requests in 60 seconds, the service drops further requests without a response, so a flood doesn't trigger a second flood. It still releases the request's shared memory. Give every request a timeout on the calling side.