Skip to content

Harden SealProof and engineer non-transitive authority #46

@bordumb

Description

@bordumb

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 seal
  • crates/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 Has

    function signatures

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions