jcp-wire: the endpoint decides at its own clock, and a captured request is decided once #322
5 changed files with 218 additions and 28 deletions
32
jcp/SPEC.md
32
jcp/SPEC.md
|
|
@ -1166,7 +1166,9 @@ A conforming implementation passes, with real checks:
|
||||||
decision enforces an inherited caveat; and a chain whose child dropped one does
|
decision enforces an inherited caveat; and a chain whose child dropped one does
|
||||||
not verify.
|
not verify.
|
||||||
- **C8 — wire binding:** a request round-trips the `jcp/1` framing canonically
|
- **C8 — wire binding:** a request round-trips the `jcp/1` framing canonically
|
||||||
and an enforcement endpoint decides + seals over a live socket.
|
and an enforcement endpoint decides + seals over a live socket; a replayed
|
||||||
|
request is refused `replayed`, and one stamped outside the window
|
||||||
|
`stale-request`.
|
||||||
- **C9 — energy honesty:** the metering ladder always yields a figure and its
|
- **C9 — energy honesty:** the metering ladder always yields a figure and its
|
||||||
provenance is internally consistent.
|
provenance is internally consistent.
|
||||||
- **C10 — delegation control:** depth narrows per hop; a leaf-only grant cannot
|
- **C10 — delegation control:** depth narrows per hop; a leaf-only grant cannot
|
||||||
|
|
@ -1231,10 +1233,16 @@ WireResponse { decision, receipt: bytes }
|
||||||
```
|
```
|
||||||
|
|
||||||
`chain` is the full delegation chain, root first (a single un-delegated grant is
|
`chain` is the full delegation chain, root first (a single un-delegated grant is
|
||||||
a one-link chain). `now` makes decision time an explicit input — a decision is
|
a one-link chain). The response carries the deterministic decision and the
|
||||||
replayable, not ambient. The response carries the deterministic decision and the
|
|
||||||
sealed JCR-1 receipt, deny and allow alike.
|
sealed JCR-1 receipt, deny and allow alike.
|
||||||
|
|
||||||
|
**Time is the enforcement point's.** The endpoint MUST decide at its own clock.
|
||||||
|
`now` is the time the caller signed the request, a freshness stamp: the endpoint
|
||||||
|
MUST deny a request whose `now` differs from its clock by more than its
|
||||||
|
freshness window as `stale-request`, and MUST NOT decide any request as of the
|
||||||
|
time it names. A caller that chose the decision time could stamp a request
|
||||||
|
before its grant expired, or inside a time caveat it has since left.
|
||||||
|
|
||||||
**Caller authentication.** `signature` is Ed25519 by `caller` over the
|
**Caller authentication.** `signature` is Ed25519 by `caller` over the
|
||||||
canonical bytes of every other field. The endpoint MUST verify it before
|
canonical bytes of every other field. The endpoint MUST verify it before
|
||||||
deciding; a request that fails is denied `bad-signature` and sealed like any
|
deciding; a request that fails is denied `bad-signature` and sealed like any
|
||||||
|
|
@ -1242,10 +1250,20 @@ other decision — the refused impersonation is evidence too. A threshold actor
|
||||||
(§2.2) signs the same field with a k-of-n aggregate, indistinguishable on the
|
(§2.2) signs the same field with a k-of-n aggregate, indistinguishable on the
|
||||||
wire.
|
wire.
|
||||||
|
|
||||||
The wire never carries a secret: grants are signed public objects, and a
|
**A request is decided once.** The wire never carries a secret: grants are
|
||||||
captured request replays into the same deterministic decision and the same
|
signed public objects, and a request is a signed public object too. The endpoint
|
||||||
budget accounting it produced the first time. The endpoint behind the
|
MUST remember every request it decided while that request's stamp is inside the
|
||||||
enforcement point is invoked only on Allow.
|
window, and MUST deny one presented again as `replayed`, so a captured request
|
||||||
|
is neither decided nor debited a second time. Once a request's stamp has left
|
||||||
|
the window, a replay of it is stale. Two byte-identical requests are one
|
||||||
|
request, so a caller that means the same action twice stamps the second later.
|
||||||
|
|
||||||
|
**The cost is the holder's measurement.** `cost` is the requester's own figure,
|
||||||
|
and the endpoint debits it as given, so a budget binds only as honestly as that
|
||||||
|
figure was measured. The requester MUST therefore be the party that metered the
|
||||||
|
action: the grant's holder is the enforcement point that ran it, as a steward is
|
||||||
|
in Mesh, rather than the agent the grant governs reporting its own consumption.
|
||||||
|
The endpoint behind the enforcement point is invoked only on Allow.
|
||||||
|
|
||||||
## 12. Non-goals
|
## 12. Non-goals
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ pub fn checks() -> Vec<Check> {
|
||||||
Check { id: "C5 receipt seal + verify", run: c5_receipt },
|
Check { id: "C5 receipt seal + verify", run: c5_receipt },
|
||||||
Check { id: "C6 revocation authority + subtree kill", run: c6_revocation },
|
Check { id: "C6 revocation authority + subtree kill", run: c6_revocation },
|
||||||
Check { id: "C7 chain verification + budget conservation + inherited caveats", run: c7_chain },
|
Check { id: "C7 chain verification + budget conservation + inherited caveats", run: c7_chain },
|
||||||
Check { id: "C8 wire binding round-trip + live decide", run: c8_wire },
|
Check { id: "C8 wire binding round-trip + live decide + no replay, endpoint clock", run: c8_wire },
|
||||||
Check { id: "C9 energy provenance honesty", run: c9_energy },
|
Check { id: "C9 energy provenance honesty", run: c9_energy },
|
||||||
Check { id: "C10 delegation control (depth narrows, leaf-only holds)", run: c10_delegation_control },
|
Check { id: "C10 delegation control (depth narrows, leaf-only holds)", run: c10_delegation_control },
|
||||||
Check { id: "C11 sharded budgets conserve the root at mint", run: c11_shard },
|
Check { id: "C11 sharded budgets conserve the root at mint", run: c11_shard },
|
||||||
|
|
@ -583,7 +583,7 @@ fn c8_wire() -> Result<(), String> {
|
||||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| e.to_string())?;
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| e.to_string())?;
|
||||||
let addr = listener.local_addr().map_err(|e| e.to_string())?;
|
let addr = listener.local_addr().map_err(|e| e.to_string())?;
|
||||||
let server = std::thread::spawn(move || {
|
let server = std::thread::spawn(move || {
|
||||||
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), steward_sk, "did:example:steward");
|
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), steward_sk, "did:example:steward").with_clock(|| 1);
|
||||||
let (mut stream, _) = listener.accept().unwrap();
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
let _ = ep.serve_stream(&mut stream);
|
let _ = ep.serve_stream(&mut stream);
|
||||||
});
|
});
|
||||||
|
|
@ -594,8 +594,28 @@ fn c8_wire() -> Result<(), String> {
|
||||||
}
|
}
|
||||||
jcp_receipt::verify(&req.frame, &sg.grant, &resp.decision, &resp.receipt, "did:example:steward", &steward_vk.to_bytes())
|
jcp_receipt::verify(&req.frame, &sg.grant, &resp.decision, &resp.receipt, "did:example:steward", &steward_vk.to_bytes())
|
||||||
.map_err(|e| format!("wire receipt failed to verify: {e}"))?;
|
.map_err(|e| format!("wire receipt failed to verify: {e}"))?;
|
||||||
|
// The same request again is refused, not decided and debited twice.
|
||||||
|
let again = client.call(&req).map_err(|e| e.to_string())?;
|
||||||
|
if again.decision.verdict != Verdict::Deny(DenyReason::Replayed) {
|
||||||
|
return Err(format!("a replayed wire request was decided again: {:?}", again.decision.verdict));
|
||||||
|
}
|
||||||
drop(client);
|
drop(client);
|
||||||
server.join().map_err(|_| "wire server panicked".to_string())?;
|
server.join().map_err(|_| "wire server panicked".to_string())?;
|
||||||
|
|
||||||
|
// The endpoint decides at its own clock: a request stamped outside its
|
||||||
|
// window is refused rather than decided as of the time it names.
|
||||||
|
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(|| 1_000).with_skew(60);
|
||||||
|
if ep.handle(&req).decision.verdict != Verdict::Deny(DenyReason::StaleRequest) {
|
||||||
|
return Err("a wire request stamped outside the window was decided".into());
|
||||||
|
}
|
||||||
|
// Inside the window, the clock still decides: a grant that expires between
|
||||||
|
// the request's stamp and the endpoint's clock is expired.
|
||||||
|
let short = grant(&issuer, holder, r, &["read"], 100_000, 999);
|
||||||
|
let late = jcp_wire::WireRequest::sign(frame(&short, r, "read", Trust::Control), vec![short.clone()], &sk(2), Energy::estimator(1), 998)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if ep.handle(&late).decision.verdict != Verdict::Deny(DenyReason::Expired) {
|
||||||
|
return Err("a wire request was decided at its own stamp rather than the endpoint's clock".into());
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -854,7 +874,7 @@ fn c13_signed_caller() -> Result<(), String> {
|
||||||
// two can — and the endpoint treats it as any signed caller
|
// two can — and the endpoint treats it as any signed caller
|
||||||
let sig = team.sign(&bytes, &[1, 3], &mut rng).map_err(|e| e.to_string())?;
|
let sig = team.sign(&bytes, &[1, 3], &mut rng).map_err(|e| e.to_string())?;
|
||||||
let req = jcp_wire::WireRequest::with_signature(f.clone(), chain.clone(), team.group_did(), cost, 1, sig);
|
let req = jcp_wire::WireRequest::with_signature(f.clone(), chain.clone(), team.group_did(), cost, 1, sig);
|
||||||
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), sk(7), "did:example:steward");
|
let mut ep = jcp_wire::Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(|| 1);
|
||||||
if !ep.handle(&req).decision.is_allow() {
|
if !ep.handle(&req).decision.is_allow() {
|
||||||
return Err("a properly threshold-signed request was denied".into());
|
return Err("a properly threshold-signed request was denied".into());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -602,6 +602,13 @@ pub enum DenyReason {
|
||||||
/// (SPEC §4.10) — a tighter wallet for work that has touched an untrusted
|
/// (SPEC §4.10) — a tighter wallet for work that has touched an untrusted
|
||||||
/// source, distinct from the cumulative `BudgetExceeded`.
|
/// source, distinct from the cumulative `BudgetExceeded`.
|
||||||
TaintBudgetExceeded,
|
TaintBudgetExceeded,
|
||||||
|
/// A wire request stamped with a `now` outside the enforcement point's
|
||||||
|
/// freshness window (SPEC §11). The point decides at its own clock, and a
|
||||||
|
/// request signed for another time is not decided.
|
||||||
|
StaleRequest,
|
||||||
|
/// A wire request the enforcement point has already decided, presented
|
||||||
|
/// again (SPEC §11). It is decided once, and debited once.
|
||||||
|
Replayed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DenyReason {
|
impl DenyReason {
|
||||||
|
|
@ -623,6 +630,8 @@ impl DenyReason {
|
||||||
DenyReason::UntrustedRoot => "untrusted-root",
|
DenyReason::UntrustedRoot => "untrusted-root",
|
||||||
DenyReason::TaintedFlow => "tainted-flow",
|
DenyReason::TaintedFlow => "tainted-flow",
|
||||||
DenyReason::TaintBudgetExceeded => "taint-budget-exceeded",
|
DenyReason::TaintBudgetExceeded => "taint-budget-exceeded",
|
||||||
|
DenyReason::StaleRequest => "stale-request",
|
||||||
|
DenyReason::Replayed => "replayed",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,8 @@ mod tests {
|
||||||
jcp_runtime::Runtime::new(),
|
jcp_runtime::Runtime::new(),
|
||||||
ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
|
ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
|
||||||
"did:example:steward",
|
"did:example:steward",
|
||||||
);
|
)
|
||||||
|
.with_clock(|| 5);
|
||||||
let resp = ep.handle(&req);
|
let resp = ep.handle(&req);
|
||||||
assert!(resp.decision.is_allow());
|
assert!(resp.decision.is_allow());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,18 @@
|
||||||
//!
|
//!
|
||||||
//! The decision stays on the enforcement side: the endpoint behind the server
|
//! The decision stays on the enforcement side: the endpoint behind the server
|
||||||
//! is invoked only on Allow. The wire never carries a secret — grants are
|
//! is invoked only on Allow. The wire never carries a secret — grants are
|
||||||
//! signed public objects, and a captured request replays into the same
|
//! signed public objects. The endpoint decides at **its own clock**: a
|
||||||
//! deterministic decision (and the same budget accounting) it would have
|
//! request's `now` is a freshness stamp, and one outside the window
|
||||||
//! produced the first time.
|
//! ([`Endpoint::with_skew`]) is denied `stale-request`. Within the window the
|
||||||
|
//! endpoint remembers what it decided, so a captured request presented again
|
||||||
|
//! is denied `replayed` rather than decided, and debited, a second time.
|
||||||
|
//!
|
||||||
|
//! The `cost` a request declares is the requester's own figure, so the
|
||||||
|
//! requester is the grant's holder and the party that metered the action
|
||||||
|
//! (SPEC §11): an endpoint takes no figure from a party it does not hold to
|
||||||
|
//! account for it.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
|
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
|
@ -26,7 +34,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use jcp_runtime::{Decision, Runtime};
|
use jcp_runtime::{Decision, Runtime};
|
||||||
use jcp_schema::{canonical_cbor, from_canonical_cbor, Did, Energy, Frame, SchemaError, SignedGrant};
|
use jcp_schema::{canonical_cbor, from_canonical_cbor, ContentId, Did, Energy, Frame, SchemaError, SignedGrant};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Cap on a single wire message. Generous for grant chains, hostile to a
|
/// Cap on a single wire message. Generous for grant chains, hostile to a
|
||||||
|
|
@ -60,8 +68,9 @@ pub struct WireRequest {
|
||||||
pub chain: Vec<SignedGrant>,
|
pub chain: Vec<SignedGrant>,
|
||||||
pub caller: Did,
|
pub caller: Did,
|
||||||
pub cost: Energy,
|
pub cost: Energy,
|
||||||
/// Decision-time seconds; the enforcement point treats time as input, not
|
/// When the caller signed the request, unix seconds: a freshness stamp. The
|
||||||
/// ambient state, so a decision is replayable.
|
/// enforcement point decides at its own clock and refuses a request whose
|
||||||
|
/// stamp is outside its window, so this cannot choose the decision time.
|
||||||
pub now: u64,
|
pub now: u64,
|
||||||
/// Ed25519 (64 bytes) by `caller` over [`WireRequest::signable_bytes`].
|
/// Ed25519 (64 bytes) by `caller` over [`WireRequest::signable_bytes`].
|
||||||
pub signature: Vec<u8>,
|
pub signature: Vec<u8>,
|
||||||
|
|
@ -137,6 +146,12 @@ pub struct WireResponse {
|
||||||
pub receipt: Vec<u8>,
|
pub receipt: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn system_clock() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map_or(0, |d| d.as_secs())
|
||||||
|
}
|
||||||
|
|
||||||
/// Write one length-prefixed canonical-CBOR message.
|
/// Write one length-prefixed canonical-CBOR message.
|
||||||
pub fn write_msg<T: Serialize>(w: &mut impl Write, msg: &T) -> Result<(), WireError> {
|
pub fn write_msg<T: Serialize>(w: &mut impl Write, msg: &T) -> Result<(), WireError> {
|
||||||
let body = canonical_cbor(msg)?;
|
let body = canonical_cbor(msg)?;
|
||||||
|
|
@ -171,17 +186,63 @@ pub fn read_msg<T: serde::de::DeserializeOwned>(r: &mut impl Read) -> Result<T,
|
||||||
Ok(from_canonical_cbor(&body)?)
|
Ok(from_canonical_cbor(&body)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far, by default, a request's `now` may stray from the endpoint's clock.
|
||||||
|
pub const DEFAULT_SKEW_SECS: u64 = 300;
|
||||||
|
|
||||||
/// An enforcement point serving `jcp/1` over byte streams: the runtime that
|
/// An enforcement point serving `jcp/1` over byte streams: the runtime that
|
||||||
/// decides, and the steward key that seals every decision.
|
/// decides, and the steward key that seals every decision.
|
||||||
pub struct Endpoint {
|
pub struct Endpoint {
|
||||||
pub runtime: Runtime,
|
pub runtime: Runtime,
|
||||||
steward_sk: SigningKey,
|
steward_sk: SigningKey,
|
||||||
steward: String,
|
steward: String,
|
||||||
|
/// Unix seconds: the time every decision is made at.
|
||||||
|
clock: fn() -> u64,
|
||||||
|
/// How far a request's `now` may stray from `clock`.
|
||||||
|
skew_secs: u64,
|
||||||
|
/// The requests decided within the window, by content id, with their stamp.
|
||||||
|
decided: HashMap<ContentId, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Endpoint {
|
impl Endpoint {
|
||||||
|
/// An endpoint deciding at the system clock, with a
|
||||||
|
/// [`DEFAULT_SKEW_SECS`] freshness window.
|
||||||
pub fn new(runtime: Runtime, steward_sk: SigningKey, steward: impl Into<String>) -> Self {
|
pub fn new(runtime: Runtime, steward_sk: SigningKey, steward: impl Into<String>) -> Self {
|
||||||
Self { runtime, steward_sk, steward: steward.into() }
|
Self {
|
||||||
|
runtime,
|
||||||
|
steward_sk,
|
||||||
|
steward: steward.into(),
|
||||||
|
clock: system_clock,
|
||||||
|
skew_secs: DEFAULT_SKEW_SECS,
|
||||||
|
decided: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decide at `clock` (unix seconds) instead of the system clock.
|
||||||
|
pub fn with_clock(mut self, clock: fn() -> u64) -> Self {
|
||||||
|
self.clock = clock;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept a request whose `now` is within `secs` of the clock.
|
||||||
|
pub fn with_skew(mut self, secs: u64) -> Self {
|
||||||
|
self.skew_secs = secs;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `req` may be decided now: fresh, and not already decided. A
|
||||||
|
/// request that may be is remembered until its stamp leaves the window,
|
||||||
|
/// after which a replay of it is stale.
|
||||||
|
fn admit(&mut self, req: &WireRequest, now: u64) -> Result<(), jcp_schema::DenyReason> {
|
||||||
|
let skew = self.skew_secs;
|
||||||
|
self.decided.retain(|_, stamp| stamp.saturating_add(skew) >= now);
|
||||||
|
if now.abs_diff(req.now) > skew {
|
||||||
|
return Err(jcp_schema::DenyReason::StaleRequest);
|
||||||
|
}
|
||||||
|
let id = ContentId::of(req).map_err(|_| jcp_schema::DenyReason::BadSignature)?;
|
||||||
|
if self.decided.insert(id, req.now).is_some() {
|
||||||
|
return Err(jcp_schema::DenyReason::Replayed);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decide one request and seal the receipt — the whole protocol step,
|
/// Decide one request and seal the receipt — the whole protocol step,
|
||||||
|
|
@ -189,18 +250,19 @@ impl Endpoint {
|
||||||
/// unauthenticated request is denied (`bad-signature`) and sealed like any
|
/// unauthenticated request is denied (`bad-signature`) and sealed like any
|
||||||
/// other decision — the refused impersonation is evidence too.
|
/// other decision — the refused impersonation is evidence too.
|
||||||
pub fn handle(&mut self, req: &WireRequest) -> WireResponse {
|
pub fn handle(&mut self, req: &WireRequest) -> WireResponse {
|
||||||
let decision = if req.verify_caller() {
|
let now = (self.clock)();
|
||||||
self.runtime.decide_chained(&req.frame, &req.chain, req.caller, req.cost, req.now)
|
let refused = if req.verify_caller() { self.admit(req, now).err() } else { Some(jcp_schema::DenyReason::BadSignature) };
|
||||||
} else {
|
let decision = match refused {
|
||||||
Decision {
|
None => self.runtime.decide_chained(&req.frame, &req.chain, req.caller, req.cost, now),
|
||||||
verdict: jcp_schema::Verdict::Deny(jcp_schema::DenyReason::BadSignature),
|
Some(reason) => Decision {
|
||||||
|
verdict: jcp_schema::Verdict::Deny(reason),
|
||||||
cost: req.cost,
|
cost: req.cost,
|
||||||
budget_remaining_uj: req
|
budget_remaining_uj: req
|
||||||
.chain
|
.chain
|
||||||
.last()
|
.last()
|
||||||
.map(|l| l.grant.budget_uj.saturating_sub(self.runtime.spent(&l.grant.id())))
|
.map(|l| l.grant.budget_uj.saturating_sub(self.runtime.spent(&l.grant.id())))
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
// The receipt names the leaf grant and — for a multi-hop chain — every
|
// The receipt names the leaf grant and — for a multi-hop chain — every
|
||||||
// debited link, so the receipt stream replays exactly (SPEC §8.1).
|
// debited link, so the receipt stream replays exactly (SPEC §8.1).
|
||||||
|
|
@ -318,6 +380,86 @@ mod tests {
|
||||||
(vec![root, leaf], did(&sk(3)), r)
|
(vec![root, leaf], did(&sk(3)), r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The fixture's requests are stamped 10; the existing tests decide there.
|
||||||
|
fn at_10() -> u64 {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A request stamped `now`, signed by the fixture's caller.
|
||||||
|
fn stamped(chain: &[SignedGrant], r: ContentId, cost: u64, now: u64) -> WireRequest {
|
||||||
|
let frame = request(chain, did(&sk(3)), r, Trust::Control, cost).frame;
|
||||||
|
WireRequest::sign(frame, chain.to_vec(), &sk(3), Energy::estimator(cost), now).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fixture's chain with a root and leaf that expire at `at`.
|
||||||
|
fn expiring(at: u64) -> (Vec<SignedGrant>, ContentId) {
|
||||||
|
let (chain, _, r) = fixture();
|
||||||
|
let mut g = chain[0].grant.clone();
|
||||||
|
g.expiry = at;
|
||||||
|
let root = issue(&sk(1), g).unwrap();
|
||||||
|
let leaf = attenuate(&root, &sk(2), did(&sk(3)), Capability::new(r, ["read"]), 50_000, at, [1; 16]).unwrap();
|
||||||
|
(vec![root, leaf], r)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn at_100() -> u64 {
|
||||||
|
100
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The endpoint decides at its own clock. A caller used to choose the
|
||||||
|
/// decision time by stamping its request, so a grant that had expired was
|
||||||
|
/// decided as of a moment it had not.
|
||||||
|
#[test]
|
||||||
|
fn the_endpoint_decides_at_its_own_clock() {
|
||||||
|
// Expires at 99. A request stamped 97 is inside the window, and was
|
||||||
|
// valid at its stamp, but the endpoint's clock reads 100.
|
||||||
|
let (chain, r) = expiring(99);
|
||||||
|
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_100).with_skew(5);
|
||||||
|
let fresh = ep.handle(&stamped(&chain, r, 1, 97));
|
||||||
|
assert_eq!(fresh.decision.verdict, Verdict::Deny(DenyReason::Expired), "decided at 100, not at the stamp 97");
|
||||||
|
let backdated = ep.handle(&stamped(&chain, r, 1, 40));
|
||||||
|
assert_eq!(backdated.decision.verdict, Verdict::Deny(DenyReason::StaleRequest), "stamped outside the window");
|
||||||
|
assert!(!backdated.receipt.is_empty(), "sealed like any other denial");
|
||||||
|
assert_eq!(ep.runtime.spent(&chain[1].grant.id()), 0);
|
||||||
|
|
||||||
|
// The same chain at an endpoint whose clock is before the expiry: allowed.
|
||||||
|
let (chain, r) = expiring(200);
|
||||||
|
let mut ok = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_100).with_skew(5);
|
||||||
|
assert!(ok.handle(&stamped(&chain, r, 1, 97)).decision.is_allow());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A captured request presented again is refused, not decided and debited
|
||||||
|
/// a second time; a request stamped differently is a new request.
|
||||||
|
#[test]
|
||||||
|
fn a_captured_request_is_decided_once() {
|
||||||
|
let (chain, _, r) = fixture();
|
||||||
|
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_10);
|
||||||
|
let req = stamped(&chain, r, 10_000, 10);
|
||||||
|
assert!(ep.handle(&req).decision.is_allow());
|
||||||
|
let again = ep.handle(&req);
|
||||||
|
assert_eq!(again.decision.verdict, Verdict::Deny(DenyReason::Replayed));
|
||||||
|
assert_eq!(ep.runtime.spent(&chain[1].grant.id()), 10_000, "debited once");
|
||||||
|
assert!(ep.handle(&stamped(&chain, r, 10_000, 11)).decision.is_allow());
|
||||||
|
assert_eq!(ep.runtime.spent(&chain[1].grant.id()), 20_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CLOCK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1_000);
|
||||||
|
fn moving() -> u64 {
|
||||||
|
CLOCK.load(std::sync::atomic::Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The endpoint forgets a request once its stamp leaves the window, and a
|
||||||
|
/// replay after that is stale rather than fresh again.
|
||||||
|
#[test]
|
||||||
|
fn a_replay_after_the_window_is_stale() {
|
||||||
|
let (chain, _, r) = fixture();
|
||||||
|
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(moving).with_skew(30);
|
||||||
|
let req = stamped(&chain, r, 1, 1_000);
|
||||||
|
assert!(ep.handle(&req).decision.is_allow());
|
||||||
|
CLOCK.store(1_031, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
assert_eq!(ep.handle(&req).decision.verdict, Verdict::Deny(DenyReason::StaleRequest));
|
||||||
|
assert!(ep.decided.is_empty(), "nothing outside the window is remembered");
|
||||||
|
}
|
||||||
|
|
||||||
// the fixture's caller is sk(3) — sign as them
|
// the fixture's caller is sk(3) — sign as them
|
||||||
fn request(chain: &[SignedGrant], _caller: Did, r: ContentId, trust: Trust, cost: u64) -> WireRequest {
|
fn request(chain: &[SignedGrant], _caller: Did, r: ContentId, trust: Trust, cost: u64) -> WireRequest {
|
||||||
let frame = Frame {
|
let frame = Frame {
|
||||||
|
|
@ -350,7 +492,7 @@ mod tests {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
let addr = listener.local_addr().unwrap();
|
let addr = listener.local_addr().unwrap();
|
||||||
let server = std::thread::spawn(move || {
|
let server = std::thread::spawn(move || {
|
||||||
let mut ep = Endpoint::new(Runtime::new(), steward_sk, "did:example:steward");
|
let mut ep = Endpoint::new(Runtime::new(), steward_sk, "did:example:steward").with_clock(at_10);
|
||||||
let (mut stream, _) = listener.accept().unwrap();
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
ep.serve_stream(&mut stream).unwrap();
|
ep.serve_stream(&mut stream).unwrap();
|
||||||
});
|
});
|
||||||
|
|
@ -385,7 +527,7 @@ mod tests {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
let addr = listener.local_addr().unwrap();
|
let addr = listener.local_addr().unwrap();
|
||||||
let server = std::thread::spawn(move || {
|
let server = std::thread::spawn(move || {
|
||||||
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward");
|
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_10);
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
let (mut stream, _) = listener.accept().unwrap();
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
ep.serve_stream(&mut stream).unwrap();
|
ep.serve_stream(&mut stream).unwrap();
|
||||||
|
|
@ -419,7 +561,7 @@ mod tests {
|
||||||
// the issuer revoked the ROOT before this endpoint came up
|
// the issuer revoked the ROOT before this endpoint came up
|
||||||
let sr = revoke(&sk(1), &root_grant, [9; 16]).unwrap();
|
let sr = revoke(&sk(1), &root_grant, [9; 16]).unwrap();
|
||||||
rt.apply_revocation(&sr, &root_grant).unwrap();
|
rt.apply_revocation(&sr, &root_grant).unwrap();
|
||||||
let mut ep = Endpoint::new(rt, sk(7), "did:example:steward");
|
let mut ep = Endpoint::new(rt, sk(7), "did:example:steward").with_clock(at_10);
|
||||||
let (mut stream, _) = listener.accept().unwrap();
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
ep.serve_stream(&mut stream).unwrap();
|
ep.serve_stream(&mut stream).unwrap();
|
||||||
});
|
});
|
||||||
|
|
@ -443,7 +585,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn an_impersonated_caller_is_denied_and_sealed() {
|
fn an_impersonated_caller_is_denied_and_sealed() {
|
||||||
let (chain, caller, r) = fixture();
|
let (chain, caller, r) = fixture();
|
||||||
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward");
|
let mut ep = Endpoint::new(Runtime::new(), sk(7), "did:example:steward").with_clock(at_10);
|
||||||
// a request claiming the caller's did, signed by someone else
|
// a request claiming the caller's did, signed by someone else
|
||||||
let frame = request(&chain, caller, r, Trust::Control, 1).frame;
|
let frame = request(&chain, caller, r, Trust::Control, 1).frame;
|
||||||
let bytes = WireRequest::signable_bytes(&frame, &chain, &caller, &Energy::estimator(1), 10).unwrap();
|
let bytes = WireRequest::signable_bytes(&frame, &chain, &caller, &Energy::estimator(1), 10).unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue