use crate::id::Id;
use crate::id_hex;
pub const PERM_READ: Id = id_hex!("A75EED8224A553DD8002576E2E8A6823");
pub const PERM_WRITE: Id = id_hex!("C56AAF4191DD4FBB9F197B79435B881D");
pub const PERM_ADMIN: Id = id_hex!("EC68A0CBF9EF421F59A0A69ED80FD79F");
use crate::inline::encodings::ed25519 as ed;
use crate::blob::encodings::simplearchive::SimpleArchive;
use crate::inline::encodings::genid::GenId;
use crate::inline::encodings::hash::Handle;
triblespace_core_macros::attributes! {
"1A8A6A9D8CA1DA67FACAB373DE21233B" as pub cap_subject: ed::ED25519PublicKey;
"2E9CD97ED0698FAF18EAEB74B5893685" as pub cap_issuer: ed::ED25519PublicKey;
"1A7DD2026BEFBE55A354CE10839CFDD6" as pub cap_scope_root: GenId;
"E825B3A8D387B4DAE1720B0EDCBFAA9E" as pub cap_parent: Handle<SimpleArchive>;
"008F7784A309CA9DEF007E4F63F87121" as pub cap_embedded_parent_sig: GenId;
"46246789D627C1B0F81B21418E179DFD" as pub scope_branch: GenId;
"230E175A083E29155C860B38BD44F2F3" as pub sig_signs: Handle<SimpleArchive>;
"E146824999D1DA7F1F0E54025F52EE13" as pub rev_target: ed::ED25519PublicKey;
}
#[allow(dead_code)]
pub const KIND_CAPABILITY: Id = id_hex!("B8D76786ACD20F344A4E5CBFC0F75772");
#[allow(dead_code)]
pub const KIND_CAPABILITY_SIG: Id = id_hex!("E6BB52CE6E02D51C3676ECE1EEA9094F");
#[allow(dead_code)]
pub const KIND_REVOCATION: Id = id_hex!("1EEAF2CF25A776547A26080E755D111C");
use ed25519::Signature;
use ed25519_dalek::SigningKey;
use ed25519_dalek::VerifyingKey;
use ed25519::signature::Signer;
use crate::blob::Blob;
use crate::blob::IntoBlob;
use crate::blob::TryFromBlob;
use crate::blob::encodings::simplearchive::UnarchiveError;
use crate::id::ExclusiveId;
use crate::macros::entity;
use crate::macros::pattern;
use crate::query::find;
use crate::trible::TribleSet;
use crate::inline::Inline;
use crate::inline::IntoInline;
use crate::inline::encodings::time::NsTAIInterval;
#[derive(Debug)]
pub enum BuildError {
ParseParentSig(UnarchiveError),
ParentSigShape,
}
pub fn build_capability(
issuer: &SigningKey,
subject: VerifyingKey,
parent: Option<(Blob<SimpleArchive>, Blob<SimpleArchive>)>,
scope_root: crate::id::Id,
scope_facts: TribleSet,
expiry: Inline<NsTAIInterval>,
) -> Result<(Blob<SimpleArchive>, Blob<SimpleArchive>), BuildError> {
let issuer_pubkey: VerifyingKey = issuer.verifying_key();
let cap_fragment = entity! {
cap_subject: issuer_subject_value(subject),
cap_issuer: issuer_subject_value(issuer_pubkey),
cap_scope_root: scope_root,
crate::metadata::expires_at: expiry,
};
let cap_id = cap_fragment
.root()
.expect("entity! always produces a rooted fragment");
let mut cap_set = TribleSet::from(cap_fragment);
cap_set += scope_facts;
if let Some((parent_cap_blob, parent_sig_blob)) = parent {
let parent_cap_handle: Inline<Handle<SimpleArchive>> =
parent_cap_blob.get_handle();
let parent_sig_set: TribleSet = TryFromBlob::<SimpleArchive>::try_from_blob(
parent_sig_blob,
)
.map_err(BuildError::ParseParentSig)?;
let mut sig_id_iter = find!(
(sig: crate::id::Id, _signed: Inline<Handle<SimpleArchive>>),
pattern!(&parent_sig_set, [{ ?sig @ sig_signs: ?_signed }])
)
.map(|(sig, _)| sig);
let sig_id = match (sig_id_iter.next(), sig_id_iter.next()) {
(Some(sig), None) => sig,
_ => return Err(BuildError::ParentSigShape),
};
cap_set += parent_sig_set;
cap_set += TribleSet::from(entity! { ExclusiveId::force_ref(&cap_id) @
cap_parent: parent_cap_handle,
cap_embedded_parent_sig: sig_id,
});
}
let cap_blob: Blob<SimpleArchive> = cap_set.to_blob();
let signature: Signature = issuer.sign(&cap_blob.bytes);
let cap_handle: Inline<Handle<SimpleArchive>> =
(&cap_blob).get_handle();
let sig_fragment = entity! {
sig_signs: cap_handle,
crate::repo::signed_by: issuer_pubkey,
crate::repo::signature_r: signature,
crate::repo::signature_s: signature,
};
let sig_blob: Blob<SimpleArchive> = TribleSet::from(sig_fragment).to_blob();
Ok((cap_blob, sig_blob))
}
fn issuer_subject_value(key: VerifyingKey) -> Inline<ed::ED25519PublicKey> {
key.to_inline()
}
fn collect_scope_facts(
set: &TribleSet,
scope_root: crate::id::Id,
) -> (HashSet<crate::id::Id>, HashSet<crate::id::Id>) {
let perms: HashSet<crate::id::Id> = find!(
(perm: crate::id::Id),
pattern!(set, [{ scope_root @ crate::metadata::tag: ?perm }])
)
.map(|(p,)| p)
.collect();
let branches: HashSet<crate::id::Id> = find!(
(branch: crate::id::Id),
pattern!(set, [{ scope_root @ scope_branch: ?branch }])
)
.map(|(b,)| b)
.collect();
(perms, branches)
}
pub fn scope_subsumes(
parent_set: &TribleSet,
parent_scope_root: crate::id::Id,
child_set: &TribleSet,
child_scope_root: crate::id::Id,
) -> bool {
let (parent_perms, parent_branches) =
collect_scope_facts(parent_set, parent_scope_root);
let (child_perms, child_branches) =
collect_scope_facts(child_set, child_scope_root);
if parent_perms.contains(&PERM_ADMIN) {
return true;
}
for perm in &child_perms {
if *perm == PERM_READ {
if !parent_perms.contains(&PERM_READ)
&& !parent_perms.contains(&PERM_WRITE)
{
return false;
}
} else if *perm == PERM_WRITE {
if !parent_perms.contains(&PERM_WRITE) {
return false;
}
} else if *perm == PERM_ADMIN {
return false;
} else {
return false;
}
}
if !parent_branches.is_empty() {
if child_branches.is_empty() {
return false;
}
for b in &child_branches {
if !parent_branches.contains(b) {
return false;
}
}
}
true
}
pub fn build_revocation(
revoker: &SigningKey,
target: VerifyingKey,
) -> (Blob<SimpleArchive>, Blob<SimpleArchive>) {
let now = hifitime::Epoch::now().expect("system time");
let timestamp: Inline<NsTAIInterval> =
(now, now).try_to_inline().expect("point interval");
let rev_fragment = entity! {
rev_target: target.to_inline(),
crate::metadata::created_at: timestamp,
};
let rev_blob: Blob<SimpleArchive> = TribleSet::from(rev_fragment).to_blob();
let signature: Signature = revoker.sign(&rev_blob.bytes);
let revoker_pubkey = revoker.verifying_key();
let rev_handle: Inline<Handle<SimpleArchive>> =
(&rev_blob).get_handle();
let sig_fragment = entity! {
sig_signs: rev_handle,
crate::repo::signed_by: revoker_pubkey,
crate::repo::signature_r: signature,
crate::repo::signature_s: signature,
};
let sig_blob: Blob<SimpleArchive> = TribleSet::from(sig_fragment).to_blob();
(rev_blob, sig_blob)
}
pub fn verify_revocation(
rev_blob: Blob<SimpleArchive>,
sig_blob: Blob<SimpleArchive>,
) -> Result<(VerifyingKey, VerifyingKey), VerifyError> {
let sig_set: TribleSet = TryFromBlob::try_from_blob(sig_blob)?;
let revoker = verify_sig_blob(&sig_set, &rev_blob)?;
let rev_set: TribleSet = TryFromBlob::try_from_blob(rev_blob)?;
let mut iter = find!(
(rev: crate::id::Id, target: VerifyingKey),
pattern!(&rev_set, [{ ?rev @ rev_target: ?target }])
);
let (_rev_id, target) = match (iter.next(), iter.next()) {
(Some(row), None) => row,
_ => return Err(VerifyError::MalformedCap),
};
Ok((revoker, target))
}
pub fn build_revocation_set<I>(
authorised_revokers: &HashSet<VerifyingKey>,
revocations: I,
) -> HashSet<VerifyingKey>
where
I: IntoIterator<Item = (Blob<SimpleArchive>, Blob<SimpleArchive>)>,
{
let mut revoked: HashSet<VerifyingKey> = HashSet::new();
for (rev_blob, sig_blob) in revocations {
if let Ok((revoker, target)) = verify_revocation(rev_blob, sig_blob) {
if authorised_revokers.contains(&revoker) {
revoked.insert(target);
}
}
}
revoked
}
pub fn extract_revocation_pairs<I>(
blobs: I,
) -> Vec<(Blob<SimpleArchive>, Blob<SimpleArchive>)>
where
I: IntoIterator<Item = Blob<SimpleArchive>>,
{
let blob_map: std::collections::HashMap<[u8; 32], Blob<SimpleArchive>> = blobs
.into_iter()
.map(|b| {
let h: Inline<Handle<SimpleArchive>> = (&b).get_handle();
(h.raw, b)
})
.collect();
let mut pairs = Vec::new();
for candidate_sig in blob_map.values() {
let Ok(sig_set): Result<TribleSet, _> =
TryFromBlob::try_from_blob(candidate_sig.clone())
else {
continue;
};
let mut sig_iter = find!(
(sig: crate::id::Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&sig_set, [{ ?sig @ sig_signs: ?h }])
);
let target_handle = match (sig_iter.next(), sig_iter.next()) {
(Some((_, h)), None) => h,
_ => continue,
};
let Some(target_blob) = blob_map.get(&target_handle.raw) else {
continue;
};
let Ok(target_set): Result<TribleSet, _> =
TryFromBlob::try_from_blob(target_blob.clone())
else {
continue;
};
let is_revocation = find!(
(e: crate::id::Id, t: VerifyingKey),
pattern!(&target_set, [{ ?e @ rev_target: ?t }])
)
.next()
.is_some();
if !is_revocation {
continue;
}
pairs.push((target_blob.clone(), candidate_sig.clone()));
}
pairs
}
use ed25519_dalek::Verifier;
use std::collections::HashSet;
use crate::inline::TryFromInline;
use crate::inline::TryToInline;
use hifitime::Epoch;
#[derive(Debug)]
pub enum VerifyError {
ParseBlob(UnarchiveError),
Fetch,
BadSignature,
SubjectMismatch,
IssuerMismatch,
Expired,
Revoked,
ScopeNotSubset,
MalformedCap,
MalformedSig,
LeafCapMissing,
NonRootMissingParent,
ChainTooDeep,
}
impl From<UnarchiveError> for VerifyError {
fn from(e: UnarchiveError) -> Self {
VerifyError::ParseBlob(e)
}
}
#[derive(Debug, Clone)]
pub struct VerifiedCapability {
pub subject: VerifyingKey,
pub scope_root: crate::id::Id,
pub cap_set: TribleSet,
}
impl VerifiedCapability {
pub fn permissions(&self) -> HashSet<crate::id::Id> {
let (perms, _) = collect_scope_facts(&self.cap_set, self.scope_root);
perms
}
pub fn granted_branches(&self) -> Option<HashSet<crate::id::Id>> {
let (_, branches) = collect_scope_facts(&self.cap_set, self.scope_root);
if branches.is_empty() { None } else { Some(branches) }
}
pub fn grants_read(&self) -> bool {
let perms = self.permissions();
perms.contains(&PERM_READ)
|| perms.contains(&PERM_WRITE)
|| perms.contains(&PERM_ADMIN)
}
pub fn grants_read_on(&self, branch: &crate::id::Id) -> bool {
if !self.grants_read() {
return false;
}
match self.granted_branches() {
None => true,
Some(set) => set.contains(branch),
}
}
}
pub const MAX_CHAIN_DEPTH: usize = 32;
fn verify_sig_blob(
sig_set: &TribleSet,
cap_blob: &Blob<SimpleArchive>,
) -> Result<VerifyingKey, VerifyError> {
let cap_handle: Inline<Handle<SimpleArchive>> = cap_blob.get_handle();
let mut iter = find!(
(sig: crate::id::Id, signer: VerifyingKey, r, s),
pattern!(sig_set, [{
?sig @
sig_signs: cap_handle,
crate::repo::signed_by: ?signer,
crate::repo::signature_r: ?r,
crate::repo::signature_s: ?s,
}])
);
let (_sig_id, signer, r, s) = match (iter.next(), iter.next()) {
(Some(row), None) => row,
_ => return Err(VerifyError::MalformedSig),
};
let signature = Signature::from_components(r, s);
signer
.verify(&cap_blob.bytes, &signature)
.map_err(|_| VerifyError::BadSignature)?;
Ok(signer)
}
fn extract_cap_fields(
cap_set: &TribleSet,
) -> Result<CapFields, VerifyError> {
let mut iter = find!(
(cap: crate::id::Id,
subject: VerifyingKey,
issuer: VerifyingKey,
scope_root: crate::id::Id,
expiry: Inline<NsTAIInterval>),
pattern!(cap_set, [{
?cap @
cap_subject: ?subject,
cap_issuer: ?issuer,
cap_scope_root: ?scope_root,
crate::metadata::expires_at: ?expiry,
}])
);
let (cap_id, subject, issuer, scope_root, expiry) = match (iter.next(), iter.next()) {
(Some(row), None) => row,
_ => return Err(VerifyError::MalformedCap),
};
let parent_handle: Option<Inline<Handle<SimpleArchive>>> = find!(
(h: Inline<Handle<SimpleArchive>>),
pattern!(cap_set, [{ cap_id @ cap_parent: ?h }])
)
.next()
.map(|(h,)| h);
let embedded_sig: Option<crate::id::Id> = find!(
(s: crate::id::Id),
pattern!(cap_set, [{ cap_id @ cap_embedded_parent_sig: ?s }])
)
.next()
.map(|(s,)| s);
Ok(CapFields {
cap_id,
subject,
issuer,
scope_root,
expiry,
parent_handle,
embedded_sig,
})
}
#[derive(Debug, Clone)]
struct CapFields {
#[allow(dead_code)]
cap_id: crate::id::Id,
subject: VerifyingKey,
issuer: VerifyingKey,
scope_root: crate::id::Id,
expiry: Inline<NsTAIInterval>,
parent_handle: Option<Inline<Handle<SimpleArchive>>>,
embedded_sig: Option<crate::id::Id>,
}
pub fn verify_chain<F>(
team_root: VerifyingKey,
leaf_sig_handle: Inline<Handle<SimpleArchive>>,
expected_subject: VerifyingKey,
revoked: &HashSet<VerifyingKey>,
mut fetch_blob: F,
) -> Result<VerifiedCapability, VerifyError>
where
F: FnMut(Inline<Handle<SimpleArchive>>) -> Option<Blob<SimpleArchive>>,
{
let now: Epoch = hifitime::Epoch::now().expect("system time");
let is_expired = |expiry: &Inline<NsTAIInterval>| -> bool {
match <(Epoch, Epoch)>::try_from_inline(expiry) {
Ok((_lower, upper)) => upper < now,
Err(_) => true,
}
};
let leaf_sig_blob = fetch_blob(leaf_sig_handle).ok_or(VerifyError::Fetch)?;
let leaf_sig_set: TribleSet = TryFromBlob::try_from_blob(leaf_sig_blob)?;
let mut leaf_cap_handle_iter = find!(
(sig: crate::id::Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&leaf_sig_set, [{
?sig @ sig_signs: ?h,
}])
);
let (_sig_id, leaf_cap_handle) = match (
leaf_cap_handle_iter.next(),
leaf_cap_handle_iter.next(),
) {
(Some(row), None) => row,
_ => return Err(VerifyError::MalformedSig),
};
let leaf_cap_blob = fetch_blob(leaf_cap_handle).ok_or(VerifyError::LeafCapMissing)?;
let leaf_signer = verify_sig_blob(&leaf_sig_set, &leaf_cap_blob)?;
let leaf_cap_set: TribleSet = TryFromBlob::try_from_blob(leaf_cap_blob.clone())?;
let leaf_fields = extract_cap_fields(&leaf_cap_set)?;
if leaf_fields.subject != expected_subject {
return Err(VerifyError::SubjectMismatch);
}
if leaf_signer != leaf_fields.issuer {
return Err(VerifyError::IssuerMismatch);
}
if is_expired(&leaf_fields.expiry) {
return Err(VerifyError::Expired);
}
if revoked.contains(&leaf_fields.issuer)
|| revoked.contains(&leaf_fields.subject)
{
return Err(VerifyError::Revoked);
}
let mut current_set = leaf_cap_set.clone();
let mut current_fields = leaf_fields.clone();
let mut depth = 0usize;
loop {
if current_fields.issuer == team_root {
return Ok(VerifiedCapability {
subject: leaf_fields.subject,
scope_root: leaf_fields.scope_root,
cap_set: leaf_cap_set,
});
}
depth += 1;
if depth > MAX_CHAIN_DEPTH {
return Err(VerifyError::ChainTooDeep);
}
let parent_handle = current_fields
.parent_handle
.ok_or(VerifyError::NonRootMissingParent)?;
let embedded_sig_id = current_fields
.embedded_sig
.ok_or(VerifyError::NonRootMissingParent)?;
let parent_cap_blob = fetch_blob(parent_handle).ok_or(VerifyError::Fetch)?;
let mut sig_facts = find!(
(signer: VerifyingKey, r, s),
pattern!(¤t_set, [{
embedded_sig_id @
crate::repo::signed_by: ?signer,
crate::repo::signature_r: ?r,
crate::repo::signature_s: ?s,
}])
);
let (parent_signer, r, s) = match (sig_facts.next(), sig_facts.next()) {
(Some(row), None) => row,
_ => return Err(VerifyError::MalformedSig),
};
let signature = Signature::from_components(r, s);
parent_signer
.verify(&parent_cap_blob.bytes, &signature)
.map_err(|_| VerifyError::BadSignature)?;
let parent_set: TribleSet = TryFromBlob::try_from_blob(parent_cap_blob)?;
let parent_fields = extract_cap_fields(&parent_set)?;
if parent_signer != parent_fields.issuer {
return Err(VerifyError::IssuerMismatch);
}
if is_expired(&parent_fields.expiry) {
return Err(VerifyError::Expired);
}
if revoked.contains(&parent_fields.issuer)
|| revoked.contains(&parent_fields.subject)
{
return Err(VerifyError::Revoked);
}
if !scope_subsumes(
&parent_set,
parent_fields.scope_root,
¤t_set,
current_fields.scope_root,
) {
return Err(VerifyError::ScopeNotSubset);
}
current_set = parent_set;
current_fields = parent_fields;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inline::TryToInline;
use ed25519_dalek::Verifier;
use hifitime::Epoch;
use rand::rngs::OsRng;
fn now_plus_24h() -> Inline<NsTAIInterval> {
let now = Epoch::now().expect("system time");
let later = now + hifitime::Duration::from_seconds(24.0 * 3600.0);
(now, later).try_to_inline().expect("valid interval")
}
fn signing_key() -> SigningKey {
SigningKey::generate(&mut OsRng)
}
fn empty_scope() -> (Id, TribleSet) {
let scope_root = crate::id::ufoid();
let scope_facts = entity! { ExclusiveId::force_ref(&scope_root) @
crate::metadata::tag: PERM_READ,
};
(*scope_root, TribleSet::from(scope_facts))
}
fn scope_with(perms: &[Id], branches: &[Id]) -> (Id, TribleSet) {
let scope_root = crate::id::ufoid();
let mut facts = TribleSet::new();
for perm in perms {
facts += TribleSet::from(entity! {
ExclusiveId::force_ref(&scope_root) @
crate::metadata::tag: *perm,
});
}
for b in branches {
facts += TribleSet::from(entity! {
ExclusiveId::force_ref(&scope_root) @
scope_branch: *b,
});
}
(*scope_root, facts)
}
#[test]
fn build_root_capability() {
let team_root = signing_key();
let founder = signing_key();
let (scope_root, scope_facts) = empty_scope();
let expiry = now_plus_24h();
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
expiry,
)
.expect("root cap builds");
let sig_set: TribleSet =
<TribleSet as TryFromBlob<SimpleArchive>>::try_from_blob(sig_blob)
.expect("valid sig blob");
let mut sig_iter = find!(
(sig: Id,
handle: Inline<Handle<SimpleArchive>>,
pubkey: VerifyingKey,
r,
s),
pattern!(&sig_set, [{
?sig @
sig_signs: ?handle,
crate::repo::signed_by: ?pubkey,
crate::repo::signature_r: ?r,
crate::repo::signature_s: ?s,
}])
);
let (_sig_entity, signed_handle, recovered_pubkey, r_v, s_v) =
sig_iter.next().expect("exactly one sig entity");
assert!(sig_iter.next().is_none(), "exactly one sig entity");
let cap_handle: Inline<Handle<SimpleArchive>> =
(&cap_blob).get_handle();
assert_eq!(signed_handle, cap_handle);
assert_eq!(recovered_pubkey, team_root.verifying_key());
let signature = ed25519::Signature::from_components(r_v, s_v);
team_root
.verifying_key()
.verify(&cap_blob.bytes, &signature)
.expect("signature verifies over the cap blob bytes");
let cap_set: TribleSet =
<TribleSet as TryFromBlob<SimpleArchive>>::try_from_blob(cap_blob)
.expect("valid cap blob");
let parents: usize = find!(
(e: Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&cap_set, [{ ?e @ cap_parent: ?h }])
)
.count();
assert_eq!(parents, 0, "root cap has no cap_parent");
let embedded: usize = find!(
(e: Id, sig: Id),
pattern!(&cap_set, [{ ?e @ cap_embedded_parent_sig: ?sig }])
)
.count();
assert_eq!(embedded, 0, "root cap has no embedded parent sig");
}
fn store_for(blobs: &[&Blob<SimpleArchive>])
-> impl FnMut(Inline<Handle<SimpleArchive>>) -> Option<Blob<SimpleArchive>>
{
let mut map = std::collections::HashMap::new();
for blob in blobs {
let handle: Inline<Handle<SimpleArchive>> = (*blob).get_handle();
map.insert(handle.raw, (*blob).clone());
}
move |h: Inline<Handle<SimpleArchive>>| map.get(&h.raw).cloned()
}
#[test]
fn verify_root_chain() {
let team_root = signing_key();
let founder = signing_key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("root cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let revoked = HashSet::new();
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
founder.verifying_key(),
&revoked,
store_for(&[&cap_blob, &sig_blob]),
);
let verified = result.expect("chain verifies");
assert_eq!(verified.subject, founder.verifying_key());
assert_eq!(verified.scope_root, scope_root);
}
#[test]
fn verify_delegated_chain() {
let team_root = signing_key();
let founder = signing_key();
let member = signing_key();
let (founder_scope_root, founder_scope_facts) = empty_scope();
let (founder_cap, founder_sig) = build_capability(
&team_root,
founder.verifying_key(),
None,
founder_scope_root,
founder_scope_facts,
now_plus_24h(),
)
.expect("founder cap builds");
let (member_scope_root, member_scope_facts) = empty_scope();
let (member_cap, member_sig) = build_capability(
&founder,
member.verifying_key(),
Some((founder_cap.clone(), founder_sig.clone())),
member_scope_root,
member_scope_facts,
now_plus_24h(),
)
.expect("member cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&member_sig).get_handle();
let revoked = HashSet::new();
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
member.verifying_key(),
&revoked,
store_for(&[
&founder_cap,
&founder_sig,
&member_cap,
&member_sig,
]),
);
let verified = result.expect("chain verifies");
assert_eq!(verified.subject, member.verifying_key());
assert_eq!(verified.scope_root, member_scope_root);
}
#[test]
fn reject_subject_mismatch() {
let team_root = signing_key();
let founder = signing_key();
let attacker = signing_key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let revoked = HashSet::new();
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
attacker.verifying_key(), &revoked,
store_for(&[&cap_blob, &sig_blob]),
);
assert!(matches!(result, Err(VerifyError::SubjectMismatch)));
}
#[test]
fn reject_wrong_team_root() {
let real_root = signing_key();
let founder = signing_key();
let bogus_root = signing_key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&real_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let revoked = HashSet::new();
let result = verify_chain(
bogus_root.verifying_key(), leaf_handle,
founder.verifying_key(),
&revoked,
store_for(&[&cap_blob, &sig_blob]),
);
assert!(matches!(result, Err(VerifyError::NonRootMissingParent)));
}
#[test]
fn reject_revoked_subject() {
let team_root = signing_key();
let founder = signing_key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let mut revoked = HashSet::new();
revoked.insert(founder.verifying_key());
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
founder.verifying_key(),
&revoked,
store_for(&[&cap_blob, &sig_blob]),
);
assert!(matches!(result, Err(VerifyError::Revoked)));
}
#[test]
fn reject_revoked_intermediate_issuer() {
let team_root = signing_key();
let founder = signing_key();
let member = signing_key();
let (founder_scope_root, founder_scope_facts) = empty_scope();
let (founder_cap, founder_sig) = build_capability(
&team_root,
founder.verifying_key(),
None,
founder_scope_root,
founder_scope_facts,
now_plus_24h(),
)
.expect("founder cap builds");
let (member_scope_root, member_scope_facts) = empty_scope();
let (member_cap, member_sig) = build_capability(
&founder,
member.verifying_key(),
Some((founder_cap.clone(), founder_sig.clone())),
member_scope_root,
member_scope_facts,
now_plus_24h(),
)
.expect("member cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&member_sig).get_handle();
let mut revoked = HashSet::new();
revoked.insert(founder.verifying_key());
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
member.verifying_key(),
&revoked,
store_for(&[
&founder_cap,
&founder_sig,
&member_cap,
&member_sig,
]),
);
assert!(matches!(result, Err(VerifyError::Revoked)));
}
#[test]
fn admin_subsumes_anything() {
let (parent_root, parent_set) = scope_with(&[PERM_ADMIN], &[]);
let (child_root, child_set) = scope_with(&[PERM_WRITE, PERM_READ], &[]);
assert!(scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn write_implies_read_for_child() {
let (parent_root, parent_set) = scope_with(&[PERM_WRITE], &[]);
let (child_root, child_set) = scope_with(&[PERM_READ], &[]);
assert!(scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn read_does_not_imply_write() {
let (parent_root, parent_set) = scope_with(&[PERM_READ], &[]);
let (child_root, child_set) = scope_with(&[PERM_WRITE], &[]);
assert!(!scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn child_cannot_claim_admin_under_non_admin_parent() {
let (parent_root, parent_set) = scope_with(&[PERM_WRITE], &[]);
let (child_root, child_set) = scope_with(&[PERM_ADMIN], &[]);
assert!(!scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn unrestricted_parent_subsumes_branch_restricted_child() {
let branch_a = *crate::id::ufoid();
let (parent_root, parent_set) = scope_with(&[PERM_READ], &[]);
let (child_root, child_set) = scope_with(&[PERM_READ], &[branch_a]);
assert!(scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn restricted_parent_rejects_unrestricted_child() {
let branch_a = *crate::id::ufoid();
let (parent_root, parent_set) = scope_with(&[PERM_READ], &[branch_a]);
let (child_root, child_set) = scope_with(&[PERM_READ], &[]);
assert!(!scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn restricted_parent_subsumes_strict_subset() {
let branch_a = *crate::id::ufoid();
let branch_b = *crate::id::ufoid();
let (parent_root, parent_set) =
scope_with(&[PERM_READ], &[branch_a, branch_b]);
let (child_root, child_set) = scope_with(&[PERM_READ], &[branch_a]);
assert!(scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn restricted_parent_rejects_disjoint_child() {
let branch_a = *crate::id::ufoid();
let branch_b = *crate::id::ufoid();
let (parent_root, parent_set) = scope_with(&[PERM_READ], &[branch_a]);
let (child_root, child_set) = scope_with(&[PERM_READ], &[branch_b]);
assert!(!scope_subsumes(&parent_set, parent_root, &child_set, child_root));
}
#[test]
fn revocation_round_trip() {
let revoker = signing_key();
let target = signing_key();
let (rev_blob, sig_blob) =
build_revocation(&revoker, target.verifying_key());
let (out_revoker, out_target) =
verify_revocation(rev_blob, sig_blob).expect("verifies");
assert_eq!(out_revoker, revoker.verifying_key());
assert_eq!(out_target, target.verifying_key());
}
#[test]
fn revocation_set_filters_unauthorised_revokers() {
let team_root = signing_key();
let bystander = signing_key();
let target_a = signing_key();
let target_b = signing_key();
let mut authorised = HashSet::new();
authorised.insert(team_root.verifying_key());
let rev_a =
build_revocation(&team_root, target_a.verifying_key());
let rev_b =
build_revocation(&bystander, target_b.verifying_key());
let revoked =
build_revocation_set(&authorised, [rev_a, rev_b]);
assert!(revoked.contains(&target_a.verifying_key()));
assert!(!revoked.contains(&target_b.verifying_key()));
}
#[test]
fn revocation_set_invalidates_chain() {
let team_root = signing_key();
let founder = signing_key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let (rev_blob, rev_sig_blob) =
build_revocation(&team_root, founder.verifying_key());
let mut authorised = HashSet::new();
authorised.insert(team_root.verifying_key());
let revoked = build_revocation_set(
&authorised,
[(rev_blob, rev_sig_blob)],
);
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
founder.verifying_key(),
&revoked,
store_for(&[&cap_blob, &sig_blob]),
);
assert!(matches!(result, Err(VerifyError::Revoked)));
}
#[test]
fn extract_revocation_pairs_finds_matched_pair() {
let team_root = signing_key();
let target = signing_key();
let (rev_blob, sig_blob) =
build_revocation(&team_root, target.verifying_key());
let (scope_root, scope_facts) = empty_scope();
let founder = signing_key();
let (cap_blob, cap_sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let pairs = extract_revocation_pairs([
rev_blob.clone(),
sig_blob.clone(),
cap_blob,
cap_sig_blob,
]);
assert_eq!(pairs.len(), 1, "exactly one revocation pair");
let (out_rev, out_sig) = &pairs[0];
assert_eq!(out_rev.bytes, rev_blob.bytes);
assert_eq!(out_sig.bytes, sig_blob.bytes);
}
#[test]
fn extract_revocation_pairs_drops_orphan_sig() {
let team_root = signing_key();
let target = signing_key();
let (_rev_blob, sig_blob) =
build_revocation(&team_root, target.verifying_key());
let pairs = extract_revocation_pairs([sig_blob]);
assert!(
pairs.is_empty(),
"orphan sig (target blob missing) is not paired"
);
}
#[test]
fn extract_then_build_revocation_set_round_trips() {
let team_root = signing_key();
let bystander = signing_key();
let target_authorised = signing_key();
let target_rogue = signing_key();
let (rev_a, sig_a) =
build_revocation(&team_root, target_authorised.verifying_key());
let (rev_b, sig_b) =
build_revocation(&bystander, target_rogue.verifying_key());
let pairs = extract_revocation_pairs([
rev_a, sig_a, rev_b, sig_b,
]);
assert_eq!(pairs.len(), 2);
let mut authorised = HashSet::new();
authorised.insert(team_root.verifying_key());
let revoked = build_revocation_set(&authorised, pairs);
assert!(revoked.contains(&target_authorised.verifying_key()));
assert!(
!revoked.contains(&target_rogue.verifying_key()),
"bystander-signed revocation is dropped",
);
}
#[test]
fn verify_rejects_chain_with_scope_violation() {
let team_root = signing_key();
let founder = signing_key();
let member = signing_key();
let (founder_scope_root, founder_scope_facts) =
scope_with(&[PERM_READ], &[]);
let (founder_cap, founder_sig) = build_capability(
&team_root,
founder.verifying_key(),
None,
founder_scope_root,
founder_scope_facts,
now_plus_24h(),
)
.expect("founder cap builds");
let (member_scope_root, member_scope_facts) =
scope_with(&[PERM_WRITE], &[]);
let (member_cap, member_sig) = build_capability(
&founder,
member.verifying_key(),
Some((founder_cap.clone(), founder_sig.clone())),
member_scope_root,
member_scope_facts,
now_plus_24h(),
)
.expect("member cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&member_sig).get_handle();
let revoked = HashSet::new();
let result = verify_chain(
team_root.verifying_key(),
leaf_handle,
member.verifying_key(),
&revoked,
store_for(&[
&founder_cap,
&founder_sig,
&member_cap,
&member_sig,
]),
);
assert!(matches!(result, Err(VerifyError::ScopeNotSubset)));
}
#[test]
fn build_delegated_capability() {
let team_root = signing_key();
let founder = signing_key();
let member = signing_key();
let (founder_scope_root, founder_scope_facts) = empty_scope();
let (founder_cap, founder_sig) = build_capability(
&team_root,
founder.verifying_key(),
None,
founder_scope_root,
founder_scope_facts,
now_plus_24h(),
)
.expect("founder cap builds");
let (member_scope_root, member_scope_facts) = empty_scope();
let (member_cap, _member_sig) = build_capability(
&founder,
member.verifying_key(),
Some((founder_cap.clone(), founder_sig.clone())),
member_scope_root,
member_scope_facts,
now_plus_24h(),
)
.expect("member cap builds");
let member_cap_set: TribleSet =
<TribleSet as TryFromBlob<SimpleArchive>>::try_from_blob(member_cap)
.expect("valid cap blob");
let founder_handle: Inline<Handle<SimpleArchive>> =
(&founder_cap).get_handle();
let mut parents = find!(
(e: Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&member_cap_set, [{ ?e @ cap_parent: ?h }])
);
let (cap_entity_id, parent_handle_v) =
parents.next().expect("cap_parent present");
assert!(parents.next().is_none(), "exactly one cap_parent");
assert_eq!(parent_handle_v, founder_handle);
let mut embedded = find!(
(sig: Id),
pattern!(&member_cap_set, [{
cap_entity_id @ cap_embedded_parent_sig: ?sig
}])
);
let (sig_id,) = embedded.next().expect("embedded sig pointer");
assert!(embedded.next().is_none(), "exactly one embedded sig");
let mut sig_facts = find!(
(pubkey: VerifyingKey, r, s),
pattern!(&member_cap_set, [{
sig_id @
crate::repo::signed_by: ?pubkey,
crate::repo::signature_r: ?r,
crate::repo::signature_s: ?s,
}])
);
let (parent_issuer_pubkey, r_v, s_v) =
sig_facts.next().expect("embedded sig has sig fields");
assert!(sig_facts.next().is_none(), "exactly one sig sub-entity");
assert_eq!(parent_issuer_pubkey, team_root.verifying_key());
let signature = ed25519::Signature::from_components(r_v, s_v);
team_root
.verifying_key()
.verify(&founder_cap.bytes, &signature)
.expect("embedded signature verifies over the parent cap bytes");
}
#[test]
fn verified_capability_permissions_read_only() {
let team_root = signing_key();
let founder = signing_key();
let (scope_root, scope_facts) = scope_with(&[PERM_READ], &[]);
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let revoked = HashSet::new();
let verified = verify_chain(
team_root.verifying_key(),
leaf_handle,
founder.verifying_key(),
&revoked,
store_for(&[&cap_blob, &sig_blob]),
)
.expect("chain valid");
let perms = verified.permissions();
assert_eq!(perms.len(), 1);
assert!(perms.contains(&PERM_READ));
assert!(verified.grants_read());
}
#[test]
fn verified_capability_unrestricted_grants_read_on_any_branch() {
let team_root = signing_key();
let founder = signing_key();
let (scope_root, scope_facts) = scope_with(&[PERM_READ], &[]);
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let verified = verify_chain(
team_root.verifying_key(),
leaf_handle,
founder.verifying_key(),
&HashSet::new(),
store_for(&[&cap_blob, &sig_blob]),
)
.expect("chain valid");
assert!(verified.granted_branches().is_none(), "unrestricted");
let any_branch = crate::id::ufoid();
assert!(verified.grants_read_on(&any_branch));
}
#[test]
fn verified_capability_branch_restricted_gates_correctly() {
let team_root = signing_key();
let founder = signing_key();
let allowed = crate::id::ufoid();
let blocked = crate::id::ufoid();
let (scope_root, scope_facts) =
scope_with(&[PERM_READ], &[*allowed]);
let (cap_blob, sig_blob) = build_capability(
&team_root,
founder.verifying_key(),
None,
scope_root,
scope_facts,
now_plus_24h(),
)
.expect("cap builds");
let leaf_handle: Inline<Handle<SimpleArchive>> =
(&sig_blob).get_handle();
let verified = verify_chain(
team_root.verifying_key(),
leaf_handle,
founder.verifying_key(),
&HashSet::new(),
store_for(&[&cap_blob, &sig_blob]),
)
.expect("chain valid");
let granted = verified.granted_branches().expect("restricted set");
assert!(granted.contains(&*allowed));
assert!(!granted.contains(&*blocked));
assert!(verified.grants_read_on(&*allowed));
assert!(!verified.grants_read_on(&*blocked));
}
#[test]
fn verified_capability_without_read_permission_blocks_reads() {
let scope_root = crate::id::ufoid();
let cap_set = TribleSet::new();
let verified = VerifiedCapability {
subject: signing_key().verifying_key(),
scope_root: *scope_root,
cap_set,
};
assert!(!verified.grants_read());
let any_branch = crate::id::ufoid();
assert!(!verified.grants_read_on(&any_branch));
}
}