Attenuation's axes all narrow by set-subtraction: remove actions, lower a ceiling, shorten an expiry. They cannot express a condition the issuer did not anticipate. `jcp-policy` can, but it is service-side — evaluated against a PolicyContext the verifier must be told about, not carried in the capability. A caveat is a condition embedded INSIDE the signed grant. Any enforcement point holding it checks offline: no call back to the issuer, no shared policy config. That is the distinction — policy is something a verifier must be told; a caveat is something the token carries. A design constraint found in the code rather than assumed: the predicate set is bounded by what `decide` actually holds. jcp-policy's `Match::ContentContains` CANNOT become a caveat, because the runtime sees a frame's `payload_hash` and never its bytes. So a condition on content is expressible only as `PayloadIs`, binding an exact payload rather than inspecting one. SPEC §4.9 states that constraint normatively so implementations don't invent undecidable predicates. Two variants express authority the fixed axes cannot: - `PayloadIs` makes a grant ONE-SHOT — it authorizes one specific call, so a leaked grant cannot be replayed against different arguments. `capability` narrows to an action; it can never narrow to a call. - `MaxCostUj` bounds a SINGLE invocation, where `budget_uj` is cumulative — so a delegate cannot burn the whole remaining budget in one call. Plus ActionIn (may narrow below the capability), CallerIs, NotBefore, NotAfter. Enforcement is in decide/decide_priced with the real cost. The two-phase `authorize` path skips only the cost-dependent caveat — the cost is unknowable there — and enforces every other; that binds at decide time and is documented rather than silently ignored. Attenuation is monotone: `attenuate` INHERITS the parent's caveats (dropping them would widen a delegate's authority — the same amplification the dual-ceiling work had to guard), `attenuate_caveated` appends, and `caveats_narrow` refuses a child that lost one. Adding the field with no default made the compiler flag every Grant construction site, which is how three bad auto-patches into function parameter lists surfaced as build errors instead of silent breakage. Empty caveat set ⇒ omitted from the canonical encoding, so a grant without caveats is byte-identical to one minted before they existed — content id and signature unchanged, asserted by test and by C24. 42 suites green, clippy clean, all conformance checks pass including C24. Co-Authored-By: Claude Opus 4.8 <[email protected]>
31 lines
1.2 KiB
Rust
31 lines
1.2 KiB
Rust
//! Mint a real JCP-grant-backed OAuth access token and print it + the issuer
|
|
//! public key, so an independent verifier (any RFC 7515/9068 JWT stack) can
|
|
//! check it. Used to prove the binding interoperates outside Rust.
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_bridge::oauth::grant_to_access_token;
|
|
use jcp_grant::issue;
|
|
use jcp_schema::{Capability, ContentId, Did, Grant};
|
|
|
|
fn main() {
|
|
let sk = SigningKey::from_bytes(&[9u8; 32]);
|
|
let issuer = Did::from_verifying_key(&sk.verifying_key());
|
|
let subject = Did::from_verifying_key(&SigningKey::from_bytes(&[2u8; 32]).verifying_key());
|
|
let grant = Grant {
|
|
issuer,
|
|
subject,
|
|
capability: Capability::new(ContentId::of_bytes(b"resource"), ["read", "write"]),
|
|
budget_uj: 5_000_000,
|
|
budget_funds: None,
|
|
caveats: Vec::new(),
|
|
expiry: 0,
|
|
parent: None,
|
|
delegate_depth: Some(3),
|
|
nonce: [7u8; 16],
|
|
};
|
|
let sg = issue(&sk, grant).expect("issue");
|
|
let token = grant_to_access_token(&sg, &sk, 1_000_000, 3600).expect("mint");
|
|
let hex: String = sk.verifying_key().to_bytes().iter().map(|b| format!("{b:02x}")).collect();
|
|
println!("TOKEN {token}");
|
|
println!("PUBKEY {hex}");
|
|
}
|