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;
"46246789D627C1B0F81B21418E179DFD" as pub scope_branch: GenId;
"230E175A083E29155C860B38BD44F2F3" as pub sig_signs: Handle<SimpleArchive>;
"ACF20EE95C6A4AE16B445590E88AB9BE" as pub sig_parent_cap: Handle<SimpleArchive>;
"8ED30E412129FB0A791BD335EACF2E82" as pub sig_embedded_parent_proof: GenId;
}
#[allow(dead_code)]
pub const KIND_CAPABILITY: Id = id_hex!("B8D76786ACD20F344A4E5CBFC0F75772");
#[allow(dead_code)]
pub const KIND_CAPABILITY_SIG: Id = id_hex!("E6BB52CE6E02D51C3676ECE1EEA9094F");
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 mut cap_set = TribleSet::from(cap_fragment);
cap_set += scope_facts;
let cap_blob: Blob<SimpleArchive> = cap_set.to_blob();
let cap_handle: Inline<Handle<SimpleArchive>> = (&cap_blob).get_handle();
let signature: Signature = issuer.sign(&cap_blob.bytes);
let mut sig_set: TribleSet = TribleSet::from(entity! {
sig_signs: cap_handle,
crate::repo::signed_by: issuer_pubkey,
crate::repo::signature_r: signature,
crate::repo::signature_s: signature,
});
let leaf_outer_id: crate::id::Id = find!(
(s: crate::id::Id, _h: Inline<Handle<SimpleArchive>>),
pattern!(&sig_set, [{ ?s @ sig_signs: ?_h }])
)
.map(|(s, _)| s)
.next()
.expect("just inserted our own outer sig entity");
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 parent_outer_iter = find!(
(sig: crate::id::Id, _signed: Inline<Handle<SimpleArchive>>),
pattern!(&parent_sig_set, [{ ?sig @ sig_signs: ?_signed }])
)
.map(|(sig, _)| sig);
let parent_outer_id = match (
parent_outer_iter.next(),
parent_outer_iter.next(),
) {
(Some(id), None) => id,
_ => return Err(BuildError::ParentSigShape),
};
let sig_signs_attr_id = sig_signs.id();
for trible in parent_sig_set.iter() {
if *trible.e() == parent_outer_id && *trible.a() == sig_signs_attr_id {
continue;
}
sig_set.insert(trible);
}
sig_set += TribleSet::from(entity! {
ExclusiveId::force_ref(&leaf_outer_id) @
sig_parent_cap: parent_cap_handle,
sig_embedded_parent_proof: parent_outer_id,
});
}
let sig_blob: Blob<SimpleArchive> = sig_set.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
}
use ed25519_dalek::Verifier;
use std::collections::HashSet;
use crate::inline::TryFromInline;
use hifitime::Epoch;
#[derive(Debug)]
pub enum VerifyError {
ParseBlob(UnarchiveError),
Fetch,
BadSignature,
SubjectMismatch,
IssuerMismatch,
Expired,
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 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),
};
Ok(CapFields {
cap_id,
subject,
issuer,
scope_root,
expiry,
})
}
#[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>,
}
pub fn verify_chain<F>(
team_root: VerifyingKey,
leaf_sig_handle: Inline<Handle<SimpleArchive>>,
expected_subject: 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 sig_set: TribleSet = TryFromBlob::try_from_blob(leaf_sig_blob)?;
let mut leaf_outer_iter = find!(
(sig: crate::id::Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&sig_set, [{ ?sig @ sig_signs: ?h }])
);
let (mut current_outer_id, leaf_cap_handle) = match (
leaf_outer_iter.next(),
leaf_outer_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_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 is_expired(&leaf_fields.expiry) {
return Err(VerifyError::Expired);
}
let outer_signer = extract_and_verify_sig_at(
&sig_set,
current_outer_id,
&leaf_cap_blob,
)?;
if outer_signer != leaf_fields.issuer {
return Err(VerifyError::IssuerMismatch);
}
let mut current_signer = outer_signer;
let mut current_cap_set = leaf_cap_set.clone();
let mut current_fields = leaf_fields.clone();
let mut depth = 0usize;
loop {
if current_signer == 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 mut parent_iter = find!(
(ph: Inline<Handle<SimpleArchive>>, pid: crate::id::Id),
pattern!(&sig_set, [{
current_outer_id @
sig_parent_cap: ?ph,
sig_embedded_parent_proof: ?pid,
}])
);
let (parent_cap_handle, parent_proof_id) = match (
parent_iter.next(),
parent_iter.next(),
) {
(Some(row), None) => row,
_ => return Err(VerifyError::NonRootMissingParent),
};
let parent_cap_blob = fetch_blob(parent_cap_handle).ok_or(VerifyError::Fetch)?;
let parent_cap_set: TribleSet =
TryFromBlob::try_from_blob(parent_cap_blob.clone())?;
let parent_fields = extract_cap_fields(&parent_cap_set)?;
let parent_signer = extract_and_verify_sig_at(
&sig_set,
parent_proof_id,
&parent_cap_blob,
)?;
if parent_signer != parent_fields.issuer {
return Err(VerifyError::IssuerMismatch);
}
if is_expired(&parent_fields.expiry) {
return Err(VerifyError::Expired);
}
if !scope_subsumes(
&parent_cap_set,
parent_fields.scope_root,
¤t_cap_set,
current_fields.scope_root,
) {
return Err(VerifyError::ScopeNotSubset);
}
current_outer_id = parent_proof_id;
current_signer = parent_signer;
current_cap_set = parent_cap_set;
current_fields = parent_fields;
}
}
fn extract_and_verify_sig_at(
sig_set: &TribleSet,
entity: crate::id::Id,
signed_blob: &Blob<SimpleArchive>,
) -> Result<VerifyingKey, VerifyError> {
let mut iter = find!(
(signer: VerifyingKey, r, s),
pattern!(sig_set, [{
entity @
crate::repo::signed_by: ?signer,
crate::repo::signature_r: ?r,
crate::repo::signature_s: ?s,
}])
);
let (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(&signed_blob.bytes, &signature)
.map_err(|_| VerifyError::BadSignature)?;
Ok(signer)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inline::TryToInline;
use ed25519_dalek::SigningKey;
use hifitime::Epoch;
use rand::rngs::OsRng;
use std::collections::HashMap;
fn key() -> SigningKey {
SigningKey::generate(&mut OsRng)
}
fn interval(seconds_from_now: f64) -> Inline<NsTAIInterval> {
let now = Epoch::now().expect("system time");
let later = now + hifitime::Duration::from_seconds(seconds_from_now);
(now, later).try_to_inline().expect("valid interval")
}
fn expired_interval() -> Inline<NsTAIInterval> {
let now = Epoch::now().expect("system time");
let past_start = now - hifitime::Duration::from_seconds(7200.0);
let past_end = now - hifitime::Duration::from_seconds(3600.0);
(past_start, past_end).try_to_inline().expect("valid interval")
}
fn empty_scope() -> (Id, TribleSet) {
let scope_root = crate::id::ufoid();
let facts = TribleSet::from(entity! { ExclusiveId::force_ref(&scope_root) @
crate::metadata::tag: PERM_READ,
});
(*scope_root, facts)
}
fn fetch_from(
blobs: &[Blob<SimpleArchive>],
) -> impl FnMut(Inline<Handle<SimpleArchive>>) -> Option<Blob<SimpleArchive>> + '_ {
let map: HashMap<_, _> = blobs
.iter()
.map(|b| {
let h: Inline<Handle<SimpleArchive>> = b.get_handle();
(h.raw, b.clone())
})
.collect();
move |h| map.get(&h.raw).cloned()
}
#[test]
fn length_one_chain_round_trips() {
let team_root = key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&team_root,
team_root.verifying_key(),
None,
scope_root,
scope_facts,
interval(3600.0),
)
.expect("build");
let sig_handle: Inline<Handle<SimpleArchive>> = (&sig_blob).get_handle();
let blobs = [cap_blob.clone(), sig_blob.clone()];
let verified = verify_chain(
team_root.verifying_key(),
sig_handle,
team_root.verifying_key(),
fetch_from(&blobs),
)
.expect("verify");
assert_eq!(verified.subject, team_root.verifying_key());
assert_eq!(verified.scope_root, scope_root);
}
fn three_level_chain()
-> (SigningKey, SigningKey, SigningKey, Vec<Blob<SimpleArchive>>, Inline<Handle<SimpleArchive>>) {
let team_root = key();
let a = key();
let b = key();
let (scope1_root, scope1_facts) = empty_scope();
let (cap_a, sig_a) = build_capability(
&team_root,
a.verifying_key(),
None,
scope1_root,
scope1_facts,
interval(3600.0),
)
.expect("build level-1");
let (scope2_root, scope2_facts) = empty_scope();
let (cap_b, sig_b) = build_capability(
&a,
b.verifying_key(),
Some((cap_a.clone(), sig_a.clone())),
scope2_root,
scope2_facts,
interval(3600.0),
)
.expect("build level-2");
let leaf_sig_handle: Inline<Handle<SimpleArchive>> = (&sig_b).get_handle();
let blobs = vec![cap_a, sig_a, cap_b, sig_b];
(team_root, a, b, blobs, leaf_sig_handle)
}
#[test]
fn length_three_chain_round_trips() {
let (team_root, _a, b, blobs, leaf_sig_handle) = three_level_chain();
let verified = verify_chain(
team_root.verifying_key(),
leaf_sig_handle,
b.verifying_key(),
fetch_from(&blobs),
)
.expect("verify");
assert_eq!(verified.subject, b.verifying_key());
}
#[test]
fn rejects_subject_mismatch() {
let (team_root, _a, _b, blobs, leaf_sig_handle) = three_level_chain();
let imposter = key();
let err = verify_chain(
team_root.verifying_key(),
leaf_sig_handle,
imposter.verifying_key(),
fetch_from(&blobs),
)
.expect_err("must reject subject mismatch");
assert!(matches!(err, VerifyError::SubjectMismatch));
}
#[test]
fn rejects_wrong_team_root() {
let (_real_team_root, _a, b, blobs, leaf_sig_handle) = three_level_chain();
let wrong_root = key();
let err = verify_chain(
wrong_root.verifying_key(),
leaf_sig_handle,
b.verifying_key(),
fetch_from(&blobs),
)
.expect_err("must reject wrong team root");
assert!(matches!(err, VerifyError::NonRootMissingParent));
}
#[test]
fn rejects_expired_leaf() {
let team_root = key();
let (scope_root, scope_facts) = empty_scope();
let (cap_blob, sig_blob) = build_capability(
&team_root,
team_root.verifying_key(),
None,
scope_root,
scope_facts,
expired_interval(),
)
.expect("build");
let sig_handle: Inline<Handle<SimpleArchive>> = (&sig_blob).get_handle();
let blobs = [cap_blob, sig_blob];
let err = verify_chain(
team_root.verifying_key(),
sig_handle,
team_root.verifying_key(),
fetch_from(&blobs),
)
.expect_err("must reject expired");
assert!(matches!(err, VerifyError::Expired));
}
#[test]
fn rejects_expired_intermediate() {
let team_root = key();
let a = key();
let b = key();
let (scope1_root, scope1_facts) = empty_scope();
let (cap_a, sig_a) = build_capability(
&team_root,
a.verifying_key(),
None,
scope1_root,
scope1_facts,
expired_interval(),
)
.expect("build level-1");
let (scope2_root, scope2_facts) = empty_scope();
let (cap_b, sig_b) = build_capability(
&a,
b.verifying_key(),
Some((cap_a.clone(), sig_a.clone())),
scope2_root,
scope2_facts,
interval(3600.0),
)
.expect("build level-2");
let leaf_sig_handle: Inline<Handle<SimpleArchive>> = (&sig_b).get_handle();
let blobs = [cap_a, sig_a, cap_b, sig_b];
let err = verify_chain(
team_root.verifying_key(),
leaf_sig_handle,
b.verifying_key(),
fetch_from(&blobs),
)
.expect_err("must reject expired intermediate");
assert!(matches!(err, VerifyError::Expired));
}
#[test]
fn cap_blob_carries_no_chain_attributes() {
let (_team_root, _a, _b, blobs, _leaf_sig_handle) = three_level_chain();
for blob in &blobs {
let set: TribleSet = match TryFromBlob::try_from_blob(blob.clone()) {
Ok(s) => s,
Err(_) => continue, };
let is_cap = find!(
(e: Id, s: VerifyingKey),
pattern!(&set, [{ ?e @ cap_subject: ?s }])
)
.next()
.is_some();
if !is_cap {
continue;
}
let has_parent_link = find!(
(e: Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&set, [{ ?e @ sig_parent_cap: ?h }])
)
.next()
.is_some();
assert!(
!has_parent_link,
"cap blob unexpectedly carries sig_parent_cap"
);
}
}
#[test]
fn leaf_sig_blob_carries_full_chain() {
let (_team_root, _a, _b, blobs, leaf_sig_handle) = three_level_chain();
let leaf_sig_blob = fetch_from(&blobs)(leaf_sig_handle).expect("fetch leaf sig");
let sig_set: TribleSet = TryFromBlob::try_from_blob(leaf_sig_blob).expect("parse sig");
let signed_by_entities: HashSet<Id> = find!(
(e: Id, s: VerifyingKey),
pattern!(&sig_set, [{ ?e @ crate::repo::signed_by: ?s }])
)
.map(|(e, _)| e)
.collect();
assert_eq!(
signed_by_entities.len(),
2,
"expected 2 signed_by entities (one per chain level); got {}",
signed_by_entities.len()
);
let parent_links: HashSet<Id> = find!(
(e: Id, h: Inline<Handle<SimpleArchive>>),
pattern!(&sig_set, [{ ?e @ sig_parent_cap: ?h }])
)
.map(|(e, _)| e)
.collect();
assert_eq!(
parent_links.len(),
1,
"expected 1 sig_parent_cap entry for length-2 chain"
);
}
}