use std::fs::File;
use std::io::Read;
use std::path::Path;
use thiserror::Error;
pub use hessra_token::{
add_service_node_attestation,
decode_token,
encode_token,
add_prefix_restriction,
add_prefix_restriction_to_token,
verify_biscuit_local,
verify_service_chain_biscuit_local,
AuthorizationVerifier,
Biscuit,
KeyPair,
PublicKey,
ServiceNode,
TokenError,
};
pub use hessra_token_identity::{
add_identity_attenuation_to_token, create_identity_token, create_short_lived_identity_token,
verify_identity_token,
};
pub use hessra_config::{ConfigError, HessraConfig, Protocol};
pub use hessra_api::{
parse_server_address, ApiError, HessraClient, HessraClientBuilder, IdentityTokenRequest,
IdentityTokenResponse, MintIdentityTokenRequest, MintIdentityTokenResponse, PublicKeyResponse,
RefreshIdentityTokenRequest, SignTokenRequest, SignTokenResponse, SignoffInfo,
StubTokenRequest, StubTokenResponse, TokenRequest, TokenResponse,
VerifyServiceChainTokenRequest, VerifyTokenRequest, VerifyTokenResponse,
};
#[derive(Error, Debug)]
pub enum SdkError {
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
#[error("API error: {0}")]
Api(#[from] ApiError),
#[error("Token error: {0}")]
Token(#[from] TokenError),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Generic(String),
}
#[derive(Clone, Debug, Default)]
pub struct ServiceChain {
nodes: Vec<ServiceNode>,
}
impl ServiceChain {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
pub fn with_nodes(nodes: Vec<ServiceNode>) -> Self {
Self { nodes }
}
pub fn builder() -> ServiceChainBuilder {
ServiceChainBuilder::new()
}
pub fn add_node(&mut self, node: ServiceNode) -> &mut Self {
self.nodes.push(node);
self
}
pub fn with_node(mut self, node: ServiceNode) -> Self {
self.nodes.push(node);
self
}
pub fn nodes(&self) -> &[ServiceNode] {
&self.nodes
}
fn to_internal(&self) -> Vec<hessra_token::ServiceNode> {
self.nodes.to_vec()
}
pub fn from_json(json: &str) -> Result<Self, SdkError> {
let nodes: Vec<ServiceNode> = serde_json::from_str(json)?;
Ok(Self::with_nodes(nodes))
}
pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, SdkError> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Self::from_json(&contents)
}
#[cfg(feature = "toml")]
pub fn from_toml(toml_str: &str) -> Result<Self, SdkError> {
use serde::Deserialize;
#[derive(Deserialize)]
struct TomlServiceChain {
nodes: Vec<ServiceNode>,
}
let chain: TomlServiceChain = toml::from_str(toml_str)
.map_err(|e| SdkError::Generic(format!("TOML parse error: {e}")))?;
Ok(Self::with_nodes(chain.nodes))
}
#[cfg(feature = "toml")]
pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, SdkError> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Self::from_toml(&contents)
}
}
#[derive(Debug, Default)]
pub struct ServiceChainBuilder {
nodes: Vec<ServiceNode>,
}
impl ServiceChainBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_node(mut self, node: ServiceNode) -> Self {
self.nodes.push(node);
self
}
pub fn build(self) -> ServiceChain {
ServiceChain::with_nodes(self.nodes)
}
}
pub struct Hessra {
client: HessraClient,
config: HessraConfig,
}
impl Hessra {
pub fn new(config: HessraConfig) -> Result<Self, SdkError> {
let client = HessraClientBuilder::new()
.from_config(&config)
.build()
.map_err(|e| SdkError::Generic(e.to_string()))?;
Ok(Self { client, config })
}
pub fn builder() -> HessraBuilder {
HessraBuilder::new()
}
pub async fn setup(&mut self) -> Result<(), SdkError> {
match self.get_public_key().await {
Ok(public_key) => {
self.config.public_key = Some(public_key);
Ok(())
}
Err(e) => Err(SdkError::Generic(e.to_string())),
}
}
pub async fn with_setup(&self) -> Result<Self, SdkError> {
match self.get_public_key().await {
Ok(public_key) => {
let config = self.config.to_builder().public_key(public_key).build()?;
Ok(Self::new(config)?)
}
Err(e) => Err(SdkError::Generic(e.to_string())),
}
}
pub async fn request_token(
&self,
resource: impl Into<String>,
operation: impl Into<String>,
domain: Option<String>,
) -> Result<TokenResponse, SdkError> {
self.client
.request_token(resource.into(), operation.into(), domain)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
fn apply_jit_attenuation(&self, identity_token: String) -> String {
if let Some(ref public_key_pem) = self.config.public_key {
if let Ok(public_key) = PublicKey::from_pem(public_key_pem.as_str()) {
if let Ok(attenuated_token) =
create_short_lived_identity_token(identity_token.clone(), public_key)
{
return attenuated_token;
}
}
}
identity_token
}
pub async fn request_token_with_identity(
&self,
resource: impl Into<String>,
operation: impl Into<String>,
identity_token: impl Into<String>,
domain: Option<String>,
) -> Result<TokenResponse, SdkError> {
let token = identity_token.into();
let attenuated_token = self.apply_jit_attenuation(token);
self.client
.request_token_with_identity(
resource.into(),
operation.into(),
attenuated_token,
domain,
)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn request_token_simple(
&self,
resource: impl Into<String>,
operation: impl Into<String>,
) -> Result<String, SdkError> {
let response = self.request_token(resource, operation, None).await?;
match response.token {
Some(token) => Ok(token),
None => Err(SdkError::Generic(format!(
"Failed to get token: {}",
response.response_msg
))),
}
}
pub async fn sign_token(
&self,
token: &str,
resource: &str,
operation: &str,
) -> Result<SignTokenResponse, SdkError> {
self.client
.sign_token(token, resource, operation)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
fn parse_authorization_service_url(url: &str) -> Result<(String, Option<u16>), SdkError> {
let url_str = if url.starts_with("http://") || url.starts_with("https://") {
url.to_string()
} else {
format!("https://{url}")
};
let parsed_url = url::Url::parse(&url_str).map_err(|e| {
SdkError::Generic(format!(
"Failed to parse authorization service URL '{url}': {e}"
))
})?;
let host = parsed_url
.host_str()
.ok_or_else(|| SdkError::Generic(format!("No host found in URL: {url}")))?;
let port = if parsed_url.port().is_some() {
parsed_url.port()
} else if url.contains(':') && !url.starts_with("http://") && !url.starts_with("https://") {
if let Some(host_port) = url.split('/').next() {
if let Some(port_str) = host_port.split(':').nth(1) {
port_str.parse::<u16>().ok()
} else {
None
}
} else {
None
}
} else {
parsed_url.port()
};
Ok((host.to_string(), port))
}
pub async fn collect_signoffs(
&self,
initial_token_response: TokenResponse,
resource: &str,
operation: &str,
) -> Result<String, SdkError> {
let pending_signoffs = match &initial_token_response.pending_signoffs {
Some(signoffs) if !signoffs.is_empty() => signoffs,
_ => {
return initial_token_response
.token
.ok_or_else(|| SdkError::Generic("No token in response".to_string()))
}
};
let mut current_token = initial_token_response.token.ok_or_else(|| {
SdkError::Generic("No initial token to collect signoffs for".to_string())
})?;
for signoff_info in pending_signoffs {
let (base_url, port) =
Self::parse_authorization_service_url(&signoff_info.authorization_service)?;
let mut client_builder = HessraClientBuilder::new()
.base_url(base_url)
.protocol(self.config.protocol.clone())
.server_ca(self.config.server_ca.clone());
if let (Some(cert), Some(key)) = (&self.config.mtls_cert, &self.config.mtls_key) {
client_builder = client_builder.mtls_cert(cert.clone()).mtls_key(key.clone());
}
if let Some(port) = port {
client_builder = client_builder.port(port);
}
let signoff_client = client_builder
.build()
.map_err(|e| SdkError::Generic(format!("Failed to create signoff client: {e}")))?;
let sign_response = signoff_client
.sign_token(¤t_token, resource, operation)
.await
.map_err(|e| {
SdkError::Generic(format!(
"Signoff failed for {}: {e}",
signoff_info.component
))
})?;
current_token = sign_response.signed_token.ok_or_else(|| {
SdkError::Generic(format!(
"No signed token returned from {}: {}",
signoff_info.component, sign_response.response_msg
))
})?;
}
Ok(current_token)
}
pub async fn request_token_with_signoffs(
&self,
resource: &str,
operation: &str,
) -> Result<String, SdkError> {
let initial_response = self.request_token(resource, operation, None).await?;
self.collect_signoffs(initial_response, resource, operation)
.await
}
pub async fn verify_token(
&self,
token: impl Into<String>,
subject: impl Into<String>,
resource: impl Into<String>,
operation: impl Into<String>,
) -> Result<(), SdkError> {
if self.config.public_key.is_some() {
self.verify_token_local(
token.into(),
subject.into(),
resource.into(),
operation.into(),
)
} else {
self.verify_token_remote(
token.into(),
subject.into(),
resource.into(),
operation.into(),
)
.await
.map(|_| ())
.map_err(|e| SdkError::Generic(e.to_string()))
}
}
pub async fn verify_token_remote(
&self,
token: impl Into<String>,
subject: impl Into<String>,
resource: impl Into<String>,
operation: impl Into<String>,
) -> Result<String, SdkError> {
self.client
.verify_token(
token.into(),
subject.into(),
resource.into(),
operation.into(),
)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub fn verify_token_local(
&self,
token: impl Into<String>,
subject: impl AsRef<str>,
resource: impl AsRef<str>,
operation: impl AsRef<str>,
) -> Result<(), SdkError> {
let public_key_str = match &self.config.public_key {
Some(key) => key,
None => return Err(SdkError::Generic("Public key not configured".to_string())),
};
let public_key = PublicKey::from_pem(public_key_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
let token_vec = decode_token(&token.into())?;
verify_biscuit_local(
token_vec,
public_key,
subject.as_ref().to_string(),
resource.as_ref().to_string(),
operation.as_ref().to_string(),
)
.map_err(SdkError::Token)
}
pub async fn verify_service_chain_token(
&self,
token: impl Into<String>,
subject: impl Into<String>,
resource: impl Into<String>,
operation: impl Into<String>,
service_chain: Option<&ServiceChain>,
component: Option<String>,
) -> Result<(), SdkError> {
match (&self.config.public_key, service_chain) {
(Some(_), Some(chain)) => self.verify_service_chain_token_local(
token.into(),
subject.into(),
resource.into(),
operation.into(),
chain,
component,
),
_ => self
.verify_service_chain_token_remote(
token.into(),
subject.into(),
resource.into(),
component,
)
.await
.map(|_| ())
.map_err(|e| SdkError::Generic(e.to_string())),
}
}
pub async fn verify_service_chain_token_remote(
&self,
token: impl Into<String>,
subject: impl Into<String>,
resource: impl Into<String>,
component: Option<String>,
) -> Result<String, SdkError> {
self.client
.verify_service_chain_token(token.into(), subject.into(), resource.into(), component)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub fn verify_service_chain_token_local(
&self,
token: String,
subject: impl AsRef<str>,
resource: impl AsRef<str>,
operation: impl AsRef<str>,
service_chain: &ServiceChain,
component: Option<String>,
) -> Result<(), SdkError> {
let public_key_str = match &self.config.public_key {
Some(key) => key,
None => return Err(SdkError::Generic("Public key not configured".to_string())),
};
let public_key = PublicKey::from_pem(public_key_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
let token_vec = decode_token(&token)?;
verify_service_chain_biscuit_local(
token_vec,
public_key,
subject.as_ref().to_string(),
resource.as_ref().to_string(),
operation.as_ref().to_string(),
service_chain.to_internal(),
component,
)
.map_err(SdkError::Token)
}
pub fn attest_service_chain_token(
&self,
token: String,
service: impl Into<String>,
) -> Result<String, SdkError> {
let keypair_str = match &self.config.personal_keypair {
Some(keypair) => keypair,
None => {
return Err(SdkError::Generic(
"Personal keypair not configured".to_string(),
))
}
};
let public_key_str = match &self.config.public_key {
Some(key) => key,
None => return Err(SdkError::Generic("Public key not configured".to_string())),
};
let keypair = KeyPair::from_private_key_pem(keypair_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
let public_key = PublicKey::from_pem(public_key_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
let token_vec = decode_token(&token)?;
let service_str = service.into();
let token_vec = add_service_node_attestation(token_vec, public_key, &service_str, &keypair)
.map_err(SdkError::Token)?;
Ok(encode_token(&token_vec))
}
pub async fn get_public_key(&self) -> Result<String, SdkError> {
self.client
.get_public_key()
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn request_identity_token(
&self,
identifier: Option<String>,
) -> Result<IdentityTokenResponse, SdkError> {
self.client
.request_identity_token(identifier)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn refresh_identity_token(
&self,
current_token: impl Into<String>,
identifier: Option<String>,
) -> Result<IdentityTokenResponse, SdkError> {
self.client
.refresh_identity_token(current_token.into(), identifier)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn mint_domain_restricted_identity_token(
&self,
subject: impl Into<String>,
duration: Option<u64>,
) -> Result<MintIdentityTokenResponse, SdkError> {
self.client
.mint_domain_restricted_identity_token(subject.into(), duration)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn request_stub_token(
&self,
target_identity: impl Into<String>,
resource: impl Into<String>,
operation: impl Into<String>,
prefix_attenuator_key: impl Into<String>,
duration: Option<u64>,
) -> Result<StubTokenResponse, SdkError> {
self.client
.request_stub_token(
target_identity.into(),
resource.into(),
operation.into(),
prefix_attenuator_key.into(),
duration,
)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn request_stub_token_with_identity(
&self,
target_identity: impl Into<String>,
resource: impl Into<String>,
operation: impl Into<String>,
prefix_attenuator_key: impl Into<String>,
identity_token: impl Into<String>,
duration: Option<u64>,
) -> Result<StubTokenResponse, SdkError> {
let token = identity_token.into();
let attenuated_token = self.apply_jit_attenuation(token);
self.client
.request_stub_token_with_identity(
target_identity.into(),
resource.into(),
operation.into(),
prefix_attenuator_key.into(),
attenuated_token,
duration,
)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub fn verify_identity_token_local(
&self,
token: impl Into<String>,
identity: impl Into<String>,
) -> Result<(), SdkError> {
let public_key_str = match &self.config.public_key {
Some(key) => key,
None => return Err(SdkError::Generic("Public key not configured".to_string())),
};
let public_key = PublicKey::from_pem(public_key_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
verify_identity_token(token.into(), public_key, identity.into())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))
}
pub fn attenuate_identity_token(
&self,
token: impl Into<String>,
delegated_identity: impl Into<String>,
duration: i64,
) -> Result<String, SdkError> {
let public_key_str = match &self.config.public_key {
Some(key) => key,
None => return Err(SdkError::Generic("Public key not configured".to_string())),
};
let public_key = PublicKey::from_pem(public_key_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
let time_config = hessra_token_core::TokenTimeConfig {
start_time: None,
duration,
};
add_identity_attenuation_to_token(
token.into(),
delegated_identity.into(),
public_key,
time_config,
)
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))
}
pub fn create_identity_token_local(
&self,
subject: impl Into<String>,
duration: i64,
) -> Result<String, SdkError> {
let keypair_str = match &self.config.personal_keypair {
Some(keypair) => keypair,
None => {
return Err(SdkError::Generic(
"Personal keypair not configured".to_string(),
))
}
};
let keypair = KeyPair::from_private_key_pem(keypair_str.as_str())
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
let time_config = hessra_token_core::TokenTimeConfig {
start_time: None,
duration,
};
create_identity_token(subject.into(), keypair, time_config)
.map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))
}
pub fn client(&self) -> &HessraClient {
&self.client
}
pub fn config(&self) -> &HessraConfig {
&self.config
}
}
#[derive(Default)]
pub struct HessraBuilder {
config_builder: hessra_config::HessraConfigBuilder,
}
impl HessraBuilder {
pub fn new() -> Self {
Self {
config_builder: HessraConfig::builder(),
}
}
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
let base_url_str = base_url.into();
let (host, embedded_port) = parse_server_address(&base_url_str);
self.config_builder = self.config_builder.base_url(host);
if let Some(port) = embedded_port {
self.config_builder = self.config_builder.port(port);
}
self
}
pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
self.config_builder = self.config_builder.mtls_key(mtls_key);
self
}
pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
self.config_builder = self.config_builder.mtls_cert(mtls_cert);
self
}
pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
self.config_builder = self.config_builder.server_ca(server_ca);
self
}
pub fn port(mut self, port: u16) -> Self {
self.config_builder = self.config_builder.port(port);
self
}
pub fn protocol(mut self, protocol: Protocol) -> Self {
self.config_builder = self.config_builder.protocol(protocol);
self
}
pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
self.config_builder = self.config_builder.public_key(public_key);
self
}
pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
self.config_builder = self.config_builder.personal_keypair(keypair);
self
}
pub fn build(self) -> Result<Hessra, SdkError> {
let config = self.config_builder.build()?;
Hessra::new(config)
}
}
pub async fn fetch_public_key(
base_url: impl Into<String>,
port: Option<u16>,
server_ca: impl Into<String>,
) -> Result<String, SdkError> {
HessraClient::fetch_public_key(base_url, port, server_ca)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
#[cfg(feature = "http3")]
pub async fn fetch_public_key_http3(
base_url: impl Into<String>,
port: Option<u16>,
server_ca: impl Into<String>,
) -> Result<String, SdkError> {
HessraClient::fetch_public_key_http3(base_url, port, server_ca)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
pub async fn fetch_ca_cert(
base_url: impl Into<String>,
port: Option<u16>,
) -> Result<String, SdkError> {
HessraClient::fetch_ca_cert(base_url, port)
.await
.map_err(|e| SdkError::Generic(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_chain_creation() {
let json = r#"[
{
"component": "service1",
"public_key": "ed25519/abcdef1234567890"
},
{
"component": "service2",
"public_key": "ed25519/0987654321fedcba"
}
]"#;
let service_chain = ServiceChain::from_json(json).unwrap();
assert_eq!(service_chain.nodes().len(), 2);
assert_eq!(service_chain.nodes()[0].component, "service1");
assert_eq!(
service_chain.nodes()[0].public_key,
"ed25519/abcdef1234567890"
);
assert_eq!(service_chain.nodes()[1].component, "service2");
assert_eq!(
service_chain.nodes()[1].public_key,
"ed25519/0987654321fedcba"
);
let mut chain = ServiceChain::new();
let node = ServiceNode {
component: "service3".to_string(),
public_key: "ed25519/1122334455667788".to_string(),
};
chain.add_node(node);
assert_eq!(chain.nodes().len(), 1);
assert_eq!(chain.nodes()[0].component, "service3");
}
#[test]
fn test_service_chain_builder() {
let builder = ServiceChainBuilder::new();
let node1 = ServiceNode {
component: "auth".to_string(),
public_key: "ed25519/auth123".to_string(),
};
let node2 = ServiceNode {
component: "payment".to_string(),
public_key: "ed25519/payment456".to_string(),
};
let chain = builder.add_node(node1).add_node(node2).build();
assert_eq!(chain.nodes().len(), 2);
assert_eq!(chain.nodes()[0].component, "auth");
assert_eq!(chain.nodes()[1].component, "payment");
}
#[test]
fn test_parse_authorization_service_url() {
let (base_url, port) =
Hessra::parse_authorization_service_url("https://127.0.0.1:4433/sign_token").unwrap();
assert_eq!(base_url, "127.0.0.1");
assert_eq!(port, Some(4433));
let (base_url, port) =
Hessra::parse_authorization_service_url("http://example.com:8080/api/sign").unwrap();
assert_eq!(base_url, "example.com");
assert_eq!(port, Some(8080));
let (base_url, port) =
Hessra::parse_authorization_service_url("test.hessra.net:443/sign_token").unwrap();
assert_eq!(base_url, "test.hessra.net");
assert_eq!(port, Some(443));
let (base_url, port) =
Hessra::parse_authorization_service_url("example.com/api/endpoint").unwrap();
assert_eq!(base_url, "example.com");
assert_eq!(port, None);
let (base_url, port) =
Hessra::parse_authorization_service_url("https://localhost:8443").unwrap();
assert_eq!(base_url, "localhost");
assert_eq!(port, Some(8443));
let (base_url, port) = Hessra::parse_authorization_service_url("api.example.org").unwrap();
assert_eq!(base_url, "api.example.org");
assert_eq!(port, None);
}
}