Rust client
The Rust client library is published as the basil crate. It's the first-class way to talk to
the broker, and it's deliberately lightweight: it avoids the heavy broker and server dependencies, so
adding it to an application doesn't transitively pull in OpenBao, the tonic server, age, or
argon2. A client app links the gRPC protocol and the client-side crypto, not the storage backend.
It offers two front ends:
Client: the async client (Tokio).BlockingClient: a synchronous wrapper for non-async callers.
Async example
use basil::{Client, KeyType, AeadAlgorithm};
# async fn run() -> basil::Result<()> {
// Connect over the broker's Unix socket. The kernel attests this process's
// uid/gid, so there is no token to present.
let mut client = Client::connect("/run/basil/basil.sock").await?;
// Sign a raw message. The private key never leaves the backend.
let signature = client.sign("web.tls.signing_key", b"hello basil").await?;
// AEAD-encrypt. Basil owns the nonce, so you can't reuse one by accident.
let envelope = client.encrypt("app.aead", b"backup-bytes").await?;
let plaintext = client.decrypt("app.aead", &envelope).await?;
// Mint a short-lived generic JWT using the issuer key's JWS algorithm.
let jwt = client
.mint_jwt(
"svc.jwt_issuer",
"spiffe://example.org/web",
Some(300),
serde_json::json!({"scope": "orders:read"}),
)
.await?;
// Ask the agent what it's fronting.
let status = client.status().await?;
println!("backend: {}, version: {}", status.backend, status.version);
# Ok(())
# }The snippets above are simplified to the common shape; several methods take richer arguments than
shown (for example AEAD algorithm selection, AAD, NATS permissions, or certificate SANs). Use your
editor's type information against the basil crate for the exact signatures. The groups below list
the full surface.
Blocking example
use basil::BlockingClient;
# fn run() -> basil::Result<()> {
let mut client = BlockingClient::connect("/run/basil/basil.sock")?;
let sig = client.sign("web.tls.signing_key", b"hello basil")?;
let status = client.status()?;
# Ok(())
# }The method surface
| Area | Methods |
|---|---|
| Connect | connect(path), connect_with_timeout(path, default_timeout) |
| Keys | new_key(key_id, key_type), import(...), import_set(entries), list_catalog(prefix) |
| Sign / verify | sign(key_id, message), sign_with_algorithm(...), verify(key_id, message, signature), verify_with_algorithm(...), get_public_key(...) |
| Encrypt / decrypt | encrypt(...), decrypt(...), wrap_envelope(...), unwrap_envelope(...) |
| Secrets / values | get_secret(...), set_secret(secret_id, value), rotate_secret(secret_id) |
| Minting & certificates | mint_jwt/mint_jwt_json, sign_nats_jwt/sign_nats_jwt_json, validate_nats_jwt, issue_certificate |
| NATS keys & curve boxes | mint_nats_user/account/operator/signer/server/curve, encrypt_nats_curve, decrypt_nats_curve |
| Admin | status(), health(), readiness(), reload(check), explain(subject, op, key), revoke(...) |
Public result/argument types include KeyHandle, SecretValue, MintedJwt, IssuedCertificate,
AgentStatus, AgentHealth, AgentReadiness, AgentReload, AgentExplanation, AgentDecision,
AgentRevocation, ImportEntry, NatsUserPermissions, and SignNatsJwtOptions. Errors surface
through basil::Error (with the basil::Result alias). Protocol enums such as KeyType,
AeadAlgorithm, and CatalogEntry are re-exported from basil-proto. Match public enums with a
wildcard arm; Basil marks evolution-sensitive enums as non-exhaustive before 1.0.
mint_jwt and sign_nats_jwt accept any serde::Serialize claim object and send UTF-8 JSON bytes
over gRPC. Use mint_jwt_json or sign_nats_jwt_json when you already have encoded JSON and need
to preserve byte-exact large integer claims. See the
NATS JWT reference for every account and user claim NATS signing
accepts and the semantic defaults Basil applies.
Basil's signing contract is over the raw message: you pass b"hello basil", not a pre-computed
hash. The broker (and its backend) does the hashing, closing a class of caller-side mistakes.
Sealed invocation helpers
The crate also exports basil::sealed_invocation for sealed-invocation handoff. Use
SealedInvocationOptions and prepare_sealed_invocation with a SignInvocationRequest body to
build a v1 tagged COSE_Sign1 for a bridged Sign call — the only invocation the broker executes.
The helper selects the protected application/basil.sign-request content type, emits deterministic
CBOR, seals the body to the broker
request-encryption public key, and signs the COSE Sig_structure with your signer. For protected
Sign responses, verify_and_decrypt_sign_response verifies the pinned broker response-signing key,
checks request binding, decrypts the body to the request-selected response key, and decodes the
trusted status and signature fields. The clear gRPC status from InvocationService.Invoke reflects
transport and protocol failures only. Trust an operation result (OK, DENIED, INVALID_REQUEST,
or INTERNAL_ERROR) only after verifying and decrypting the response body.
See Sealed invocations for the COSE profile, fixture path, replay
and audience rules, UnsealCose, and the protected response contract.
Streaming encryption
The crate also exports basil::stream for bounded-memory file and large-payload encryption:
encrypt_aead / decrypt_aead for symmetric CEKs, and encrypt_ml_kem / decrypt_ml_kem for
ML-KEM-wrapped CEKs. The format is wire-identical to the Go stream subpackage.
See Streaming encryption.
SPIFFE in Rust
For workload identity, you don't need a bespoke integration:
- Use the upstream
spiffe(rust-spiffe) crate. Its auto-rotatingX509Sourceconnects to the Basil socket and keeps the X.509-SVID fresh on Basil's configuredsvid-ttl-secscadence, with no Basil-specific TLS adapter required. - Or use the in-tree generated Workload API client
basil_proto::spiffe::spiffe_workload_api_client::SpiffeWorkloadApiClient(alreadypub, exercised by the SPIFFE e2e tests).
The high-level spiffe client attaches the required workload.spiffe.io: true metadata header for
you; a raw generated client must set it itself or every call fails closed with InvalidArgument.
A Rust service gets an auto-rotating SVID natively via the spiffe crate. Save the
spiffe-helper sidecar for services you can't recompile.
Where to go next
- Integration patterns: native, sidecar, or pre-fetch after designing the secret out.
- Sealed invocations: the bridged-transport COSE helpers.
- Streaming encryption: files and large payloads without full buffering.
- NATS integration: NATS JWT validation and curve xkey boxes.