Skip to main content

cratefield_core/ports/
signer.rs

1//! The `Signer` port and the signed-token payload (ADR 0006). The HMAC
2//! reference implementation lives in `cratefield-core::signer` (issue #3).
3
4use thiserror::Error;
5
6/// Which secret a token was signed with. Tokens name their key so rotation
7/// never breaks links in flight.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Kid {
10    Cur,
11    Prev,
12}
13
14/// The signed payload: `{ purpose, subject, exp?, kid }`.
15///
16/// `purpose` scopes a token to one use (`confirm`, `unsubscribe`, ...), so a
17/// confirm link can never be replayed as an unsubscribe. `exp` is a Unix
18/// timestamp in seconds; confirm tokens expire (7-day default), unsubscribe
19/// tokens do not.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Payload {
22    pub purpose: String,
23    pub subject: String,
24    pub exp: Option<u64>,
25    pub kid: Kid,
26}
27
28/// Failures surfaced by `verify` beyond "the token is simply invalid",
29/// which is reported as `None`.
30#[derive(Debug, Clone, PartialEq, Eq, Error)]
31pub enum SignatureError {
32    #[error("token payload is not valid UTF-8/JSON")]
33    Malformed,
34    #[error("token is expired")]
35    Expired,
36    #[error("token purpose {actual:?} does not match expected {expected:?}")]
37    WrongPurpose { actual: String, expected: String },
38}
39
40/// Produces and verifies `base64url(json).base64url(mac)` tokens where the
41/// MAC is computed over the **encoded** payload string, so a token has
42/// exactly one valid encoding (ADR 0006).
43pub trait Signer: Send + Sync {
44    fn sign(&self, payload: &Payload) -> String;
45    /// `None` for malformed input, tampering, expiry or wrong purpose;
46    /// never panics on malformed input.
47    fn verify(&self, token: &str, expected_purpose: &str) -> Option<Payload>;
48}