use std::sync::Arc;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use hmac::{Hmac, KeyInit, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use subtle::ConstantTimeEq;
use crate::Clock;
use crate::ports::signer::{Kid, MAX_KID_NAME, Payload, Signer};
type HmacSha256 = Hmac<Sha256>;
pub const MIN_SECRET_BYTES: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SignerError {
#[error("harness secret must be at least {MIN_SECRET_BYTES} bytes")]
SecretTooShort,
#[error("key id {0:?} is already present in the ring")]
KeyIdTaken(String),
#[error("key id {0:?} was revoked and can never re-enter the ring (issue #137)")]
KeyRevoked(String),
#[error("a revoked secret can never be re-added under another key id (issue #137)")]
RevokedSecretReuse,
#[error("key id {0:?} exceeds the {MAX_KID_NAME}-character limit")]
KeyIdTooLong(String),
#[error(
"the ring is full and every live key is the signing key; nothing can be retired to make room"
)]
RingFull,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyState {
Signing,
VerifyingOnly,
Revoked,
}
#[derive(Clone)]
pub struct RingKey {
kid: Kid,
secret: Vec<u8>,
state: KeyState,
}
#[derive(Clone)]
pub struct KeyRing {
keys: Vec<RingKey>,
}
impl KeyRing {
pub const CAPACITY: usize = 4;
#[must_use]
pub fn new() -> Self {
Self { keys: Vec::new() }
}
#[must_use]
pub fn len(&self) -> usize {
self.keys.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
#[must_use]
pub fn states(&self) -> Vec<(Kid, KeyState)> {
self.keys
.iter()
.map(|key| (key.kid.clone(), key.state))
.collect()
}
#[must_use]
pub fn signing_key(&self) -> Option<&RingKey> {
self.keys.iter().find(|key| key.state == KeyState::Signing)
}
#[must_use]
pub fn state_of(&self, kid: &Kid) -> Option<KeyState> {
self.keys
.iter()
.find(|key| &key.kid == kid)
.map(|key| key.state)
}
pub fn rotate_signing(
&mut self,
kid: Kid,
secret: impl Into<Vec<u8>>,
) -> Result<Option<Kid>, SignerError> {
self.install(kid, secret.into(), KeyState::Signing)
}
pub fn add_verifying_only(
&mut self,
kid: Kid,
secret: impl Into<Vec<u8>>,
) -> Result<Option<Kid>, SignerError> {
self.install(kid, secret.into(), KeyState::VerifyingOnly)
}
pub fn revoke(&mut self, kid: &Kid) -> bool {
if let Some(key) = self.keys.iter_mut().find(|key| &key.kid == kid) {
key.state = KeyState::Revoked;
return true;
}
if self.keys.len() >= Self::CAPACITY && !self.make_room_for_tombstone() {
return false;
}
self.keys.push(RingKey {
kid: kid.clone(),
secret: Vec::new(),
state: KeyState::Revoked,
});
false
}
fn make_room_for_tombstone(&mut self) -> bool {
match self
.keys
.iter()
.position(|key| key.state == KeyState::Revoked && key.secret.is_empty())
{
Some(index) => {
self.keys.remove(index);
true
}
None => false,
}
}
fn install(
&mut self,
kid: Kid,
secret: Vec<u8>,
state: KeyState,
) -> Result<Option<Kid>, SignerError> {
if secret.len() < MIN_SECRET_BYTES {
return Err(SignerError::SecretTooShort);
}
if kid_name_len(&kid) > MAX_KID_NAME {
return Err(SignerError::KeyIdTooLong(kid_display(&kid)));
}
if let Some(existing) = self.state_of(&kid) {
return Err(match existing {
KeyState::Revoked => SignerError::KeyRevoked(kid_display(&kid)),
_ => SignerError::KeyIdTaken(kid_display(&kid)),
});
}
if self.keys.iter().any(|key| {
key.state == KeyState::Revoked && !key.secret.is_empty() && key.secret == secret
}) {
return Err(SignerError::RevokedSecretReuse);
}
if state == KeyState::Signing
&& let Some(previous) = self
.keys
.iter_mut()
.find(|key| key.state == KeyState::Signing)
{
previous.state = KeyState::VerifyingOnly;
}
let mut retired = None;
while self.keys.len() >= Self::CAPACITY {
match self.evict_oldest_non_signing() {
Some(evicted) => retired = Some(evicted),
None => return Err(SignerError::RingFull),
}
}
self.keys.push(RingKey { kid, secret, state });
Ok(retired)
}
fn evict_oldest_non_signing(&mut self) -> Option<Kid> {
let index = self
.keys
.iter()
.position(|key| key.state == KeyState::Revoked)
.or_else(|| {
self.keys
.iter()
.position(|key| key.state == KeyState::VerifyingOnly)
})?;
let evicted = self.keys.remove(index);
if self.keys.len() < Self::CAPACITY - 1 {
self.keys.insert(
index,
RingKey {
kid: evicted.kid.clone(),
secret: Vec::new(),
state: KeyState::Revoked,
},
);
}
Some(evicted.kid)
}
}
impl Default for KeyRing {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for KeyRing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyRing")
.field("keys", &self.states())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct TokenPolicy {
rules: Vec<(String, i64)>,
default_secs: Option<i64>,
}
pub const CONFIRM_TOKEN_MAX_TTL_SECS: i64 = 7 * 86_400;
pub const STATUS_TOKEN_MAX_TTL_SECS: i64 = 90 * 86_400;
pub const DEFAULT_TOKEN_MAX_TTL_SECS: i64 = 30 * 86_400;
pub const UNSUBSCRIBE_ACTION: &str = "unsubscribe";
impl Default for TokenPolicy {
fn default() -> Self {
Self {
rules: vec![
("confirm".to_owned(), CONFIRM_TOKEN_MAX_TTL_SECS),
("status".to_owned(), STATUS_TOKEN_MAX_TTL_SECS),
],
default_secs: Some(DEFAULT_TOKEN_MAX_TTL_SECS),
}
}
}
impl TokenPolicy {
#[must_use]
pub fn with_max(mut self, action: &str, secs: Option<i64>) -> Self {
let ceiling = secs.unwrap_or(0);
self.rules.retain(|(name, _)| name != action);
self.rules.push((action.to_owned(), ceiling));
self
}
#[must_use]
pub fn max_ttl_secs(&self, purpose: &str) -> Option<i64> {
let action = purpose.split_once('.').map_or(purpose, |(_, rest)| rest);
if action == UNSUBSCRIBE_ACTION {
return None;
}
match self.rules.iter().find(|(name, _)| name == action) {
Some((_, 0)) => None,
Some((_, secs)) => Some(*secs),
None => self.default_secs,
}
}
#[must_use]
pub fn effective_exp(&self, purpose: &str, requested: Option<u64>, now: u64) -> Option<u64> {
self.max_ttl_secs(purpose).map(|ceiling| {
let limit = now.saturating_add(u64::try_from(ceiling.max(0)).unwrap_or(0));
requested.map_or(limit, |exp| exp.min(limit))
})
}
}
#[derive(Serialize, Deserialize)]
struct PayloadJson {
purpose: String,
subject: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
exp: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
iss: Option<String>,
kid: String,
}
#[derive(Clone)]
pub struct HmacSigner {
inner: Arc<SignerInner>,
}
#[derive(Clone)]
struct SignerInner {
ring: KeyRing,
clock: Arc<dyn Clock>,
binding: Option<String>,
policy: TokenPolicy,
}
impl std::fmt::Debug for HmacSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HmacSigner")
.field("ring", &self.inner.ring)
.field("bound", &self.inner.binding.is_some())
.finish()
}
}
impl HmacSigner {
pub fn new(
current_secret: impl Into<String>,
previous_secret: Option<String>,
) -> Result<Self, SignerError> {
let mut ring = KeyRing::new();
ring.rotate_signing(Kid::Cur, current_secret.into().into_bytes())?;
if let Some(previous) = previous_secret {
ring.add_verifying_only(Kid::Prev, previous.into_bytes())?;
}
Ok(Self::from_ring(ring))
}
#[must_use]
pub fn from_ring(ring: KeyRing) -> Self {
Self {
inner: Arc::new(SignerInner {
ring,
clock: Arc::new(crate::SystemClock),
binding: None,
policy: TokenPolicy::default(),
}),
}
}
#[must_use]
pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
Arc::make_mut(&mut self.inner).clock = clock;
self
}
#[must_use]
pub fn with_binding(mut self, venture: Option<String>, env: Option<String>) -> Self {
let binding = match (
venture.filter(|value| !value.is_empty()),
env.filter(|value| !value.is_empty()),
) {
(None, None) => None,
(venture, env) => Some(format!(
"{}|{}",
venture.as_deref().unwrap_or("-"),
env.as_deref().unwrap_or("-")
)),
};
Arc::make_mut(&mut self.inner).binding = binding;
self
}
#[must_use]
pub fn with_policy(mut self, policy: TokenPolicy) -> Self {
Arc::make_mut(&mut self.inner).policy = policy;
self
}
#[must_use]
pub fn ring_states(&self) -> Vec<(Kid, KeyState)> {
self.inner.ring.states()
}
fn now_secs(&self) -> u64 {
u64::try_from(self.inner.clock.now().unix_timestamp().max(0)).unwrap_or(0)
}
fn mac(key: &[u8], encoded_payload: &str) -> [u8; 32] {
let mut mac =
<HmacSha256 as KeyInit>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(encoded_payload.as_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&mac.finalize().into_bytes());
out
}
fn mac_matches_any_live_key(&self, mac: [u8; 32], encoded_payload: &str) -> bool {
let mut matched = false;
for key in &self.inner.ring.keys {
if key.state == KeyState::Revoked || key.secret.is_empty() {
continue;
}
matched |= bool::from(mac.ct_eq(&Self::mac(&key.secret, encoded_payload)));
}
matched
}
}
impl Signer for HmacSigner {
fn sign(&self, payload: &Payload) -> String {
let Some(signing) = self.inner.ring.signing_key() else {
tracing::error!(purpose = %payload.purpose, "signer ring has no signing key; emitting an invalid token");
return String::new();
};
let exp = self
.inner
.policy
.effective_exp(&payload.purpose, payload.exp, self.now_secs());
let json = PayloadJson {
purpose: payload.purpose.clone(),
subject: payload.subject.clone(),
exp,
iss: self.inner.binding.clone(),
kid: kid_wire_name(&signing.kid),
};
let Ok(serialized) = serde_json::to_string(&json) else {
tracing::error!(purpose = %payload.purpose, "signer payload failed to serialize; emitting an invalid token");
return String::new();
};
let encoded = URL_SAFE_NO_PAD.encode(serialized);
let mac = Self::mac(&signing.secret, &encoded);
format!("{encoded}.{}", URL_SAFE_NO_PAD.encode(mac))
}
fn verify(&self, token: &str, expected_purpose: &str) -> Option<Payload> {
let (encoded_payload, encoded_mac) = token.split_once('.')?;
if encoded_payload.is_empty() || encoded_mac.is_empty() {
return None;
}
if token.matches('.').count() != 1 {
return None;
}
let mac: [u8; 32] = URL_SAFE_NO_PAD.decode(encoded_mac).ok()?.try_into().ok()?;
let json = URL_SAFE_NO_PAD
.decode(encoded_payload)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())?;
let payload: PayloadJson = serde_json::from_str(&json).ok()?;
let kid = parse_kid(&payload.kid)?;
if !self.mac_matches_any_live_key(mac, encoded_payload) {
return None;
}
if payload.purpose != expected_purpose {
return None;
}
if payload.iss.is_some() && payload.iss.as_deref() != self.inner.binding.as_deref() {
return None;
}
if let Some(exp) = payload.exp
&& self.now_secs() >= exp
{
return None;
}
Some(Payload {
purpose: payload.purpose,
subject: payload.subject,
exp: payload.exp,
kid,
})
}
}
fn kid_wire_name(kid: &Kid) -> String {
match kid {
Kid::Cur => "cur".to_owned(),
Kid::Prev => "prev".to_owned(),
Kid::Named(name) => name.clone(),
}
}
fn kid_display(kid: &Kid) -> String {
kid_wire_name(kid)
}
fn kid_name_len(kid: &Kid) -> usize {
match kid {
Kid::Cur | Kid::Prev => 3,
Kid::Named(name) => name.len(),
}
}
fn parse_kid(name: &str) -> Option<Kid> {
match name {
"cur" => Some(Kid::Cur),
"prev" => Some(Kid::Prev),
other if !other.is_empty() && other.len() <= MAX_KID_NAME => {
Some(Kid::Named(other.to_owned()))
}
_ => None,
}
}