use std::{
fmt::Display,
str::FromStr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header};
use scion_sdk_token_validator::validator::{Token, insecure_const_ed25519_key_pair_pem};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
const DEFAULT_SNAP_TOKEN_VALIDITY: u64 = 86400;
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, Clone)]
pub struct Pssid(pub Uuid);
impl Default for Pssid {
fn default() -> Self {
Self::new()
}
}
impl Pssid {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl FromStr for Pssid {
type Err = std::io::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match Uuid::parse_str(value) {
Ok(uuid) => Ok(Pssid(uuid)),
Err(_) => {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid PSSID",
))
}
}
}
}
impl From<Pssid> for String {
fn from(pssid: Pssid) -> Self {
pssid.0.to_string()
}
}
impl Display for Pssid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SnapTokenClaims {
pub pssid: Pssid,
pub exp: u64,
pub jti: String,
}
impl Token for SnapTokenClaims {
fn id(&self) -> String {
self.pssid.to_string()
}
fn exp_time(&self) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(self.exp)
}
fn required_claims() -> Vec<&'static str> {
vec!["exp", "pssid"]
}
}
#[derive(Clone)]
pub struct MockSnapTokenCreator {
uuid: Uuid,
token_validity: u64,
}
impl MockSnapTokenCreator {
pub fn new_with_expiry(token_validity: u64) -> Self {
Self {
uuid: Uuid::new_v4(),
token_validity,
}
}
pub fn new_seeded(seed: String) -> Self {
Self {
uuid: Uuid::new_v5(&SSID_NAMESPACE, seed.as_bytes()),
token_validity: DEFAULT_SNAP_TOKEN_VALIDITY,
}
}
pub fn token(&self) -> String {
insecure_snap_token(self.uuid, self.token_validity)
}
}
pub fn dummy_snap_token() -> String {
insecure_snap_token(Uuid::new_v4(), DEFAULT_SNAP_TOKEN_VALIDITY)
}
pub fn dummy_snap_token_with_validity(valid_seconds: u64) -> String {
insecure_snap_token(Uuid::new_v4(), valid_seconds + 1)
}
const SSID_NAMESPACE: Uuid = Uuid::from_bytes([
126, 135, 110, 147, 40, 228, 76, 8, 164, 39, 42, 4, 3, 103, 82, 211,
]);
pub fn seeded_dummy_snap_token(seed: String) -> String {
insecure_snap_token(
Uuid::new_v5(&SSID_NAMESPACE, seed.as_bytes()),
DEFAULT_SNAP_TOKEN_VALIDITY,
)
}
fn insecure_snap_token(uuid: Uuid, expiry: u64) -> String {
let (encoding_key, _) = insecure_const_snap_token_key_pair();
let claims = SnapTokenClaims {
pssid: Pssid(uuid),
exp: (SystemTime::now() + Duration::from_secs(expiry))
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
jti: uuid.to_string(),
};
jsonwebtoken::encode(&Header::new(Algorithm::EdDSA), &claims, &encoding_key).unwrap()
}
pub fn insecure_const_snap_token_key_pair() -> (EncodingKey, DecodingKey) {
let (private_pem, public_pem) = insecure_const_ed25519_key_pair_pem();
let encoding_key = EncodingKey::from_ed_pem(pem::encode(&private_pem).as_bytes()).unwrap();
let decoding_key = DecodingKey::from_ed_pem(pem::encode(&public_pem).as_bytes()).unwrap();
(encoding_key, decoding_key)
}
#[cfg(test)]
mod tests {
use jsonwebtoken::{Algorithm, Header, encode};
use scion_sdk_token_validator::validator::{TokenValidator, TokenValidatorError, Validator};
use test_log::test;
use super::*;
use crate::AnyClaims;
#[test]
fn validate_valid_token_succeeds() {
let (_, decoding_key) = insecure_const_snap_token_key_pair();
let validator: Validator<AnyClaims> = Validator::new(decoding_key, None);
let token = dummy_snap_token();
let result = validator.validate(SystemTime::now(), &token);
assert!(result.is_ok());
}
#[test]
fn validate_invalid_token_returns_error() {
let (_encoding_key, decoding_key) = insecure_const_snap_token_key_pair();
let validator: Validator<AnyClaims> = Validator::new(decoding_key, None);
let token = "invalid-jwt-token";
let result = validator.validate(SystemTime::now(), token);
assert!(matches!(result, Err(TokenValidatorError::JwtError(_))));
}
#[test]
fn validate_expired_token_returns_error() {
let (encoding_key, decoding_key) = insecure_const_snap_token_key_pair();
let validator: Validator<AnyClaims> = Validator::new(decoding_key, None);
let now = SystemTime::now();
let expiry = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
let claims = SnapTokenClaims {
pssid: Pssid(Uuid::new_v4()),
exp: expiry,
jti: "test".to_string(),
};
let token = encode(&Header::new(Algorithm::EdDSA), &claims, &encoding_key).unwrap();
let result = validator.validate(now, &token);
assert!(matches!(result, Err(TokenValidatorError::TokenExpired(_))));
}
}