Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked.
manifests. Existing profile persistence, wildcard evaluation, bootstrap,
socket and wire behavior remain unchanged. Closes #1233. Refs #1228.

- **Capability-registry revision 1 now has fixed semantics and BLAKE3 digest
vectors.** All 51 kernel entries bind scope, target kinds, delegability,
privileged status and provenance; request mappings and current enforcement
roles fail tests if they drift outside the registry. Authorization and
persisted state remain unchanged. Closes #1235. Refs #1228 and #1233.

### Changed

- **Device key IDs now use BLAKE3.** The short per-device handle is derived from
Expand Down
170 changes: 168 additions & 2 deletions crates/astrid-core/src/capability_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use std::num::NonZeroU32;
use thiserror::Error;

use crate::capability_grammar::{
CapabilityDanger, CapabilityGrammarError, CapabilityScope, validate_capability,
CAPABILITY_CATALOG, CapabilityDanger, CapabilityGrammarError, CapabilityScope,
validate_capability,
};
use util::{
domain_hash, encode_array_len, encode_bool, encode_bytes, encode_text, encode_unsigned,
Expand Down Expand Up @@ -60,7 +61,6 @@ impl CapabilityRegistryRevision {
///
/// This set is authority-bearing and frozen for its registry schema revision.
/// Expanding it requires an intentional schema revision and reviewed digest vectors.
#[cfg(test)]
const CAPABILITY_REGISTRY_REVISION_1_IDS: [&str; 51] = [
"system:shutdown",
"system:status",
Expand Down Expand Up @@ -115,6 +115,160 @@ const CAPABILITY_REGISTRY_REVISION_1_IDS: [&str; 51] = [
"authority:repair",
];

/// Schema revision for the 51-ID authority registry.
pub const CAPABILITY_REGISTRY_REVISION_1: CapabilityRegistryRevision =
CapabilityRegistryRevision::new(NonZeroU32::MIN);

#[derive(Clone, Copy)]
struct RevisionSemantics {
scope: CapabilityScope,
target_kinds: &'static [AuthorityTargetKind],
delegable: bool,
privileged: bool,
}

/// Build the content-addressed registry for the 51 fixed capability IDs.
///
/// # Errors
///
/// Returns an error if an ID lacks fixed semantics or display metadata, or if
/// any definition fails registry validation.
pub fn capability_registry_revision_1() -> Result<CapabilityRegistryManifest, AuthorityRegistryError>
{
let entries = CAPABILITY_REGISTRY_REVISION_1_IDS
.into_iter()
.map(|id| {
let semantics = revision_1_semantics(id).ok_or_else(|| {
AuthorityRegistryError::MissingRevisionDefinition { id: id.to_string() }
})?;
let danger = revision_1_danger(id).ok_or_else(|| {
AuthorityRegistryError::MissingRevisionDisplayMetadata { id: id.to_string() }
})?;
RegisteredCapability::new(
ExactCapabilityId::new(id.to_string())?,
semantics.scope,
semantics.target_kinds.iter().copied(),
danger,
semantics.delegable,
semantics.privileged,
CapabilitySource::Kernel,
)
})
.collect::<Result<Vec<_>, AuthorityRegistryError>>()?;

CapabilityRegistryManifest::new(CAPABILITY_REGISTRY_REVISION_1, entries)
}

fn revision_1_danger(id: &str) -> Option<CapabilityDanger> {
CAPABILITY_CATALOG
.iter()
.find(|entry| entry.id == id)
.map(|entry| entry.danger)
.or_else(|| {
matches!(
id,
"system:resources:unbounded"
| "net_bind"
| "uplink"
| "capsule:access:any"
| "authority:profile:manage"
| "authority:repair"
)
.then_some(CapabilityDanger::Extreme)
})
}

fn revision_1_semantics(id: &str) -> Option<RevisionSemantics> {
use AuthorityTargetKind::{
AuditScope, CapsuleInstance, CapsulePackage, Credential, Group, Principal, System,
};
use CapabilityScope::{Global, Self_};

let semantics = match id {
"system:shutdown" => RevisionSemantics::new(Global, &[System], false, true),
"system:status" => RevisionSemantics::new(Global, &[System], false, false),
"capsule:install" => RevisionSemantics::new(Global, &[System, CapsulePackage], true, true),
"self:capsule:install" => {
RevisionSemantics::new(Self_, &[Principal, CapsulePackage], true, false)
},
"capsule:reload" | "capsule:remove" => {
RevisionSemantics::new(Global, &[System, CapsuleInstance], true, true)
},
"self:capsule:reload"
| "self:capsule:remove"
| "self:workspace:promote"
| "self:workspace:rollback" => {
RevisionSemantics::new(Self_, &[Principal, CapsuleInstance], true, false)
},
"capsule:list" | "agent:list" | "group:list" | "invite:list" => {
RevisionSemantics::new(Global, &[System], true, true)
},
"self:capsule:list"
| "self:agent:list"
| "self:group:list"
| "self:quota:get"
| "self:approval:respond" => RevisionSemantics::new(Self_, &[Principal], true, false),
"agent:create" | "agent:create:clone" | "agent:modify" => {
RevisionSemantics::new(Global, &[Principal, Group, CapsulePackage], true, true)
},
"agent:create:inherit"
| "agent:delete"
| "agent:enable"
| "agent:disable"
| "quota:set"
| "quota:get"
| "caps:grant"
| "caps:revoke"
| "caps:token:list" => RevisionSemantics::new(Global, &[Principal], true, true),
"self:quota:set" => RevisionSemantics::new(Self_, &[Principal], true, true),
"group:create" | "group:delete" | "group:modify" => {
RevisionSemantics::new(Global, &[Group], true, true)
},
"caps:token:mint" | "caps:token:revoke" => {
RevisionSemantics::new(Global, &[Principal, Credential], true, true)
},
"invite:issue" => RevisionSemantics::new(Global, &[Group, Credential], true, true),
"invite:redeem" => {
RevisionSemantics::new(Global, &[Principal, Group, Credential], false, true)
},
"invite:revoke" => RevisionSemantics::new(Global, &[Credential], true, true),
"audit:read_all" => RevisionSemantics::new(Global, &[AuditScope], true, true),
"self:auth:pair" => RevisionSemantics::new(Self_, &[Principal, Credential], true, true),
"self:auth:pair:admin" => {
RevisionSemantics::new(Self_, &[Principal, Credential], false, true)
},
"auth:pair:redeem" => RevisionSemantics::new(Global, &[Principal, Credential], false, true),
"auth:pair" => RevisionSemantics::new(Global, &[Principal, Credential], true, true),
"system:resources:unbounded" | "net_bind" | "uplink" => {
RevisionSemantics::new(Self_, &[Principal, CapsuleInstance], false, true)
},
"capsule:access:any" => {
RevisionSemantics::new(Self_, &[CapsulePackage, CapsuleInstance], false, true)
},
"authority:profile:manage" | "authority:repair" => {
RevisionSemantics::new(Global, &[System, Principal, Group, Credential], false, true)
},
_ => return None,
};
Some(semantics)
}

impl RevisionSemantics {
const fn new(
scope: CapabilityScope,
target_kinds: &'static [AuthorityTargetKind],
delegable: bool,
privileged: bool,
) -> Self {
Self {
scope,
target_kinds,
delegable,
privileged,
}
}
}

/// A validated capability identifier containing no wildcard segment.
///
/// The generic storage parameter permits borrowed views at validation and
Expand Down Expand Up @@ -653,6 +807,18 @@ impl CapabilityRegistryManifest {
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AuthorityRegistryError {
/// A fixed capability ID has no authorization definition.
#[error("capability-registry revision 1 entry {id:?} has no authorization definition")]
MissingRevisionDefinition {
/// Capability identifier.
id: String,
},
/// A fixed capability ID has no danger classification.
#[error("capability-registry revision 1 entry {id:?} has no display metadata")]
MissingRevisionDisplayMetadata {
/// Capability identifier.
id: String,
},
/// A capability ID failed the existing static capability grammar.
#[error("invalid capability id {id:?}: {source}")]
InvalidCapabilityId {
Expand Down
Loading