use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::OidcError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum JwsAlg {
#[serde(rename = "RS256")]
Rs256,
#[serde(rename = "RS384")]
Rs384,
#[serde(rename = "RS512")]
Rs512,
#[serde(rename = "ES256")]
Es256,
#[serde(rename = "ES384")]
Es384,
#[serde(rename = "ES512")]
Es512,
#[serde(rename = "PS256")]
Ps256,
#[serde(rename = "PS384")]
Ps384,
#[serde(rename = "PS512")]
Ps512,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unsupported or unsafe JWS algorithm: {0:?}")]
pub struct UnsupportedJwsAlg(pub String);
impl JwsAlg {
pub fn parse(name: &str) -> Result<JwsAlg, UnsupportedJwsAlg> {
match name {
"RS256" => Ok(JwsAlg::Rs256),
"RS384" => Ok(JwsAlg::Rs384),
"RS512" => Ok(JwsAlg::Rs512),
"ES256" => Ok(JwsAlg::Es256),
"ES384" => Ok(JwsAlg::Es384),
"ES512" => Ok(JwsAlg::Es512),
"PS256" => Ok(JwsAlg::Ps256),
"PS384" => Ok(JwsAlg::Ps384),
"PS512" => Ok(JwsAlg::Ps512),
other => Err(UnsupportedJwsAlg(other.to_string())),
}
}
pub fn as_str(&self) -> &'static str {
match self {
JwsAlg::Rs256 => "RS256",
JwsAlg::Rs384 => "RS384",
JwsAlg::Rs512 => "RS512",
JwsAlg::Es256 => "ES256",
JwsAlg::Es384 => "ES384",
JwsAlg::Es512 => "ES512",
JwsAlg::Ps256 => "PS256",
JwsAlg::Ps384 => "PS384",
JwsAlg::Ps512 => "PS512",
}
}
}
impl std::fmt::Display for JwsAlg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct OidcValidationConfig {
pub issuer: String,
pub audience: String,
pub allowed_algorithms: Vec<JwsAlg>,
pub max_clock_skew_secs: i64,
pub jwks_cache_ttl_secs: u64,
}
impl OidcValidationConfig {
pub fn builder() -> OidcValidationConfigBuilder {
OidcValidationConfigBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct OidcValidationConfigBuilder {
issuer: Option<String>,
audience: Option<String>,
allowed_algorithms: Option<Vec<JwsAlg>>,
max_clock_skew_secs: Option<i64>,
jwks_cache_ttl_secs: Option<u64>,
}
impl OidcValidationConfigBuilder {
pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
pub fn allowed_algorithms(mut self, algorithms: Vec<JwsAlg>) -> Self {
self.allowed_algorithms = Some(algorithms);
self
}
pub fn max_clock_skew_secs(mut self, secs: i64) -> Self {
self.max_clock_skew_secs = Some(secs);
self
}
pub fn jwks_cache_ttl_secs(mut self, secs: u64) -> Self {
self.jwks_cache_ttl_secs = Some(secs);
self
}
pub fn build(self) -> Result<OidcValidationConfig, String> {
Ok(OidcValidationConfig {
issuer: self
.issuer
.ok_or_else(|| "issuer is required".to_string())?,
audience: self
.audience
.ok_or_else(|| "audience is required".to_string())?,
allowed_algorithms: self
.allowed_algorithms
.unwrap_or_else(|| vec![JwsAlg::Rs256, JwsAlg::Es256]),
max_clock_skew_secs: self.max_clock_skew_secs.unwrap_or(60),
jwks_cache_ttl_secs: self.jwks_cache_ttl_secs.unwrap_or(3600),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampConfig {
pub tsa_uri: Option<String>,
pub timeout_secs: u64,
pub fallback_on_error: bool,
}
impl Default for TimestampConfig {
fn default() -> Self {
Self {
tsa_uri: Some("http://timestamp.sigstore.dev/api/v1/timestamp".to_string()),
timeout_secs: 10,
fallback_on_error: true,
}
}
}
#[async_trait::async_trait]
pub trait JwtValidator: Send + Sync {
async fn validate(
&self,
token: &str,
config: &OidcValidationConfig,
now: DateTime<Utc>,
) -> Result<serde_json::Value, OidcError>;
}
#[async_trait::async_trait]
pub trait JwksClient: Send + Sync {
async fn fetch_jwks(&self, issuer_url: &str) -> Result<serde_json::Value, OidcError>;
}
#[async_trait::async_trait]
pub trait TimestampClient: Send + Sync {
async fn timestamp(
&self,
data: &[u8],
config: &TimestampConfig,
) -> Result<Option<Vec<u8>>, OidcError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unsigned_symmetric_or_unknown_algorithm_is_not_an_accepted_jws_algorithm() {
assert_eq!(JwsAlg::parse("RS256").unwrap(), JwsAlg::Rs256);
assert_eq!(JwsAlg::parse("ES256").unwrap(), JwsAlg::Es256);
assert!(JwsAlg::parse("none").is_err());
assert!(JwsAlg::parse("None").is_err());
assert!(JwsAlg::parse("HS256").is_err());
assert!(JwsAlg::parse("rs256").is_err());
assert!(JwsAlg::parse("").is_err());
}
#[test]
fn test_oidc_validation_config_builder() {
let config = OidcValidationConfig::builder()
.issuer("https://token.actions.githubusercontent.com")
.audience("sigstore")
.allowed_algorithms(vec![JwsAlg::Rs256])
.max_clock_skew_secs(120)
.jwks_cache_ttl_secs(7200)
.build();
assert!(config.is_ok());
let cfg = config.unwrap();
assert_eq!(cfg.issuer, "https://token.actions.githubusercontent.com");
assert_eq!(cfg.audience, "sigstore");
assert_eq!(cfg.allowed_algorithms, vec![JwsAlg::Rs256]);
assert_eq!(cfg.max_clock_skew_secs, 120);
assert_eq!(cfg.jwks_cache_ttl_secs, 7200);
}
#[test]
fn test_oidc_validation_config_defaults() {
let config = OidcValidationConfig::builder()
.issuer("https://example.com")
.audience("test")
.build();
assert!(config.is_ok());
let cfg = config.unwrap();
assert_eq!(cfg.max_clock_skew_secs, 60);
assert_eq!(cfg.jwks_cache_ttl_secs, 3600);
assert_eq!(cfg.allowed_algorithms, vec![JwsAlg::Rs256, JwsAlg::Es256]);
}
#[test]
fn test_oidc_validation_config_missing_issuer() {
let config = OidcValidationConfig::builder().audience("test").build();
assert!(config.is_err());
}
#[test]
fn test_oidc_validation_config_missing_audience() {
let config = OidcValidationConfig::builder()
.issuer("https://example.com")
.build();
assert!(config.is_err());
}
#[test]
fn test_timestamp_config_default() {
let config = TimestampConfig::default();
assert!(config.tsa_uri.is_some());
assert_eq!(config.timeout_secs, 10);
assert!(config.fallback_on_error);
}
#[test]
fn test_timestamp_config_custom() {
let config = TimestampConfig {
tsa_uri: Some("http://custom-tsa.example.com".to_string()),
timeout_secs: 20,
fallback_on_error: false,
};
assert_eq!(
config.tsa_uri,
Some("http://custom-tsa.example.com".to_string())
);
assert_eq!(config.timeout_secs, 20);
assert!(!config.fallback_on_error);
}
}