use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum KeyFormat {
PasetoV4Local,
PasetoV4Public,
JwtRs256,
JwtEs256,
}
impl fmt::Display for KeyFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PasetoV4Local => write!(f, "paseto_v4_local"),
Self::PasetoV4Public => write!(f, "paseto_v4_public"),
Self::JwtRs256 => write!(f, "jwt_rs256"),
Self::JwtEs256 => write!(f, "jwt_es256"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseKeyFormatError {
pub input: String,
}
impl fmt::Display for ParseKeyFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid key format '{}': expected one of \
paseto_v4_local, paseto_v4_public, jwt_rs256, jwt_es256",
self.input
)
}
}
impl std::error::Error for ParseKeyFormatError {}
impl FromStr for KeyFormat {
type Err = ParseKeyFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"paseto_v4_local" => Ok(Self::PasetoV4Local),
"paseto_v4_public" => Ok(Self::PasetoV4Public),
"jwt_rs256" => Ok(Self::JwtRs256),
"jwt_es256" => Ok(Self::JwtEs256),
other => Err(ParseKeyFormatError {
input: other.to_string(),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum KeyStatus {
Active,
Draining,
Retired,
}
impl fmt::Display for KeyStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Active => write!(f, "active"),
Self::Draining => write!(f, "draining"),
Self::Retired => write!(f, "retired"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseKeyStatusError {
pub input: String,
}
impl fmt::Display for ParseKeyStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid key status '{}': expected one of active, draining, retired",
self.input
)
}
}
impl std::error::Error for ParseKeyStatusError {}
impl FromStr for KeyStatus {
type Err = ParseKeyStatusError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"active" => Ok(Self::Active),
"draining" => Ok(Self::Draining),
"retired" => Ok(Self::Retired),
other => Err(ParseKeyStatusError {
input: other.to_string(),
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SigningKeyMetadata {
pub kid: String,
pub format: KeyFormat,
pub key_material: String,
pub status: KeyStatus,
pub created_at: DateTime<Utc>,
pub activated_at: Option<DateTime<Utc>>,
pub draining_since: Option<DateTime<Utc>>,
pub retired_at: Option<DateTime<Utc>>,
pub drain_expires_at: Option<DateTime<Utc>>,
pub service_name: String,
pub key_hash: String,
}
impl PartialEq for SigningKeyMetadata {
fn eq(&self, other: &Self) -> bool {
self.kid == other.kid
}
}
impl Eq for SigningKeyMetadata {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_format_display() {
assert_eq!(KeyFormat::PasetoV4Local.to_string(), "paseto_v4_local");
assert_eq!(KeyFormat::PasetoV4Public.to_string(), "paseto_v4_public");
assert_eq!(KeyFormat::JwtRs256.to_string(), "jwt_rs256");
assert_eq!(KeyFormat::JwtEs256.to_string(), "jwt_es256");
}
#[test]
fn test_key_format_from_str_valid() {
assert_eq!(
KeyFormat::from_str("paseto_v4_local").unwrap(),
KeyFormat::PasetoV4Local
);
assert_eq!(
KeyFormat::from_str("paseto_v4_public").unwrap(),
KeyFormat::PasetoV4Public
);
assert_eq!(
KeyFormat::from_str("jwt_rs256").unwrap(),
KeyFormat::JwtRs256
);
assert_eq!(
KeyFormat::from_str("jwt_es256").unwrap(),
KeyFormat::JwtEs256
);
}
#[test]
fn test_key_format_from_str_invalid() {
let err = KeyFormat::from_str("unknown").unwrap_err();
assert_eq!(err.input, "unknown");
assert!(err.to_string().contains("invalid key format"));
assert!(err.to_string().contains("unknown"));
}
#[test]
fn test_key_format_roundtrip() {
let formats = [
KeyFormat::PasetoV4Local,
KeyFormat::PasetoV4Public,
KeyFormat::JwtRs256,
KeyFormat::JwtEs256,
];
for format in formats {
let s = format.to_string();
let parsed = KeyFormat::from_str(&s).unwrap();
assert_eq!(format, parsed);
}
}
#[test]
fn test_key_format_serde_roundtrip() {
let format = KeyFormat::PasetoV4Local;
let json = serde_json::to_string(&format).expect("serialize");
let deserialized: KeyFormat = serde_json::from_str(&json).expect("deserialize");
assert_eq!(format, deserialized);
}
#[test]
fn test_key_status_display() {
assert_eq!(KeyStatus::Active.to_string(), "active");
assert_eq!(KeyStatus::Draining.to_string(), "draining");
assert_eq!(KeyStatus::Retired.to_string(), "retired");
}
#[test]
fn test_key_status_from_str_valid() {
assert_eq!(KeyStatus::from_str("active").unwrap(), KeyStatus::Active);
assert_eq!(
KeyStatus::from_str("draining").unwrap(),
KeyStatus::Draining
);
assert_eq!(KeyStatus::from_str("retired").unwrap(), KeyStatus::Retired);
}
#[test]
fn test_key_status_from_str_invalid() {
let err = KeyStatus::from_str("pending").unwrap_err();
assert_eq!(err.input, "pending");
assert!(err.to_string().contains("invalid key status"));
}
#[test]
fn test_key_status_roundtrip() {
let statuses = [KeyStatus::Active, KeyStatus::Draining, KeyStatus::Retired];
for status in statuses {
let s = status.to_string();
let parsed = KeyStatus::from_str(&s).unwrap();
assert_eq!(status, parsed);
}
}
#[test]
fn test_key_status_serde_roundtrip() {
let status = KeyStatus::Draining;
let json = serde_json::to_string(&status).expect("serialize");
let deserialized: KeyStatus = serde_json::from_str(&json).expect("deserialize");
assert_eq!(status, deserialized);
}
fn sample_key() -> SigningKeyMetadata {
SigningKeyMetadata {
kid: "sigkey_test123".to_string(),
format: KeyFormat::PasetoV4Local,
key_material: "dGVzdGtleW1hdGVyaWFs".to_string(),
status: KeyStatus::Active,
created_at: Utc::now(),
activated_at: Some(Utc::now()),
draining_since: None,
retired_at: None,
drain_expires_at: None,
service_name: "test-service".to_string(),
key_hash: "abc123hash".to_string(),
}
}
#[test]
fn test_signing_key_metadata_equality_by_kid() {
let key1 = sample_key();
let mut key2 = sample_key();
key2.status = KeyStatus::Draining; assert_eq!(key1, key2);
}
#[test]
fn test_signing_key_metadata_inequality_by_kid() {
let key1 = sample_key();
let mut key2 = sample_key();
key2.kid = "sigkey_other456".to_string();
assert_ne!(key1, key2);
}
#[test]
fn test_signing_key_metadata_serde_roundtrip() {
let key = sample_key();
let json = serde_json::to_string(&key).expect("serialize");
let deserialized: SigningKeyMetadata = serde_json::from_str(&json).expect("deserialize");
assert_eq!(key.kid, deserialized.kid);
assert_eq!(key.format, deserialized.format);
assert_eq!(key.status, deserialized.status);
assert_eq!(key.key_material, deserialized.key_material);
assert_eq!(key.service_name, deserialized.service_name);
assert_eq!(key.key_hash, deserialized.key_hash);
}
#[test]
fn test_parse_key_format_error_is_std_error() {
let err = ParseKeyFormatError {
input: "bad".to_string(),
};
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_parse_key_status_error_is_std_error() {
let err = ParseKeyStatusError {
input: "bad".to_string(),
};
let _: &dyn std::error::Error = &err;
}
}