Rust SDK: pre-issued grants¶
Beta: a Git dependency, not on crates.io, and not yet part of the cross-language interoperability run. Crate:
genesis-mesh-sdk1.7.0.
A grant (1.6.0) is an authorisation the Network Authority (NA) signs in advance, so a device can make a known, bounded kind of change while it cannot reach the NA. Break-glass records a change and has it judged afterwards; a grant authorises it beforehand. When the NA issues a grant, it checks that every policy that applies allows every request the grant permits.
The NA issues grants only with EVIDENCE_GRANTS=on, which is off by
default; until then every grant route answers 404 grants_disabled. It
needs EVIDENCE_OUT_OF_BAND=on, which needs EVIDENCE_STORE=on; both are off
by default too. On the device, GrantHolder::act needs a record outbox
(ClientOptions::with_record_outbox, see Rust SDK: governed actions and records;
RecordOutboxRequired otherwise). The grant routes are beta; the signed records are
stable. The rules in full, and the limits of offline trust, are in
Pre-Issued Grants.
client.grants (GrantClient) calls the grant routes, and GrantHolder
acts within one grant on the device. Of the SDKs, only the TypeScript and
Rust SDKs (and the edge agent, which uses this crate) have grant holders.
Issuing and revoking¶
grants.issue (privileged) names one bearer (a registered executor key), the
capabilities, the basis (attestation_id or agreement), a validity of at
most 24 hours, a budget (max_uses), the uses one time token may anchor
(max_uses_per_time_token) and a closed parameter_envelope:
use genesis_mesh_sdk::{json, ExecutionRecorder, GenesisMeshClient, Value};
async fn issue(
admin: &GenesisMeshClient,
recorder: &ExecutionRecorder,
attestation_id: &str,
valid_until: &str, // RFC 3339, at most 24 hours ahead
) -> genesis_mesh_sdk::Result<Value> {
admin
.grants
.issue(json!({
"bearer_key_id": recorder.key_id(),
"capabilities": ["secret.manage"],
"attestation_id": attestation_id,
"valid_until": valid_until,
"max_uses": 3,
"max_uses_per_time_token": 2,
"parameter_envelope": {
"lifetime_days": {"number_range": {
"number_type": "integer", "minimum": 1, "maximum": 60}},
},
}))
.await
}
Each envelope parameter is one_of a set of scalars, subset_of a set of
strings, or a number_range (integer or number, inclusive or exclusive
bounds). The NA answers with the signed grant and its first time token, or
refuses naming the first problem (grants::ISSUANCE_REFUSALS; for example
grant_gate_may_deny, HTTP 422 and so GenesisMeshError::Validation, when
a value inside the envelope may be denied). err.code() gives the code.
grants.list, grants.get and grants.uses (read) read grants and a
grant’s use chain; grants.revoke (privileged) revokes one. A use made
before a revocation stands; one at or after it is refused.
Acting within a grant¶
A GrantHolder holds one grant for the device whose ExecutionRecorder
signs with the bearer key. Create it from what issue returned, then call
act:
use genesis_mesh_sdk::{
json, ActionError, ActionReport, ExecutionRecorder, GenesisMeshClient, GovernedVerification,
GrantActionOutcome, GrantFallback, GrantHolder, GrantRequest, Value,
};
fn hold(
issued: &Value,
recorder: ExecutionRecorder,
na_public_key: String,
) -> genesis_mesh_sdk::Result<GrantHolder> {
GrantHolder::from_issued(issued, recorder, vec![na_public_key])
}
async fn rotate(
gm: &GenesisMeshClient,
holder: &mut GrantHolder,
na_public_key: String,
) -> Result<(), Box<dyn std::error::Error>> {
let verify = GovernedVerification {
operator_public_keys: vec![na_public_key],
..Default::default()
};
let request = GrantRequest::new("secret.manage", json!({"lifetime_days": 30}))
.on("kv:site-7/api-key", "rotate")
.with_fallback(GrantFallback::new("the credential must rotate now", verify));
match holder
.act(gm, request, |_authority| async move {
Ok::<_, ActionError>(ActionReport::<()> {
execution_parameters: Some(json!({"version_id": "v9"})),
..ActionReport::default()
})
})
.await?
{
GrantActionOutcome::Used(used) => eprintln!("use {}", used.record["use_sequence"]),
GrantActionOutcome::FellBack { unusable, .. } => eprintln!("fell back: {}", unusable.code),
}
// Persist the holder after every use: `holder.state()`, `GrantHolder::restore`.
Ok(())
}
GrantHolder::new (or from_issued) checks that the grant and its time
token verify under the NA keys and name this recorder’s key as the bearer.
act then checks each use offline, in the NA’s order: the bearer key’s
resource prefix (set_resource_prefix), the capability, the envelope
(grants::envelope_problem: closed, numbers compared exactly, true is not
1), revocation (the feed the holder last read), the validity, the budget,
and the uses allowed per time token. It then runs the action, signs a use
chained to the previous one and citing the time token, keeps it in the record
outbox and submits it.
A request the grant does not cover (
grant_use_out_of_scope,grant_capability_mismatch,grant_envelope_mismatch), a grant not valid yet, and secret material in the parameters are refused before anything runs (GrantRefused, orOutOfBandRecordwithgrant_use_secret_material).Metadata the secret guard refuses after the action ran is left out of the use and named in
dropped; a failed action is recorded as a use with outcomefailure(ActionFailed).Keep
holder.state()(GrantHolderState, serde-serializable) durably and rebuild the holder withGrantHolder::restore. A holder that forgets its uses signs a second use at a position it already used, which the NA refuses.
Reconnecting and the fallback¶
On reconnecting, holder.sync(&gm) reads and verifies the revocation feed,
uploads the record outbox (the grant’s uses one at a time in chain order,
before observations) and asks for a new time token naming the head of the
holder’s chain. A use the NA refuses for good is a dead letter
(GRANT_USE_PERMANENT_REFUSALS); the NA keeps it as a quarantine entry and it
still takes its place in the chain. An earlier use not uploaded yet
(grant_use_chain_gap) and a use dated ahead of the NA’s clock
(grant_use_in_future) are retried.
When the grant has ended, its budget or time token is spent, it is revoked or
its bearer key is retired, act falls back with the request’s
GrantFallback to governed_action_with_break_glass with the grant’s
capability: the NA’s decision when it answers (a DENY does not run the
action), break-glass when it does not. Without a fallback the request is
refused (GrantRefused with the reason). A grant on an agreement needs
GrantFallback::evaluate.
Verifying grants¶
verify::verify_evidence_events, verify::verify_record and
audit_pack::verify_audit_pack check the grant entry kinds (grant,
grant_execution, grant_revocation) and time tokens: each signature, the
store’s grant rules (grant_use_chain_break, grant_missing,
grant_bearer_mismatch, grant_use_out_of_scope and others), the use rules
with the NA’s codes (grants::use_problem), and the revocation feed
(verify::verify_revocation_feed). Turn grants on only once every verifier
that reads your exports, packs and feed runs 1.6 or later: a 1.5 verifier
refuses the new kinds. See Rust SDK: offline verification.