Skip to content

guides

Intended Documentation

Rust Safety-Critical Firmware

Wire format spec for the Intended edge verifier. Reference Rust crate ships at crates/intended-verifier.

Integrating Intended in Rust Safety-Critical Firmware (DOC-P4)#

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

Status of the Rust SDK: not yet shipped. This guide describes the canonical wire format, the JWT claim shape, and the verification contract that the Rust SDK + edge verifier (TOK-P1) will implement. Use this as the spec for in-house verifier code while we ship.

Why the Rust SDK is the certified path#

The Rust SDK + edge verifier binary (TOK-P1) is the commercial product in this stack. Everything else — the Python SDK, the ROS2 wrapper, the HTTP API — exists to make the Rust verifier the right choice for the hot path. Eventually it ships:

  • no_std core (Rust core only) for embedded targets
  • A signed binary for industrial PCs and ARM gateways (TOK-P2)
  • 24-hour offline operation via JWKS cache (TOK-P3)
  • Sub-50ms verification latency at the edge
  • Targeted at IEC 61508 SIL-2 / ISO 13849 PLd compliance, with a certification path to SIL-3 / PLe

If you need authority gating in Rust before TOK-P1 ships, the recommended path is to verify against the cloud's published JWKS on a non-RT thread and pass bool ok to your RT loop.

Wire format (canonical, locked for v1)#

An Authority Token is an RS256 JWT with the following physical-AI claims on top of the standard set:

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

  "intended": {
    "version": 2,
    "oilCode": "OIL-1501",
    "tenantId": "tnt_…",

    "actorIdentity": "cobot-east-3",
    "actorIdentityKind": "ieee-802-1ar-devid",

    "deadlineMs": 200,
    "issuedAtMs": 1735689600000,
    "expiresAtMs": 1735689600200,

    "safeDefault": "hold-position",
    "safetyBit": true,
    "safetyCitations": ["OIL-2902"],

    "dagNodeId": "pick-step-1",
    "realTimeTier": "rt-soft",

    "physicalStateRef": "state_…",   // hash of the snapshot the cloud evaluated against
    "operatorTicketId": null         // populated only when a prior ESCALATE was approved
  }
}

exp is in seconds (RFC 7519); expiresAtMs is in milliseconds (operationally useful for sub-second TTLs that round to the same exp). Verifiers MUST enforce both.

Verification contract#

The verifier MUST reject the token if any of the following are true:

  1. Signature — does not validate against the issuer's published JWKS (https://api.intended.so/.well-known/jwks.json).
  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 robot manufacture).
  6. oilCode — 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.
  8. physicalStateRef — required to be present and recent (≤ deadlineMs). The verifier may optionally re-snapshot state and compare; the cloud-side snapshot is the authoritative one.

Verifiers MUST NOT rely on nbf for time-bounded operation; use expiresAtMs for sub-second windows.

Reference verifier sketch#

rust
// THIS IS A SPEC SKETCH. Use the shipped intended_verifier crate when
// available — it handles JWKS rotation, time attestation, side-channel
// resistance, and certification-relevant edge cases this sketch does not.

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

#[derive(Debug, Deserialize)]
struct IntendedClaims {
    version: u8,
    oil_code: String,
    actor_identity: String,
    deadline_ms: u32,
    issued_at_ms: i64,
    expires_at_ms: i64,
    safe_default: String,
    safety_bit: bool,
    safety_citations: Vec<String>,
    dag_node_id: String,
    real_time_tier: String,
    physical_state_ref: String,
    operator_ticket_id: Option<String>,
}

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

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

pub struct VerifierConfig {
    pub bound_actor_identity: String,    // your robot's DevID
    pub trusted_issuers: Vec<String>,    // ["https://api.intended.so"]
    pub policy_allowlist: Vec<String>,   // OIL codes this cell may execute
    pub jwks: jsonwebtoken::jwk::JwkSet, // cached locally per TOK-P3
}

pub struct VerifyOk {
    pub oil_code: String,
    pub safe_default: String,
    pub safety_bit: bool,
    pub expires_at_ms: i64,
}

pub fn verify(token: &str, cfg: &VerifierConfig, attested_now_ms: i64)
    -> Result<VerifyOk, VerifyError>
{
    // 1. Lookup signing key by `kid` in cached JWKS.
    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)?;

    // 2. Validate signature + standard claims.
    let mut validation = Validation::new(Algorithm::RS256);
    validation.set_audience(&["intended-edge-verifier"]);
    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(VerifyOk {
        oil_code:      claims.intended.oil_code,
        safe_default:  claims.intended.safe_default,
        safety_bit:    claims.intended.safety_bit,
        expires_at_ms: claims.intended.expires_at_ms,
    })
}

RT loop integration#

Verification is allocation-free with the right setup (heapless::Vec for the citations list, fixed-size buffers for claim strings). Target 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 inside one cycle. For sub-1ms loops, verify out-of-band on a separate core and pass a verified-flag into the control task via a lock-free queue.

Time attestation#

The verifier's attested_now_ms MUST come from PTP / NTP (AUD-P7), NOT from SystemTime::now(). Spoofing system time is the obvious attack on time-bound credentials. The shipped verifier integrates with linuxptp / chrony / Microchip TSN cards.

JWKS rotation + offline#

Cache JWKS to disk (TOK-P3 — 24-hour offline operation). Refresh:

  • Eagerly at startup
  • On every kid cache miss
  • Periodically (default: every 60 minutes)
  • Never blocking the RT path

If JWKS cache is older than 24 hours and refresh fails, the verifier SHOULD fail closed for new tokens but continue accepting in-flight tokens until their expiresAtMs.

Until the SDK ships#

A minimal in-house verifier following this spec is a few hundred lines of Rust against jsonwebtoken and reqwest. Treat it as uncertified until you adopt the shipped intended_verifier crate — the Intended certified primitive applies only to our binary.

When TOK-P1 lands, migration is a crate swap: the public API matches this spec.

See also#

Rust Safety-Critical Firmware | Intended