Skip to content

guides

Intended Documentation

Rust Safety-Critical Firmware (Roadmap)

Wire-format and verification-contract specification for the Intended edge verifier. The native Rust verifier crate is not yet shipped — this page is the spec for in-house verifier code.

RoadmapSource: codeValidated: 2026-06-07

Rust Safety-Critical Firmware (Roadmap)#

Audience: firmware engineers writing Rust for safety-critical motion control — ros2_control hardware interfaces, no_std / RTOS motor controllers, automotive ECUs, surgical real-time stacks.

Roadmap — the Rust verifier is not in the repository

There is no shipped Rust SDK or edge-verifier crate today. The crates/intended-verifier artifact referenced elsewhere does not exist in the repo yet. This page documents the token wire format and verification contract so you can write an in-house verifier now — and so that in-house verifier maps cleanly onto the shipped crate when it lands. The token format itself is real and stable (it is what the cloud signer emits); the Rust crate and the dedicated edge verifier binary are forward-looking.

Verify against the cloud today#

Until the crate ships, the recommended path is: fetch and cache the signer's JWKS from https://api.intended.so/.well-known/jwks.json on a non-RT thread, verify the RS256 signature and claims there, and pass a verified flag into your RT loop. The verification rules below are exactly what that thread must enforce.

The dev/sandbox signer is ephemeral

In dev/sandbox the JWKS endpoint serves an ephemeral per-process keypair that rotates whenever the API process restarts. Refresh JWKS on a kid cache miss, not only on a timer. A KMS-backed signer (INTENDED_PHYSICAL_KMS_KEY_ID) provides a stable key when configured.

Token wire format#

An Authority Token is an RS256 JWT. Header: { "alg": "RS256", "typ": "JWT", "kid": … }. Body:

json
{
  "iss": "https://api.intended.so",
  "sub": "cobot-east-3",
  "aud": "intended-edge-verifier",
  "iat": 1735689600,
  "exp": 1735689601,
  "jti": "01HQ…",

  "intended": {
    "version": 2,
    "oiCode": "OI-1502",
    "tenantId": "tenant_acme_prod",
    "actorIdentity": "cobot-east-3",
    "actorIdentityKind": "ieee-802-1ar-devid",
    "deadlineMs": 200,
    "issuedAtMs": 1735689600000,
    "expiresAtMs": 1735689600200,
    "safeDefault": "hold-position",
    "safetyBit": true,
    "safetyCitations": ["OI-2903", "OI-2904", "OI-2906"],
    "dagNodeId": "pick-step-1",
    "realTimeTier": "rt-soft",
    "physicalStateRef": "state_…",
    "operatorTicketId": null
  }
}

exp is in seconds (RFC 7519); intended.expiresAtMs is in milliseconds — operationally useful for sub-second TTLs that round to the same exp. The token lifetime is dagNode.deadlineMs, not a fixed value. Enforce both exp and expiresAtMs.

Audience must be intended-edge-verifier

The signer mints aud: "intended-edge-verifier". The sample verifier in the intended-ros2 package defaults expected_audience="intended-edge" — a mismatch that rejects every valid token. Configure your verifier's expected audience to intended-edge-verifier.

Verification contract#

The verifier MUST reject the token if any of the following hold:

  1. Signature — does not validate against the issuer's published JWKS (selected by kid).
  2. iss — not in the configured issuer allow-list.
  3. aud — not intended-edge-verifier.
  4. exp / expiresAtMs — past current attested time.
  5. actorIdentity — does not match the verifier's bound identity (typically the IEEE 802.1AR DevID provisioned at manufacture).
  6. oiCode — not in the operator's policy allow-list for this actor + cell.
  7. safetyBit mismatch — token claims safetyBit: false but the action class requires safety-rated authorization on this site.

Use expiresAtMs for sub-second windows; do not rely on nbf.

Token lifecycle
A token is minted on ALLOW with a kid-identified RS256 signature and a deadline-derived expiry; a verifier fetches JWKS by kid, checks signature, issuer, audience, expiry, bound identity, and OI allow-list, then the token expires at deadlineMs or is revoked via the revocations feed.

Mint on ALLOW, verify against JWKS by kid, expire at deadlineMs. Revocation propagates via /v1/physical/revocations; offline verifiers cannot honor revocation until they reconnect.

Reference verifier sketch#

rust
// SPEC SKETCH — not the shipped crate (which does not exist yet). When the
// intended_verifier crate ships it will handle JWKS rotation, time
// attestation, side-channel resistance, and certification edge cases this
// sketch does not.

use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct IntendedClaims {
    version: u8,
    #[serde(rename = "oiCode")]      oil_code: String,
    #[serde(rename = "actorIdentity")] actor_identity: String,
    #[serde(rename = "expiresAtMs")]   expires_at_ms: i64,
    #[serde(rename = "safeDefault")]   safe_default: String,
    #[serde(rename = "safetyBit")]     safety_bit: bool,
}

#[derive(Debug, Deserialize)]
struct AuthorityTokenClaims {
    iss: String,
    sub: String,
    intended: IntendedClaims,
}

#[derive(Debug, thiserror::Error)]
pub enum VerifyError {
    #[error("signature invalid")]             SignatureInvalid,
    #[error("issuer not trusted: {0}")]       IssuerNotTrusted(String),
    #[error("expired (now {now} > exp {exp})")] Expired { now: i64, exp: i64 },
    #[error("actor mismatch: expected {expected}, got {actual}")]
                                              ActorMismatch { expected: String, actual: String },
    #[error("OI {0} not allowed for actor {1}")]
                                              OilNotAllowed(String, String),
    #[error("malformed token: {0}")]          Malformed(String),
}

pub struct VerifierConfig {
    pub bound_actor_identity: String,
    pub trusted_issuers: Vec<String>,        // ["https://api.intended.so"]
    pub policy_allowlist: Vec<String>,       // OI codes this cell may execute
    pub jwks: jsonwebtoken::jwk::JwkSet,
}

pub fn verify(token: &str, cfg: &VerifierConfig, attested_now_ms: i64)
    -> Result<IntendedClaims, VerifyError>
{
    let header = jsonwebtoken::decode_header(token)
        .map_err(|e| VerifyError::Malformed(e.to_string()))?;
    let kid = header.kid.ok_or_else(|| VerifyError::Malformed("missing kid".into()))?;
    let jwk = cfg.jwks.find(&kid).ok_or(VerifyError::SignatureInvalid)?;
    let key = DecodingKey::from_jwk(jwk).map_err(|_| VerifyError::SignatureInvalid)?;

    let mut validation = Validation::new(Algorithm::RS256);
    validation.set_audience(&["intended-edge-verifier"]);   // MUST match the signer
    let data = decode::<AuthorityTokenClaims>(token, &key, &validation)
        .map_err(|_| VerifyError::SignatureInvalid)?;
    let claims = data.claims;

    if !cfg.trusted_issuers.contains(&claims.iss) {
        return Err(VerifyError::IssuerNotTrusted(claims.iss));
    }
    if claims.intended.expires_at_ms < attested_now_ms {
        return Err(VerifyError::Expired { now: attested_now_ms, exp: claims.intended.expires_at_ms });
    }
    if claims.intended.actor_identity != cfg.bound_actor_identity {
        return Err(VerifyError::ActorMismatch {
            expected: cfg.bound_actor_identity.clone(),
            actual:   claims.intended.actor_identity,
        });
    }
    if !cfg.policy_allowlist.contains(&claims.intended.oil_code) {
        return Err(VerifyError::OilNotAllowed(claims.intended.oil_code, claims.sub));
    }
    Ok(claims.intended)
}

RT loop integration#

Verification is allocation-light with fixed-size buffers. Indicative budget on a 1 GHz Cortex-A:

OpBudget
Header parse + kid lookup< 5 µs
RS256 signature verify1–4 ms (key-size dependent)
Claim extraction + checks< 5 µs
Total hot path≤ 5 ms typical, 10 ms worst case

For 10-ms control loops this fits one cycle. For sub-1ms loops, verify out-of-band on a separate core and pass a verified flag via a lock-free queue.

Time attestation#

attested_now_ms MUST come from PTP / NTP, not SystemTime::now() — spoofing system time is the obvious attack on time-bound credentials. Loss of trustworthy time must result in a defined safe state, not silent acceptance.

Revocation#

Poll GET /v1/physical/revocations?since=<asOfMs> and reject any token whose jti appears. An offline verifier cannot honor revocation until it reconnects; bound the offline window accordingly (see the operator guide).

Until the crate ships#

A minimal in-house verifier following this spec is a few hundred lines of Rust over jsonwebtoken + reqwest. Treat it as uncertified — Intended makes no certification claim about your verifier code. When the intended_verifier crate lands, migration is intended to be a crate swap against this same contract.

See also#

Rust Safety-Critical Firmware (Roadmap) | Intended