-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Two engineering fixes from the deep analysis
1. Harden SealProof (seal the seal)
Current state: SealProof is a public unit struct. External code can write:
impl Permission for MyType {
type __CapsecSeal = capsec_core::__private::SealProof;
}This is a soft barrier (#[doc(hidden)]), not a hard one. While exploiting it doesn't enable forging built-in caps, the README claims "no external code can forge a Cap<P> in safe Rust" which is technically inaccurate.
Proposed fix: Change SealProof from a unit struct to a struct with a private field, constructible only via a #[doc(hidden)] function. Then change Cap::__capsec_new_derived() to require a value proof (not just a type-level associated type):
#[doc(hidden)]
pub mod __private {
pub struct SealProof(()); // private field
pub trait SealToken {}
impl SealToken for SealProof {}
#[doc(hidden)]
pub const fn __capsec_seal() -> SealProof {
SealProof(())
}
}// Requires a value proof, not just a type
#[doc(hidden)]
pub fn __capsec_new_derived(_seal: crate::__private::SealProof) -> Self
where
P: Permission<__CapsecSeal = crate::__private::SealProof>,Attacker now needs to explicitly call __capsec_seal() — not just name a type. Still bypassable (pub), but requires deliberate intent. The naming makes abuse obvious in code review.
Limitation: Cannot be made fully airtight at the library level in stable Rust. Any type that a proc macro references from a user crate must be pub.
2. Non-transitive authority via CapProvider (the bigger win)
Current state: Has<FsRead> passes full authority. Attenuated<P, S> does NOT implement Has<P>, so scoped capabilities can't be used transparently in the Has<P> system. This means capsec does not achieve Wyvern-style non-transitive authority.
Why Attenuated can't implement Has
: If Attenuated<FsRead, DirScope> implements Has<FsRead>, any callee can extract an unscoped Cap<FsRead> via cap_ref() and bypass the scope entirely.
Proposed fix: CapProvider<P> trait that unifies Has<P> (unscoped) and Attenuated<P, S> (scoped), with transparent scope enforcement at the I/O site:
pub trait CapProvider<P: Permission> {
fn provide_cap(&self, target: &str) -> Result<Cap<P>, CapSecError>;
}
// Blanket: Has<P> always provides (no scope check)
impl<P: Permission, T: Has<P>> CapProvider<P> for T {
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
Ok(self.cap_ref())
}
}
// Attenuated: scope check on every I/O
impl<P: Permission, S: Scope> CapProvider<P> for Attenuated<P, S> {
fn provide_cap(&self, target: &str) -> Result<Cap<P>, CapSecError> {
self.check(target)?;
Ok(Cap::new())
}
}capsec-std functions change from &impl Has<P> to &impl CapProvider<P>:
// Before
pub fn read(path: impl AsRef<Path>, cap: &impl Has<FsRead>) -> io::Result<Vec<u8>>
// After — accepts both raw caps and scoped caps
pub fn read(path: impl AsRef<Path>, cap: &impl CapProvider<FsRead>) -> Result<Vec<u8>, CapSecError>What this achieves: A logger module wrapping Attenuated<FsWrite, DirScope> can pass it to capsec-std functions, and the scope is enforced at every I/O site. Callees can't escape the scope even though they receive what looks like FsWrite authority. This IS Wyvern-style non-transitive authority.
Breaking change: capsec-std function signatures change. Existing Has<P> code continues to work via the blanket impl, but signatures are technically different. Major version bump.
Priority
Issue 2 (CapProvider) is the higher-impact fix — a genuine architectural improvement that closes the gap with Wyvern. Issue 1 (seal hardening) is incremental.
References
- Deep analysis: Section 1.1 (SealProof attack) and Section 2.3 (non-transitivity)
- Melicher et al. (2017): authority non-transitivity theorem
crates/capsec-core/src/permission.rs:163-176— current sealcrates/capsec-core/src/cap.rs:48-64—__capsec_new_derived()crates/capsec-core/src/attenuate.rs— Attenuated<P, S>crates/capsec-std/src/fs.rs— current Hasfunction signatures