use super::capability::CapabilitySet;
use super::org::OrgId;
use super::org_authority::OwnerAudienceCredential;
use super::org_grant::{CapabilityAuthorityId, OrgAudienceSecret, OrgCapabilityGrant};
use super::org_revocation::OrgRevocationState;
use super::org_scoped_ann::ScopedCapabilityAnnouncement;
use crate::adapter::net::identity::EntityId;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum CapabilityAudienceScope {
Public,
Owner {
org_id: OrgId,
audience_handle: [u8; 32],
},
Grant {
grant_id: [u8; 32],
audience_handle: [u8; 32],
},
}
pub enum AudienceAuthority<'a> {
Owner {
owner_org: OrgId,
audience_handle: [u8; 32],
discovery_key: &'a [u8; 32],
},
Granted {
grant: &'a OrgCapabilityGrant,
secret: &'a OrgAudienceSecret,
},
}
impl<'a> AudienceAuthority<'a> {
pub fn owner(owner_org: OrgId, credential: &'a OwnerAudienceCredential) -> Self {
Self::Owner {
owner_org,
audience_handle: credential.audience_handle,
discovery_key: credential.discovery_key(),
}
}
pub fn granted(grant: &'a OrgCapabilityGrant, secret: &'a OrgAudienceSecret) -> Self {
Self::Granted { grant, secret }
}
pub fn audience_handle(&self) -> &[u8; 32] {
match self {
Self::Owner {
audience_handle, ..
} => audience_handle,
Self::Granted { secret, .. } => &secret.audience_handle,
}
}
}
#[derive(Clone, Debug)]
pub struct LocalMemberStanding {
pub member: EntityId,
pub generation: u32,
}
pub struct ScopedIngestContext<'a> {
pub local_owner_org: OrgId,
pub floors: &'a OrgRevocationState,
pub local_member: Option<LocalMemberStanding>,
pub now_secs: u64,
pub skew_secs: u64,
}
#[derive(Clone)]
pub struct VerifiedScopedCapability {
scope: CapabilityAudienceScope,
provider: EntityId,
owner_org: OrgId,
generation: u64,
expires_at: u64,
provider_cert_generation: u32,
grant_signature: Option<[u8; 64]>,
descriptor: Vec<u8>,
}
impl VerifiedScopedCapability {
pub fn scope(&self) -> &CapabilityAudienceScope {
&self.scope
}
pub fn provider(&self) -> &EntityId {
&self.provider
}
pub fn owner_org(&self) -> &OrgId {
&self.owner_org
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn expires_at(&self) -> u64 {
self.expires_at
}
pub fn provider_cert_generation(&self) -> u32 {
self.provider_cert_generation
}
pub fn grant_signature(&self) -> Option<&[u8; 64]> {
self.grant_signature.as_ref()
}
pub fn descriptor(&self) -> &[u8] {
&self.descriptor
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn for_test(
scope: CapabilityAudienceScope,
provider: EntityId,
owner_org: OrgId,
generation: u64,
expires_at: u64,
provider_cert_generation: u32,
grant_signature: Option<[u8; 64]>,
descriptor: Vec<u8>,
) -> Self {
Self {
scope,
provider,
owner_org,
generation,
expires_at,
provider_cert_generation,
grant_signature,
descriptor,
}
}
}
impl std::fmt::Debug for VerifiedScopedCapability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VerifiedScopedCapability")
.field("scope", &self.scope)
.field("provider", &self.provider)
.field("owner_org", &self.owner_org)
.field("generation", &self.generation)
.field("expires_at", &self.expires_at)
.field("provider_cert_generation", &self.provider_cert_generation)
.field("has_grant_signature", &self.grant_signature.is_some())
.field("descriptor_len", &self.descriptor.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScopedIngestError {
AudienceKindMismatch,
NotForThisOwner,
HandleMismatch,
ProviderCertMismatch,
MembershipInvalid,
MembershipRevoked,
LocalMembershipRevoked,
Expired,
GrantInvalid,
GrantWrongGrantee,
GrantMissingDiscover,
GrantIdMismatch,
SecretMismatch,
TargetNotCovered,
DecryptFailed,
DescriptorInvalid,
DescriptorOutsideGrant,
}
impl std::fmt::Display for ScopedIngestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ScopedIngestError::AudienceKindMismatch => "audience kind mismatch",
ScopedIngestError::NotForThisOwner => "envelope owner org is not this node's org",
ScopedIngestError::HandleMismatch => "audience handle mismatch",
ScopedIngestError::ProviderCertMismatch => "provider membership cert does not bind P",
ScopedIngestError::MembershipInvalid => "provider membership cert invalid",
ScopedIngestError::MembershipRevoked => "provider membership below revocation floor",
ScopedIngestError::LocalMembershipRevoked => {
"this node's own membership is below its org's revocation floor \
(rotate the owner audience key)"
}
ScopedIngestError::Expired => "scoped announcement expired",
ScopedIngestError::GrantInvalid => "capability grant invalid",
ScopedIngestError::GrantWrongGrantee => "grant does not name this org",
ScopedIngestError::GrantMissingDiscover => "grant lacks DISCOVER rights",
ScopedIngestError::GrantIdMismatch => "envelope grant id does not match the grant",
ScopedIngestError::SecretMismatch => "installed secret does not match the grant",
ScopedIngestError::TargetNotCovered => "provider outside the grant target scope",
ScopedIngestError::DecryptFailed => "scoped announcement AEAD open failed",
ScopedIngestError::DescriptorInvalid => {
"granted descriptor is not the canonical single-capability shape"
}
ScopedIngestError::DescriptorOutsideGrant => {
"granted descriptor names a capability the grant does not authorize"
}
};
f.write_str(s)
}
}
impl std::error::Error for ScopedIngestError {}
pub fn verify_scoped_ingest(
envelope: &ScopedCapabilityAnnouncement,
authority: &AudienceAuthority<'_>,
ctx: &ScopedIngestContext<'_>,
) -> Result<VerifiedScopedCapability, ScopedIngestError> {
if let Some(local) = &ctx.local_member {
let floor = ctx.floors.floor_for(&ctx.local_owner_org, &local.member);
if local.generation < floor {
return Err(ScopedIngestError::LocalMembershipRevoked);
}
}
match authority {
AudienceAuthority::Owner {
owner_org,
audience_handle,
discovery_key,
} => verify_owner_ingest(envelope, *owner_org, audience_handle, discovery_key, ctx),
AudienceAuthority::Granted { grant, secret } => {
verify_granted_ingest(envelope, grant, secret, ctx)
}
}
}
fn verify_owner_ingest(
envelope: &ScopedCapabilityAnnouncement,
owner_org: OrgId,
audience_handle: &[u8; 32],
discovery_key: &[u8; 32],
ctx: &ScopedIngestContext<'_>,
) -> Result<VerifiedScopedCapability, ScopedIngestError> {
if !envelope.is_owner_audience() {
return Err(ScopedIngestError::AudienceKindMismatch);
}
if owner_org != ctx.local_owner_org || envelope.owner_org() != &owner_org {
return Err(ScopedIngestError::NotForThisOwner);
}
if envelope.audience_handle() != audience_handle {
return Err(ScopedIngestError::HandleMismatch);
}
verify_provider_membership(envelope, &owner_org, ctx)?;
let expires_at = effective_expires_at(envelope, &[envelope.owner_cert().not_after]);
if is_expired_at(expires_at, ctx) {
return Err(ScopedIngestError::Expired);
}
let descriptor = envelope
.open_with(discovery_key)
.map_err(|_| ScopedIngestError::DecryptFailed)?;
Ok(VerifiedScopedCapability {
scope: CapabilityAudienceScope::Owner {
org_id: owner_org,
audience_handle: *audience_handle,
},
provider: envelope.provider().clone(),
owner_org,
generation: envelope.generation(),
expires_at,
provider_cert_generation: envelope.owner_cert().generation,
grant_signature: None,
descriptor,
})
}
fn verify_granted_ingest(
envelope: &ScopedCapabilityAnnouncement,
grant: &OrgCapabilityGrant,
secret: &OrgAudienceSecret,
ctx: &ScopedIngestContext<'_>,
) -> Result<VerifiedScopedCapability, ScopedIngestError> {
if envelope.is_owner_audience() {
return Err(ScopedIngestError::AudienceKindMismatch);
}
if !secret.matches_grant(grant) {
return Err(ScopedIngestError::SecretMismatch);
}
if envelope.audience_handle() != &secret.audience_handle {
return Err(ScopedIngestError::HandleMismatch);
}
if envelope.grant_id() != &grant.grant_id {
return Err(ScopedIngestError::GrantIdMismatch);
}
grant
.verify()
.map_err(|_| ScopedIngestError::GrantInvalid)?;
grant
.is_valid_at_with_skew(ctx.now_secs, ctx.skew_secs)
.map_err(|_| ScopedIngestError::GrantInvalid)?;
if grant.grantee_org != ctx.local_owner_org {
return Err(ScopedIngestError::GrantWrongGrantee);
}
if !grant.permits_discover() {
return Err(ScopedIngestError::GrantMissingDiscover);
}
if envelope.owner_org() != &grant.issuer_org {
return Err(ScopedIngestError::ProviderCertMismatch);
}
if !grant
.target_scope
.covers(envelope.provider(), Some(&grant.issuer_org))
{
return Err(ScopedIngestError::TargetNotCovered);
}
verify_provider_membership(envelope, &grant.issuer_org, ctx)?;
let expires_at = effective_expires_at(
envelope,
&[envelope.owner_cert().not_after, grant.not_after],
);
if is_expired_at(expires_at, ctx) {
return Err(ScopedIngestError::Expired);
}
let descriptor = envelope
.open_with(secret.discovery_key())
.map_err(|_| ScopedIngestError::DecryptFailed)?;
descriptor_binds_grant_capability(&descriptor, &grant.capability)?;
Ok(VerifiedScopedCapability {
scope: CapabilityAudienceScope::Grant {
grant_id: grant.grant_id,
audience_handle: *envelope.audience_handle(),
},
provider: envelope.provider().clone(),
owner_org: *envelope.owner_org(),
generation: envelope.generation(),
expires_at,
provider_cert_generation: envelope.owner_cert().generation,
grant_signature: Some(grant.signature),
descriptor,
})
}
pub(crate) fn decode_declared_capabilities(descriptor: &[u8]) -> Vec<CapabilityAuthorityId> {
let Some(caps) = CapabilitySet::from_bytes(descriptor) else {
return Vec::new();
};
caps.tags
.iter()
.map(|tag| CapabilityAuthorityId::for_tag(&tag.to_string()))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect()
}
pub struct PreparedScopedCapability {
verified: VerifiedScopedCapability,
declarations: std::sync::Arc<[CapabilityAuthorityId]>,
}
impl PreparedScopedCapability {
pub(crate) fn prepare(verified: VerifiedScopedCapability) -> Self {
let declarations = decode_declared_capabilities(verified.descriptor()).into();
Self {
verified,
declarations,
}
}
pub(crate) fn into_parts(
self,
) -> (
VerifiedScopedCapability,
std::sync::Arc<[CapabilityAuthorityId]>,
) {
(self.verified, self.declarations)
}
}
fn descriptor_binds_grant_capability(
descriptor: &[u8],
capability: &CapabilityAuthorityId,
) -> Result<(), ScopedIngestError> {
let caps = CapabilitySet::from_bytes(descriptor).ok_or(ScopedIngestError::DescriptorInvalid)?;
let mut tags = caps.tags.iter();
let (Some(tag), None) = (tags.next(), tags.next()) else {
return Err(ScopedIngestError::DescriptorInvalid);
};
if !caps.metadata.is_empty() {
return Err(ScopedIngestError::DescriptorInvalid);
}
if caps.to_bytes_compact() != descriptor {
return Err(ScopedIngestError::DescriptorInvalid);
}
if &CapabilityAuthorityId::for_tag(&tag.to_string()) != capability {
return Err(ScopedIngestError::DescriptorOutsideGrant);
}
Ok(())
}
fn verify_provider_membership(
envelope: &ScopedCapabilityAnnouncement,
expected_org: &OrgId,
ctx: &ScopedIngestContext<'_>,
) -> Result<(), ScopedIngestError> {
let cert = envelope.owner_cert();
if envelope.provider() != &cert.member || &cert.org_id != expected_org {
return Err(ScopedIngestError::ProviderCertMismatch);
}
cert.is_valid_at_with_skew(ctx.now_secs, ctx.skew_secs)
.map_err(|_| ScopedIngestError::MembershipInvalid)?;
let floor = ctx.floors.floor_for(expected_org, envelope.provider());
if cert.generation < floor {
return Err(ScopedIngestError::MembershipRevoked);
}
Ok(())
}
fn effective_expires_at(envelope: &ScopedCapabilityAnnouncement, bounds: &[u64]) -> u64 {
bounds.iter().copied().fold(envelope.expires_at(), u64::min)
}
fn is_expired_at(effective_expires_at: u64, ctx: &ScopedIngestContext<'_>) -> bool {
ctx.now_secs >= effective_expires_at.saturating_add(ctx.skew_secs)
}
#[cfg(test)]
mod tests {
use super::super::org::{
current_timestamp, OrgKeypair, OrgMembershipCert, OrgRevocationBundle,
};
use super::super::org_grant::{CapabilityAuthorityId, GrantRights, GrantTargetScope};
use super::super::org_scoped_ann::ScopedAnnouncementError;
use super::*;
use crate::adapter::net::identity::EntityKeypair;
use std::collections::BTreeMap;
const NOW: u64 = 10_000;
const SKEW: u64 = 60;
fn provider_kp() -> EntityKeypair {
EntityKeypair::from_bytes([2u8; 32])
}
struct OwnerFixture {
provider: EntityKeypair,
org: OrgKeypair,
credential: OwnerAudienceCredential,
envelope: ScopedCapabilityAnnouncement,
descriptor: Vec<u8>,
}
fn owner_fixture() -> OwnerFixture {
let provider = provider_kp();
let org = OrgKeypair::from_bytes([1u8; 32]);
let credential = OwnerAudienceCredential::generate(org.org_id());
let cert = OrgMembershipCert::issue_at(
&org,
provider.entity_id().clone(),
5,
0,
1_000_000,
0x1234,
);
let descriptor = b"owner-capability-descriptor".to_vec();
let envelope = ScopedCapabilityAnnouncement::build_owner(
&provider,
org.org_id(),
cert,
credential.audience_handle,
credential.discovery_key(),
3,
20_000,
&descriptor,
)
.expect("build owner envelope");
OwnerFixture {
provider,
org,
credential,
envelope,
descriptor,
}
}
fn owner_ctx(org: OrgId, floors: &OrgRevocationState) -> ScopedIngestContext<'_> {
ScopedIngestContext {
local_owner_org: org,
floors,
now_secs: NOW,
skew_secs: SKEW,
local_member: None,
}
}
#[test]
fn owner_ingest_happy_path() {
let f = owner_fixture();
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::owner(f.org.org_id(), &f.credential);
let ctx = owner_ctx(f.org.org_id(), &floors);
let verified = verify_scoped_ingest(&f.envelope, &authority, &ctx).expect("owner ingest");
assert_eq!(
verified.scope(),
&CapabilityAudienceScope::Owner {
org_id: f.org.org_id(),
audience_handle: f.credential.audience_handle,
}
);
assert_eq!(verified.provider(), f.provider.entity_id());
assert_eq!(verified.descriptor(), f.descriptor.as_slice());
}
#[test]
fn owner_ingest_clamps_expiry_to_the_provider_certificate() {
let provider = provider_kp();
let org = OrgKeypair::from_bytes([1u8; 32]);
let credential = OwnerAudienceCredential::generate(org.org_id());
let cert_not_after = 12_000;
let cert = OrgMembershipCert::issue_at(
&org,
provider.entity_id().clone(),
5,
0,
cert_not_after,
0x1234,
);
let descriptor = b"owner-capability-descriptor".to_vec();
let envelope = ScopedCapabilityAnnouncement::build_owner(
&provider,
org.org_id(),
cert,
credential.audience_handle,
credential.discovery_key(),
3,
u64::MAX, &descriptor,
)
.expect("build owner envelope");
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::owner(org.org_id(), &credential);
let ctx = owner_ctx(org.org_id(), &floors);
let verified = verify_scoped_ingest(&envelope, &authority, &ctx).expect("owner ingest");
assert_eq!(
verified.expires_at(),
cert_not_after,
"the record must be admitted under the CERTIFICATE's lifetime, not \
the publisher's claim",
);
assert_ne!(
verified.expires_at(),
u64::MAX,
"storing the claim verbatim makes the record permanently \
discoverable after the certificate lapses",
);
}
#[test]
fn granted_ingest_clamps_expiry_to_the_grant() {
let now = current_timestamp();
let provider = provider_kp();
let issuer = OrgKeypair::from_bytes([1u8; 32]); let a_org = OrgKeypair::from_bytes([9u8; 32]).org_id(); let (grant, secret) = OrgCapabilityGrant::try_issue(
&issuer,
a_org,
CapabilityAuthorityId::for_tag("nrpc:svc"),
GrantRights::INVOKE.union(GrantRights::DISCOVER),
exact_target(&provider),
3600,
)
.expect("issue grant");
let secret = secret.expect("discover mints a secret");
let cert = OrgMembershipCert::issue_at(
&issuer,
provider.entity_id().clone(),
5,
now.saturating_sub(3600),
now + 100_000,
0x1234,
);
let descriptor = CapabilitySet::new().add_tag("nrpc:svc").to_bytes_compact();
let envelope = ScopedCapabilityAnnouncement::build_granted(
&provider,
issuer.org_id(),
cert.clone(),
grant.grant_id,
secret.audience_handle,
secret.discovery_key(),
4,
u64::MAX, &descriptor,
)
.expect("build granted envelope");
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&grant, &secret);
let ctx = granted_ctx(a_org, now, &floors);
let verified = verify_scoped_ingest(&envelope, &authority, &ctx).expect("granted ingest");
assert_eq!(
verified.expires_at(),
grant.not_after,
"a granted record must be bounded by the GRANT, which here expires \
before the provider certificate",
);
assert!(
grant.not_after < cert.not_after,
"precondition: the grant must be the shorter ceiling, else this \
test would pass on the certificate bound alone",
);
}
#[test]
fn a_record_clamped_into_the_past_is_refused_as_expired() {
let provider = provider_kp();
let org = OrgKeypair::from_bytes([1u8; 32]);
let credential = OwnerAudienceCredential::generate(org.org_id());
let cert = OrgMembershipCert::issue_at(
&org,
provider.entity_id().clone(),
5,
0,
NOW - (SKEW * 2),
0x1234,
);
let descriptor = b"owner-capability-descriptor".to_vec();
let envelope = ScopedCapabilityAnnouncement::build_owner(
&provider,
org.org_id(),
cert,
credential.audience_handle,
credential.discovery_key(),
3,
u64::MAX,
&descriptor,
)
.expect("build owner envelope");
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::owner(org.org_id(), &credential);
let ctx = owner_ctx(org.org_id(), &floors);
assert!(
verify_scoped_ingest(&envelope, &authority, &ctx).is_err(),
"a record with no live credential behind it must not be stored",
);
}
#[test]
fn a_locally_revoked_node_stops_ingesting() {
let f = owner_fixture();
let local = EntityKeypair::from_bytes([0x5Au8; 32]).entity_id().clone();
assert_ne!(
&local,
f.provider.entity_id(),
"the reader must be distinct from the publisher",
);
let authority = AudienceAuthority::owner(f.org.org_id(), &f.credential);
fn ctx_at<'a>(
org: OrgId,
member: &EntityId,
floors: &'a OrgRevocationState,
generation: u32,
) -> ScopedIngestContext<'a> {
ScopedIngestContext {
local_owner_org: org,
floors,
now_secs: NOW,
skew_secs: SKEW,
local_member: Some(LocalMemberStanding {
member: member.clone(),
generation,
}),
}
}
let org_id = f.org.org_id();
let no_floors = OrgRevocationState::empty();
assert!(
verify_scoped_ingest(
&f.envelope,
&authority,
&ctx_at(org_id, &local, &no_floors, 3)
)
.is_ok(),
"an in-good-standing reader ingests normally",
);
let mut floors_map = std::collections::BTreeMap::new();
floors_map.insert(local.clone(), 4u32);
let bundle = OrgRevocationBundle::try_issue(&f.org, &floors_map).expect("bundle");
let mut floored = OrgRevocationState::empty();
floored.merge_bundle(&bundle);
assert_eq!(
verify_scoped_ingest(
&f.envelope,
&authority,
&ctx_at(org_id, &local, &floored, 3)
)
.unwrap_err(),
ScopedIngestError::LocalMembershipRevoked,
"a locally revoked reader must refuse the ingest",
);
assert!(
verify_scoped_ingest(
&f.envelope,
&authority,
&ctx_at(org_id, &local, &floored, 4)
)
.is_ok(),
"a cert AT the floor is not revoked",
);
}
#[test]
fn owner_ingest_rejects_a_granted_authority() {
let f = owner_fixture();
let a_org = OrgKeypair::from_bytes([9u8; 32]);
let (grant, secret) = OrgCapabilityGrant::try_issue(
&f.org,
a_org.org_id(),
CapabilityAuthorityId::for_tag("nrpc:svc"),
GrantRights::DISCOVER,
GrantTargetScope::AnyNodeOwnedBy(f.org.org_id()),
3600,
)
.expect("issue grant");
let secret = secret.expect("discover mints a secret");
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&grant, &secret);
let ctx = owner_ctx(f.org.org_id(), &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::AudienceKindMismatch
);
}
#[test]
fn owner_ingest_rejects_wrong_owner_and_wrong_handle() {
let f = owner_fixture();
let floors = OrgRevocationState::empty();
let other = OrgKeypair::from_bytes([7u8; 32]);
let authority = AudienceAuthority::owner(other.org_id(), &f.credential);
let ctx = owner_ctx(other.org_id(), &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::NotForThisOwner
);
let wrong_handle = AudienceAuthority::Owner {
owner_org: f.org.org_id(),
audience_handle: [0xAAu8; 32],
discovery_key: f.credential.discovery_key(),
};
let ctx = owner_ctx(f.org.org_id(), &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &wrong_handle, &ctx).unwrap_err(),
ScopedIngestError::HandleMismatch
);
}
#[test]
fn owner_ingest_rejects_wrong_key_as_decrypt_failure() {
let f = owner_fixture();
let floors = OrgRevocationState::empty();
let wrong_key = [0x55u8; 32];
let authority = AudienceAuthority::Owner {
owner_org: f.org.org_id(),
audience_handle: f.credential.audience_handle,
discovery_key: &wrong_key,
};
let ctx = owner_ctx(f.org.org_id(), &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::DecryptFailed
);
}
#[test]
fn owner_ingest_rejects_a_revoked_provider() {
let f = owner_fixture();
let mut floors_map = BTreeMap::new();
floors_map.insert(f.provider.entity_id().clone(), 6u32);
let bundle = OrgRevocationBundle::try_issue(&f.org, &floors_map).expect("bundle");
let mut floors = OrgRevocationState::empty();
floors.merge_bundle(&bundle);
let authority = AudienceAuthority::owner(f.org.org_id(), &f.credential);
let ctx = owner_ctx(f.org.org_id(), &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::MembershipRevoked
);
}
#[test]
fn owner_ingest_rejects_an_expired_envelope() {
let f = owner_fixture();
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::owner(f.org.org_id(), &f.credential);
let ctx = ScopedIngestContext {
local_owner_org: f.org.org_id(),
floors: &floors,
now_secs: 20_000 + SKEW,
skew_secs: SKEW,
local_member: None,
};
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::Expired
);
}
struct GrantedFixture {
provider: EntityKeypair,
issuer: OrgKeypair, a_org: OrgId, grant: OrgCapabilityGrant,
secret: OrgAudienceSecret,
envelope: ScopedCapabilityAnnouncement,
descriptor: Vec<u8>,
now: u64,
}
fn granted_fixture_expiring_in(target: GrantTargetScope, expiry_offset: u64) -> GrantedFixture {
let now = current_timestamp();
let provider = provider_kp();
let issuer = OrgKeypair::from_bytes([1u8; 32]); let a_org = OrgKeypair::from_bytes([9u8; 32]).org_id(); let (grant, secret) = OrgCapabilityGrant::try_issue(
&issuer,
a_org,
CapabilityAuthorityId::for_tag("nrpc:svc"),
GrantRights::INVOKE.union(GrantRights::DISCOVER),
target,
3600,
)
.expect("issue grant");
let secret = secret.expect("discover mints a secret");
let cert = OrgMembershipCert::issue_at(
&issuer,
provider.entity_id().clone(),
5,
now.saturating_sub(3600),
now + 3600,
0x1234,
);
let descriptor = CapabilitySet::new().add_tag("nrpc:svc").to_bytes_compact();
let envelope = ScopedCapabilityAnnouncement::build_granted(
&provider,
issuer.org_id(), cert,
grant.grant_id,
secret.audience_handle,
secret.discovery_key(),
4,
now + expiry_offset,
&descriptor,
)
.expect("build granted envelope");
GrantedFixture {
provider,
issuer,
a_org,
grant,
secret,
envelope,
descriptor,
now,
}
}
fn granted_fixture(target: GrantTargetScope) -> GrantedFixture {
granted_fixture_expiring_in(target, 3600)
}
fn exact_target(p: &EntityKeypair) -> GrantTargetScope {
GrantTargetScope::ExactNode(p.entity_id().clone())
}
fn granted_ctx(
a_org: OrgId,
now_secs: u64,
floors: &OrgRevocationState,
) -> ScopedIngestContext<'_> {
ScopedIngestContext {
local_owner_org: a_org,
floors,
now_secs,
skew_secs: SKEW,
local_member: None,
}
}
#[test]
fn granted_ingest_happy_path_exact_target() {
let p = provider_kp();
let f = granted_fixture(exact_target(&p));
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(f.a_org, f.now + 5, &floors);
let verified = verify_scoped_ingest(&f.envelope, &authority, &ctx).expect("granted ingest");
assert_eq!(
verified.scope(),
&CapabilityAudienceScope::Grant {
grant_id: f.grant.grant_id,
audience_handle: f.secret.audience_handle,
}
);
assert_eq!(verified.owner_org(), &f.issuer.org_id());
assert_eq!(verified.descriptor(), f.descriptor.as_slice());
assert_eq!(verified.grant_signature(), Some(&f.grant.signature));
}
#[test]
fn granted_ingest_binds_descriptor_to_the_grant_capability() {
let p = provider_kp();
let f = granted_fixture(exact_target(&p));
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(f.a_org, f.now + 5, &floors);
let build = |descriptor: &[u8]| {
let cert = OrgMembershipCert::issue_at(
&f.issuer,
p.entity_id().clone(),
5,
f.now.saturating_sub(3600),
f.now + 3600,
0x1234,
);
ScopedCapabilityAnnouncement::build_granted(
&p,
f.issuer.org_id(),
cert,
f.grant.grant_id,
f.secret.audience_handle,
f.secret.discovery_key(),
4,
f.now + 3600,
descriptor,
)
.expect("build")
};
let c1 = CapabilitySet::new().add_tag("nrpc:svc").to_bytes_compact();
assert!(verify_scoped_ingest(&build(&c1), &authority, &ctx).is_ok());
let c2 = CapabilitySet::new()
.add_tag("nrpc:other")
.to_bytes_compact();
assert_eq!(
verify_scoped_ingest(&build(&c2), &authority, &ctx).unwrap_err(),
ScopedIngestError::DescriptorOutsideGrant
);
let c1c2 = CapabilitySet::new()
.add_tag("nrpc:svc")
.add_tag("nrpc:other")
.to_bytes_compact();
assert_eq!(
verify_scoped_ingest(&build(&c1c2), &authority, &ctx).unwrap_err(),
ScopedIngestError::DescriptorInvalid
);
let with_meta = CapabilitySet::new()
.add_tag("nrpc:svc")
.with_metadata("k".to_string(), "v".to_string())
.to_bytes_compact();
assert_eq!(
verify_scoped_ingest(&build(&with_meta), &authority, &ctx).unwrap_err(),
ScopedIngestError::DescriptorInvalid
);
assert_eq!(
verify_scoped_ingest(&build(&[0xFFu8; 7]), &authority, &ctx).unwrap_err(),
ScopedIngestError::DescriptorInvalid
);
}
#[test]
fn granted_ingest_happy_path_any_owned_by() {
let f = granted_fixture(GrantTargetScope::AnyNodeOwnedBy(
OrgKeypair::from_bytes([1u8; 32]).org_id(),
));
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(f.a_org, f.now + 5, &floors);
assert!(verify_scoped_ingest(&f.envelope, &authority, &ctx).is_ok());
}
#[test]
fn granted_ingest_rejects_owner_authority() {
let p = provider_kp();
let f = granted_fixture(exact_target(&p));
let credential = OwnerAudienceCredential::generate(f.issuer.org_id());
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::owner(f.issuer.org_id(), &credential);
let ctx = granted_ctx(f.issuer.org_id(), f.now + 5, &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::AudienceKindMismatch
);
}
#[test]
fn granted_ingest_rejects_wrong_grantee() {
let p = provider_kp();
let f = granted_fixture(exact_target(&p));
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(
OrgKeypair::from_bytes([0x33u8; 32]).org_id(),
f.now + 5,
&floors,
);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::GrantWrongGrantee
);
}
#[test]
fn granted_ingest_rejects_a_mismatched_secret() {
let p = provider_kp();
let f = granted_fixture(exact_target(&p));
let (_other_grant, other_secret) = OrgCapabilityGrant::try_issue(
&f.issuer,
f.a_org,
CapabilityAuthorityId::for_tag("nrpc:other"),
GrantRights::DISCOVER,
GrantTargetScope::AnyNodeOwnedBy(f.issuer.org_id()),
3600,
)
.expect("issue other grant");
let other_secret = other_secret.expect("secret");
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &other_secret);
let ctx = granted_ctx(f.a_org, f.now + 5, &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::SecretMismatch
);
}
#[test]
fn granted_ingest_rejects_a_provider_outside_the_exact_target() {
let other = EntityKeypair::from_bytes([0x44u8; 32]);
let f = granted_fixture(GrantTargetScope::ExactNode(other.entity_id().clone()));
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(f.a_org, f.now + 5, &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::TargetNotCovered
);
}
#[test]
fn granted_ingest_rejects_a_revoked_provider() {
let p = provider_kp();
let f = granted_fixture(exact_target(&p));
let mut floors_map = BTreeMap::new();
floors_map.insert(f.provider.entity_id().clone(), 6u32);
let bundle = OrgRevocationBundle::try_issue(&f.issuer, &floors_map).expect("bundle");
let mut floors = OrgRevocationState::empty();
floors.merge_bundle(&bundle);
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(f.a_org, f.now + 5, &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::MembershipRevoked
);
}
#[test]
fn granted_ingest_rejects_an_expired_envelope() {
let p = provider_kp();
let f = granted_fixture_expiring_in(exact_target(&p), 100);
let floors = OrgRevocationState::empty();
let authority = AudienceAuthority::granted(&f.grant, &f.secret);
let ctx = granted_ctx(f.a_org, f.now + 300, &floors);
assert_eq!(
verify_scoped_ingest(&f.envelope, &authority, &ctx).unwrap_err(),
ScopedIngestError::Expired
);
}
#[test]
fn an_expired_grants_key_cannot_decrypt_a_freshly_issued_successors_envelope() {
let now = current_timestamp();
let provider = provider_kp();
let issuer = OrgKeypair::from_bytes([1u8; 32]); let a_org = OrgKeypair::from_bytes([9u8; 32]).org_id(); let capability = CapabilityAuthorityId::for_tag("nrpc:svc");
let target = GrantTargetScope::ExactNode(provider.entity_id().clone());
let cert = OrgMembershipCert::issue_at(
&issuer,
provider.entity_id().clone(),
5,
now.saturating_sub(3600),
now + 3600,
0x1234,
);
let descriptor = CapabilitySet::new().add_tag("nrpc:svc").to_bytes_compact();
let (secret1, binding1) = OrgAudienceSecret::mint([0x11u8; 32]);
let g1 = OrgCapabilityGrant::issue_at(
&issuer,
[0x11u8; 32],
a_org,
capability,
GrantRights::DISCOVER,
target.clone(),
Some(binding1),
now.saturating_sub(7200),
now.saturating_sub(3600),
0xA1,
);
let (secret2, binding2) = OrgAudienceSecret::mint([0x22u8; 32]);
let g2 = OrgCapabilityGrant::issue_at(
&issuer,
[0x22u8; 32],
a_org,
capability,
GrantRights::DISCOVER,
target.clone(),
Some(binding2),
now.saturating_sub(3600),
now + 3600,
0xA2,
);
assert_ne!(g1.grant_id, g2.grant_id);
assert_ne!(secret1.audience_handle, secret2.audience_handle);
assert_ne!(secret1.discovery_key(), secret2.discovery_key());
let g2_env = ScopedCapabilityAnnouncement::build_granted(
&provider,
issuer.org_id(),
cert.clone(),
g2.grant_id,
secret2.audience_handle,
secret2.discovery_key(),
4,
now + 3600,
&descriptor,
)
.expect("build G2 envelope");
assert_eq!(
g2_env
.open_with(secret2.discovery_key())
.expect("K2 opens G2"),
descriptor
);
assert_eq!(
g2_env.open_with(secret1.discovery_key()).unwrap_err(),
ScopedAnnouncementError::SealOpenFailed,
"the former grant's key cannot decrypt the successor's envelope",
);
let floors = OrgRevocationState::empty();
let g2_authority = AudienceAuthority::granted(&g2, &secret2);
let ctx = granted_ctx(a_org, now, &floors);
assert!(verify_scoped_ingest(&g2_env, &g2_authority, &ctx).is_ok());
let g1_env = ScopedCapabilityAnnouncement::build_granted(
&provider,
issuer.org_id(),
cert,
g1.grant_id,
secret1.audience_handle,
secret1.discovery_key(),
4,
now + 3600,
&descriptor,
)
.expect("build G1 envelope");
let g1_authority = AudienceAuthority::granted(&g1, &secret1);
assert_eq!(
verify_scoped_ingest(&g1_env, &g1_authority, &ctx).unwrap_err(),
ScopedIngestError::GrantInvalid,
);
}
}