Skip to main content

cheers_verify/
edge.rs

1//! The [`EdgeVerifier`] facade — what a CF Worker holds.
2
3use cheers_core::{Claims, Error, PeerKey, TokenVerifier};
4
5use crate::revocation::RevocationReader;
6
7/// Edge facade: verify a token, then check it hasn't been revoked.
8///
9/// Holds a [`TokenVerifier`] and a [`RevocationReader`] — and nothing else.
10/// There is **no `TokenMinter` in this type**, so an edge built on it cannot
11/// forge sessions even if compromised; that absence is the whole point of the
12/// asymmetric codec split. Generic (not `dyn`) so the "no minter" property is a
13/// fact about the type, not a runtime convention. Pair a
14/// [`PasetoV4PublicVerifier`](crate::PasetoV4PublicVerifier) with a CF-KV-backed
15/// `RevocationReader`.
16pub struct EdgeVerifier<V, Rd> {
17    verifier: V,
18    revoked: Rd,
19}
20
21impl<V, Rd> EdgeVerifier<V, Rd>
22where
23    V: TokenVerifier + Send + Sync,
24    Rd: RevocationReader,
25{
26    pub fn new(verifier: V, revoked: Rd) -> Self {
27        Self { verifier, revoked }
28    }
29
30    /// The revocation set this verifier reads — **read-only**.
31    ///
32    /// Exposed (R728-F2) so a surface that also admits a *different* claim
33    /// shape checks the SAME set. `McpClaims` is verified by an inherent
34    /// method on [`PasetoV4PublicVerifier`](crate::PasetoV4PublicVerifier)
35    /// rather than through [`TokenVerifier`], so it cannot ride
36    /// [`verify_at`](Self::verify_at); handing such a caller its own second
37    /// reader would let the two halves of one route disagree about what is
38    /// dead. Returning the reader cannot widen anyone —
39    /// [`RevocationReader`] can ask, never revoke.
40    pub fn revocations(&self) -> &Rd {
41        &self.revoked
42    }
43
44    /// Verify `token` against `now`, then reject it if its `jti` is revoked.
45    ///
46    /// Two stages, cheapest first: cryptographic verification (stateless, local
47    /// to the edge) gates the revocation read, so a forged or expired token
48    /// never reaches the revocation set. A token that verifies but whose `jti`
49    /// is revoked returns [`Error::Revoked`].
50    pub async fn verify_at(&self, token: &str, now: i64) -> Result<Claims, Error> {
51        let claims = self.verifier.verify_at(token, now)?;
52        if self.revoked.is_revoked(&claims.jti).await? {
53            return Err(Error::Revoked);
54        }
55        Ok(claims)
56    }
57
58    /// [`verify_at`](Self::verify_at), plus: the token must be bound to
59    /// `presented` — the long-lived public key the *connecting peer* already
60    /// proved possession of in the transport handshake (R515).
61    ///
62    /// This is what turns a bearer token into a proof that **token holder ==
63    /// connecting peer**. Without it a stolen token replays under the thief's
64    /// own node key and they become the victim user; with it the stolen token
65    /// names a key the thief cannot present, and this returns
66    /// [`Error::PeerKeyMismatch`].
67    ///
68    /// Three stages, cheapest and most local first: signature (stateless),
69    /// binding (a byte compare, still local), then the revocation read (the
70    /// only I/O). An **unbound** token is rejected here too — it asserts
71    /// nothing about any node key, so it cannot satisfy a binding check. A
72    /// deployment that also admits unbound tokens must call
73    /// [`verify_at`](Self::verify_at) and branch on
74    /// [`Claims::peer_key`](cheers_core::Claims::peer_key) itself, so that
75    /// choice is visible at the call site.
76    ///
77    /// The caller supplies `presented` from its own transport — cheers has no
78    /// view of the connection and never infers the peer key from the token.
79    pub async fn verify_bound_at(
80        &self,
81        token: &str,
82        presented: &PeerKey,
83        now: i64,
84    ) -> Result<Claims, Error> {
85        let claims = self.verifier.verify_at(token, now)?;
86        if !claims.is_bound_to(presented) {
87            return Err(Error::PeerKeyMismatch);
88        }
89        if self.revoked.is_revoked(&claims.jti).await? {
90            return Err(Error::Revoked);
91        }
92        Ok(claims)
93    }
94}