Call the L4Re service
This guide shows how an L4Re task uses DataspaceEndpoint to have the service generate an ML-DSA key
and sign a message with it. The examples are C++ because DataspaceEndpoint is a C++
class. They need only wvds_crypto.h and the DataspaceEndpoint header. The build_… functions
that assemble messages are plain C and drop into C code unchanged.
Before you begin:
- The service runs as a task of its own; see Deployment forms compared.
- Your task has access to your platform vendor's
DataspaceEndpointand sets it up the way the vendor requires: registry server as a static variable, constructor on the main thread, server loop at the end ofmain.
1. Create the endpoint
#include "wvds_crypto.h" #include "DataspaceEndpoint.h" static L4Re::Util::Registry_server<> server; int main() { DataspaceEndpoint ep(&server, "CRYPTO_CLI", // your name, as the service expects it "CRYPTO_SVC", // the service's name 64 * 1024, // size of the peer's region 5000000, // peer timeout on_crypto_response); // start a worker thread that calls wait_for_initialization() and then sends requests server.loop(); }
The names are fixed in the service. It won't find a task with a different name.
2. Have a key generated
static size_t build_ml_dsa_keygen(uint8_t *buf, uint8_t level) { wvds_init_header((wvds_message_header_t *)buf, WVDS_REQ_ML_DSA_KEYGEN, 0, 1); WVDS_PAYLOAD_PTR(buf)[0] = level; return WVDS_MESSAGE_SIZE(1); } /* on the worker thread, after wait_for_initialization() */ uint8_t *buf = (uint8_t *)ep.allocate_local_blocking(WVDS_MESSAGE_SIZE(1)); size_t len = build_ml_dsa_keygen(buf, WVDS_ML_DSA_65); ep.add_new_req(next_id++, WVDS_REQ_ML_DSA_KEYGEN, buf, len);
The response carries the new key ID in the first 4 bytes of the payload, followed by the public key, 1952 bytes for ML-DSA-65.
3. Have a message signed
static size_t build_ml_dsa_sign(uint8_t *buf, uint32_t key_id, const uint8_t *msg, uint32_t msg_len) { wvds_init_header((wvds_message_header_t *)buf, WVDS_REQ_ML_DSA_SIGN, key_id, 1 + msg_len); uint8_t *p = WVDS_PAYLOAD_PTR(buf); p[0] = WVDS_ML_DSA_65; /* not evaluated; the level comes from the key */ memcpy(p + 1, msg, msg_len); return WVDS_MESSAGE_SIZE(1 + msg_len); }
The whole request, that is, the 16-byte header, the level byte, and the message, has to fit into the
64 KiB region the service maps from your task. Keep messages well below 64 KiB, and sign a hash for
larger data. The response is the signature itself, exactly WVDS_ML_DSA_65_SIG_SIZE bytes for
ML-DSA-65.
4. Handle responses
int on_crypto_response(DataspaceEndpoint *self, u_int64_t id, u_int8_t type, const u_int8_t *data, l4_size_t size) { wvds_message_header_t hdr; if (size >= sizeof hdr) { memcpy(&hdr, data, sizeof hdr); const uint8_t *payload = data + sizeof hdr; if (!wvds_validate_header(&hdr) || sizeof hdr + hdr.payload_len != size) { /* not a valid response from the service */ } else if (wvds_is_error_response(data, size)) { uint32_t code; memcpy(&code, payload, sizeof code); /* wvds_error_name((wvds_error_code_t)code) returns the name */ } else { /* evaluate the content based on the pending request with this id */ } } self->free_peer_req(id, type, data, size); /* always release */ return 0; }
Three things make an integration robust:
- Errors live in the header. The IPC type of a response is always
0x80. Onlyreq_typein the header tells you whether it's an error;wvds_is_error_responsechecks exactly that. - Always release. Call
free_peer_reqfor every response, including invalid ones. Otherwise the service's region fills up and it can no longer respond. - Plan for timeouts. Under overload, the service drops requests without a response. Track outstanding
idvalues and give up on a request after a deadline.