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}