Skip to main content

hessra_sdk/
lib.rs

1//! # Hessra SDK
2//!
3//! A Rust client library for interacting with Hessra authentication and authorization services.
4//!
5//! The Hessra SDK provides a robust and flexible way to request and verify both identity tokens
6//! and authorization tokens for protected resources. Authentication can be done via mutual TLS (mTLS)
7//! or using identity tokens for most operations.
8//!
9//! This crate combines functionality from:
10//! - `hessra-token`: Authorization token verification and attestation
11//! - `hessra-token-identity`: Identity token creation, verification, and delegation
12//! - `hessra-config`: Configuration management
13//! - `hessra-api`: HTTP client for the Hessra service
14//!
15//! ## Features
16//!
17//! - **Flexible configuration**: Load configuration from various sources (environment variables, files, etc.)
18//! - **Protocol support**: HTTP/1.1 support with optional HTTP/3 via feature flag
19//! - **Dual authentication**: Support for both mTLS and identity token authentication
20//! - **Identity tokens**: Hierarchical, delegatable identity tokens for authentication
21//! - **Authorization tokens**: Request and verify authorization tokens for resources
22//! - **Local verification**: Retrieve and store public keys for offline token verification
23//! - **Service chains**: Support for service chain attestation and verification
24//!
25//! ## Feature Flags
26//!
27//! - `http3`: Enables HTTP/3 protocol support
28//! - `toml`: Enables configuration loading from TOML files
29//! - `wasm`: Enables WebAssembly support for token verification
30
31use std::fs::File;
32use std::io::Read;
33use std::path::Path;
34use thiserror::Error;
35
36// Re-export everything from the component crates
37pub use hessra_token::{
38    // Token attestation
39    add_service_node_attestation,
40    decode_token,
41    encode_token,
42    // Prefix restriction functions
43    add_prefix_restriction,
44    add_prefix_restriction_to_token,
45    // Token verification
46    verify_biscuit_local,
47    verify_service_chain_biscuit_local,
48    // Verification builder
49    AuthorizationVerifier,
50    // Re-exported biscuit types
51    Biscuit,
52    KeyPair,
53    PublicKey,
54    // Service chain types
55    ServiceNode,
56    // Token errors
57    TokenError,
58};
59
60// Re-export identity token functionality
61pub use hessra_token_identity::{
62    add_identity_attenuation_to_token, create_identity_token, create_short_lived_identity_token,
63    verify_identity_token,
64};
65
66pub use hessra_config::{ConfigError, HessraConfig, Protocol};
67
68pub use hessra_api::{
69    parse_server_address, ApiError, HessraClient, HessraClientBuilder, IdentityTokenRequest,
70    IdentityTokenResponse, MintIdentityTokenRequest, MintIdentityTokenResponse, PublicKeyResponse,
71    RefreshIdentityTokenRequest, SignTokenRequest, SignTokenResponse, SignoffInfo,
72    StubTokenRequest, StubTokenResponse, TokenRequest, TokenResponse,
73    VerifyServiceChainTokenRequest, VerifyTokenRequest, VerifyTokenResponse,
74};
75
76/// Errors that can occur in the Hessra SDK
77#[derive(Error, Debug)]
78pub enum SdkError {
79    /// Configuration error
80    #[error("Configuration error: {0}")]
81    Config(#[from] ConfigError),
82
83    /// API error
84    #[error("API error: {0}")]
85    Api(#[from] ApiError),
86
87    /// Token error
88    #[error("Token error: {0}")]
89    Token(#[from] TokenError),
90
91    /// JSON serialization error
92    #[error("JSON error: {0}")]
93    Json(#[from] serde_json::Error),
94
95    /// I/O error
96    #[error("I/O error: {0}")]
97    Io(#[from] std::io::Error),
98
99    /// Generic error
100    #[error("{0}")]
101    Generic(String),
102}
103
104/// A chain of service nodes
105///
106/// Represents an ordered sequence of service nodes that form a processing chain.
107/// The order of nodes in the chain is significant - it defines the expected
108/// order of processing and attestation.
109#[derive(Clone, Debug, Default)]
110pub struct ServiceChain {
111    /// The nodes in the chain, in order
112    nodes: Vec<ServiceNode>,
113}
114
115impl ServiceChain {
116    /// Create a new empty service chain
117    pub fn new() -> Self {
118        Self { nodes: Vec::new() }
119    }
120
121    /// Create a service chain with the given nodes
122    pub fn with_nodes(nodes: Vec<ServiceNode>) -> Self {
123        Self { nodes }
124    }
125
126    /// Create a new service chain builder
127    pub fn builder() -> ServiceChainBuilder {
128        ServiceChainBuilder::new()
129    }
130
131    /// Add a node to the chain
132    pub fn add_node(&mut self, node: ServiceNode) -> &mut Self {
133        self.nodes.push(node);
134        self
135    }
136
137    /// Add a node to the chain (builder style)
138    pub fn with_node(mut self, node: ServiceNode) -> Self {
139        self.nodes.push(node);
140        self
141    }
142
143    /// Get the nodes in the chain
144    pub fn nodes(&self) -> &[ServiceNode] {
145        &self.nodes
146    }
147
148    /// Convert to internal representation for token verification
149    fn to_internal(&self) -> Vec<hessra_token::ServiceNode> {
150        self.nodes.to_vec()
151    }
152
153    /// Load a service chain from a JSON string
154    pub fn from_json(json: &str) -> Result<Self, SdkError> {
155        let nodes: Vec<ServiceNode> = serde_json::from_str(json)?;
156        Ok(Self::with_nodes(nodes))
157    }
158
159    /// Load a service chain from a JSON file
160    pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, SdkError> {
161        let mut file = File::open(path)?;
162        let mut contents = String::new();
163        file.read_to_string(&mut contents)?;
164        Self::from_json(&contents)
165    }
166
167    /// Load a service chain from a TOML string
168    #[cfg(feature = "toml")]
169    pub fn from_toml(toml_str: &str) -> Result<Self, SdkError> {
170        use serde::Deserialize;
171
172        #[derive(Deserialize)]
173        struct TomlServiceChain {
174            nodes: Vec<ServiceNode>,
175        }
176
177        let chain: TomlServiceChain = toml::from_str(toml_str)
178            .map_err(|e| SdkError::Generic(format!("TOML parse error: {e}")))?;
179
180        Ok(Self::with_nodes(chain.nodes))
181    }
182
183    /// Load a service chain from a TOML file
184    #[cfg(feature = "toml")]
185    pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, SdkError> {
186        let mut file = File::open(path)?;
187        let mut contents = String::new();
188        file.read_to_string(&mut contents)?;
189        Self::from_toml(&contents)
190    }
191}
192
193/// Builder for a service chain
194#[derive(Debug, Default)]
195pub struct ServiceChainBuilder {
196    nodes: Vec<ServiceNode>,
197}
198
199impl ServiceChainBuilder {
200    /// Create a new service chain builder
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    /// Add a node to the chain
206    pub fn add_node(mut self, node: ServiceNode) -> Self {
207        self.nodes.push(node);
208        self
209    }
210
211    /// Build the service chain
212    pub fn build(self) -> ServiceChain {
213        ServiceChain::with_nodes(self.nodes)
214    }
215}
216
217/// Unified SDK for Hessra authentication services
218///
219/// This struct provides a high-level interface combining functionality
220/// from all component crates (config, token, api).
221pub struct Hessra {
222    client: HessraClient,
223    config: HessraConfig,
224}
225
226impl Hessra {
227    /// Create a new Hessra SDK instance from a configuration
228    pub fn new(config: HessraConfig) -> Result<Self, SdkError> {
229        let client = HessraClientBuilder::new()
230            .from_config(&config)
231            .build()
232            .map_err(|e| SdkError::Generic(e.to_string()))?;
233
234        Ok(Self { client, config })
235    }
236
237    /// Create a builder for a Hessra SDK instance
238    pub fn builder() -> HessraBuilder {
239        HessraBuilder::new()
240    }
241
242    /// Setup the SDK with the public key
243    ///
244    /// This will fetch the public key from the Hessra service and set it in the SDK configuration.
245    /// If the public key is already set, it will be overwritten.
246    /// Requires a mutable reference to the SDK instance.
247    pub async fn setup(&mut self) -> Result<(), SdkError> {
248        match self.get_public_key().await {
249            Ok(public_key) => {
250                self.config.public_key = Some(public_key);
251                Ok(())
252            }
253            Err(e) => Err(SdkError::Generic(e.to_string())),
254        }
255    }
256
257    /// Setup the SDK with the public key and return a new instance
258    ///
259    /// This will fetch the public key from the Hessra service and set it in the SDK configuration.
260    /// If the public key is already set, it will be overwritten.
261    pub async fn with_setup(&self) -> Result<Self, SdkError> {
262        match self.get_public_key().await {
263            Ok(public_key) => {
264                let config = self.config.to_builder().public_key(public_key).build()?;
265                Ok(Self::new(config)?)
266            }
267            Err(e) => Err(SdkError::Generic(e.to_string())),
268        }
269    }
270
271    /// Request a token for a resource
272    /// Returns the full TokenResponse which may include pending signoffs for multi-party tokens
273    ///
274    /// # Arguments
275    /// * `resource` - The resource identifier to request authorization for
276    /// * `operation` - The operation to request authorization for
277    /// * `domain` - Optional domain for domain-restricted identity token verification
278    pub async fn request_token(
279        &self,
280        resource: impl Into<String>,
281        operation: impl Into<String>,
282        domain: Option<String>,
283    ) -> Result<TokenResponse, SdkError> {
284        self.client
285            .request_token(resource.into(), operation.into(), domain)
286            .await
287            .map_err(|e| SdkError::Generic(e.to_string()))
288    }
289
290    /// Apply JIT attenuation to an identity token for secure transmission
291    /// Creates a short-lived (5 second) version of the token
292    fn apply_jit_attenuation(&self, identity_token: String) -> String {
293        // Only apply JIT attenuation if we have a public key configured
294        if let Some(ref public_key_pem) = self.config.public_key {
295            // Parse the public key
296            if let Ok(public_key) = PublicKey::from_pem(public_key_pem.as_str()) {
297                // Apply JIT attenuation for 5-second expiry
298                if let Ok(attenuated_token) =
299                    create_short_lived_identity_token(identity_token.clone(), public_key)
300                {
301                    return attenuated_token;
302                }
303            }
304        }
305        // If attenuation fails or public key is not configured, return the original token
306        identity_token
307    }
308
309    /// Request a token for a resource using an identity token for authentication
310    /// This method should be used when you have a delegated identity token
311    /// and want to request authorization tokens as that delegated identity
312    /// The identity token will be automatically attenuated with a 5-second expiry for security
313    ///
314    /// # Arguments
315    /// * `resource` - The resource identifier to request authorization for
316    /// * `operation` - The operation to request authorization for
317    /// * `identity_token` - The identity token to use for authentication
318    /// * `domain` - Optional domain for domain-restricted identity token verification
319    pub async fn request_token_with_identity(
320        &self,
321        resource: impl Into<String>,
322        operation: impl Into<String>,
323        identity_token: impl Into<String>,
324        domain: Option<String>,
325    ) -> Result<TokenResponse, SdkError> {
326        let token = identity_token.into();
327        // Apply JIT attenuation to the identity token
328        let attenuated_token = self.apply_jit_attenuation(token);
329
330        self.client
331            .request_token_with_identity(
332                resource.into(),
333                operation.into(),
334                attenuated_token,
335                domain,
336            )
337            .await
338            .map_err(|e| SdkError::Generic(e.to_string()))
339    }
340
341    /// Request a token for a resource (simple version)
342    /// Returns just the token string for backward compatibility
343    pub async fn request_token_simple(
344        &self,
345        resource: impl Into<String>,
346        operation: impl Into<String>,
347    ) -> Result<String, SdkError> {
348        let response = self.request_token(resource, operation, None).await?;
349        match response.token {
350            Some(token) => Ok(token),
351            None => Err(SdkError::Generic(format!(
352                "Failed to get token: {}",
353                response.response_msg
354            ))),
355        }
356    }
357
358    /// Sign a multi-party token by calling an authorization service's signoff endpoint
359    pub async fn sign_token(
360        &self,
361        token: &str,
362        resource: &str,
363        operation: &str,
364    ) -> Result<SignTokenResponse, SdkError> {
365        self.client
366            .sign_token(token, resource, operation)
367            .await
368            .map_err(|e| SdkError::Generic(e.to_string()))
369    }
370
371    /// Parse an authorization service URL to extract base URL and port
372    /// Handles URLs like "https://hostname:port/path" or "hostname:port/path"
373    /// Returns (base_url, port) where base_url is just the hostname part
374    fn parse_authorization_service_url(url: &str) -> Result<(String, Option<u16>), SdkError> {
375        let url_str = if url.starts_with("http://") || url.starts_with("https://") {
376            url.to_string()
377        } else {
378            // If no protocol, assume https for parsing
379            format!("https://{url}")
380        };
381
382        let parsed_url = url::Url::parse(&url_str).map_err(|e| {
383            SdkError::Generic(format!(
384                "Failed to parse authorization service URL '{url}': {e}"
385            ))
386        })?;
387
388        let host = parsed_url
389            .host_str()
390            .ok_or_else(|| SdkError::Generic(format!("No host found in URL: {url}")))?;
391
392        // For URLs where port is not explicitly specified but the scheme indicates a default port,
393        // we need to check if the original URL had an explicit port
394        let port = if parsed_url.port().is_some() {
395            parsed_url.port()
396        } else if url.contains(':') && !url.starts_with("http://") && !url.starts_with("https://") {
397            // If the original URL has a colon and no protocol, it likely has an explicit port
398            // Try to extract it manually
399            if let Some(host_port) = url.split('/').next() {
400                if let Some(port_str) = host_port.split(':').nth(1) {
401                    port_str.parse::<u16>().ok()
402                } else {
403                    None
404                }
405            } else {
406                None
407            }
408        } else {
409            parsed_url.port()
410        };
411
412        Ok((host.to_string(), port))
413    }
414
415    /// Collect all required signoffs for a multi-party token
416    /// Returns the fully signed token once all signoffs are collected
417    pub async fn collect_signoffs(
418        &self,
419        initial_token_response: TokenResponse,
420        resource: &str,
421        operation: &str,
422    ) -> Result<String, SdkError> {
423        // If no pending signoffs, return the token immediately
424        let pending_signoffs = match &initial_token_response.pending_signoffs {
425            Some(signoffs) if !signoffs.is_empty() => signoffs,
426            _ => {
427                return initial_token_response
428                    .token
429                    .ok_or_else(|| SdkError::Generic("No token in response".to_string()))
430            }
431        };
432
433        let mut current_token = initial_token_response.token.ok_or_else(|| {
434            SdkError::Generic("No initial token to collect signoffs for".to_string())
435        })?;
436
437        // For each SignoffInfo in pending_signoffs, create a client and call sign_token
438        for signoff_info in pending_signoffs {
439            // Parse the authorization service URL to extract base URL and port
440            let (base_url, port) =
441                Self::parse_authorization_service_url(&signoff_info.authorization_service)?;
442
443            // Create a temporary client for this authorization service
444            // Note: This is a simplified approach. In practice, you might want to
445            // have a configuration system for managing multiple service certificates
446            let mut client_builder = HessraClientBuilder::new()
447                .base_url(base_url)
448                .protocol(self.config.protocol.clone())
449                .server_ca(self.config.server_ca.clone());
450
451            // Add mTLS if configured
452            if let (Some(cert), Some(key)) = (&self.config.mtls_cert, &self.config.mtls_key) {
453                client_builder = client_builder.mtls_cert(cert.clone()).mtls_key(key.clone());
454            }
455
456            if let Some(port) = port {
457                client_builder = client_builder.port(port);
458            }
459
460            let signoff_client = client_builder
461                .build()
462                .map_err(|e| SdkError::Generic(format!("Failed to create signoff client: {e}")))?;
463
464            let sign_response = signoff_client
465                .sign_token(&current_token, resource, operation)
466                .await
467                .map_err(|e| {
468                    SdkError::Generic(format!(
469                        "Signoff failed for {}: {e}",
470                        signoff_info.component
471                    ))
472                })?;
473
474            current_token = sign_response.signed_token.ok_or_else(|| {
475                SdkError::Generic(format!(
476                    "No signed token returned from {}: {}",
477                    signoff_info.component, sign_response.response_msg
478                ))
479            })?;
480        }
481
482        Ok(current_token)
483    }
484
485    /// Request a token and automatically collect any required signoffs
486    /// This is a convenience method that combines token request and signoff collection
487    pub async fn request_token_with_signoffs(
488        &self,
489        resource: &str,
490        operation: &str,
491    ) -> Result<String, SdkError> {
492        let initial_response = self.request_token(resource, operation, None).await?;
493        self.collect_signoffs(initial_response, resource, operation)
494            .await
495    }
496
497    /// Verify a token
498    ///
499    /// This function verifies a token using either the remote Hessra service or
500    /// locally using the service's public key if one is configured. This will always
501    /// prefer to verify locally if a public key is configured.
502    pub async fn verify_token(
503        &self,
504        token: impl Into<String>,
505        subject: impl Into<String>,
506        resource: impl Into<String>,
507        operation: impl Into<String>,
508    ) -> Result<(), SdkError> {
509        if self.config.public_key.is_some() {
510            self.verify_token_local(
511                token.into(),
512                subject.into(),
513                resource.into(),
514                operation.into(),
515            )
516        } else {
517            self.verify_token_remote(
518                token.into(),
519                subject.into(),
520                resource.into(),
521                operation.into(),
522            )
523            .await
524            .map(|_| ())
525            .map_err(|e| SdkError::Generic(e.to_string()))
526        }
527    }
528
529    /// Verify a token using the remote Hessra service
530    pub async fn verify_token_remote(
531        &self,
532        token: impl Into<String>,
533        subject: impl Into<String>,
534        resource: impl Into<String>,
535        operation: impl Into<String>,
536    ) -> Result<String, SdkError> {
537        self.client
538            .verify_token(
539                token.into(),
540                subject.into(),
541                resource.into(),
542                operation.into(),
543            )
544            .await
545            .map_err(|e| SdkError::Generic(e.to_string()))
546    }
547
548    /// Verify a token locally using cached public keys
549    pub fn verify_token_local(
550        &self,
551        token: impl Into<String>,
552        subject: impl AsRef<str>,
553        resource: impl AsRef<str>,
554        operation: impl AsRef<str>,
555    ) -> Result<(), SdkError> {
556        let public_key_str = match &self.config.public_key {
557            Some(key) => key,
558            None => return Err(SdkError::Generic("Public key not configured".to_string())),
559        };
560
561        let public_key = PublicKey::from_pem(public_key_str.as_str())
562            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
563
564        // Convert token to Vec<u8>
565        let token_vec = decode_token(&token.into())?;
566
567        verify_biscuit_local(
568            token_vec,
569            public_key,
570            subject.as_ref().to_string(),
571            resource.as_ref().to_string(),
572            operation.as_ref().to_string(),
573        )
574        .map_err(SdkError::Token)
575    }
576
577    /// Verify a service chain token
578    ///
579    /// This function verifies a service chain token using either the remote Hessra service or
580    /// locally using the service's public key if one is configured. This will always
581    /// prefer to verify locally if a public key is configured and a service chain is provided.
582    pub async fn verify_service_chain_token(
583        &self,
584        token: impl Into<String>,
585        subject: impl Into<String>,
586        resource: impl Into<String>,
587        operation: impl Into<String>,
588        service_chain: Option<&ServiceChain>,
589        component: Option<String>,
590    ) -> Result<(), SdkError> {
591        match (&self.config.public_key, service_chain) {
592            (Some(_), Some(chain)) => self.verify_service_chain_token_local(
593                token.into(),
594                subject.into(),
595                resource.into(),
596                operation.into(),
597                chain,
598                component,
599            ),
600            _ => self
601                .verify_service_chain_token_remote(
602                    token.into(),
603                    subject.into(),
604                    resource.into(),
605                    component,
606                )
607                .await
608                .map(|_| ())
609                .map_err(|e| SdkError::Generic(e.to_string())),
610        }
611    }
612
613    /// Verify a service chain token using the remote Hessra service
614    pub async fn verify_service_chain_token_remote(
615        &self,
616        token: impl Into<String>,
617        subject: impl Into<String>,
618        resource: impl Into<String>,
619        component: Option<String>,
620    ) -> Result<String, SdkError> {
621        self.client
622            .verify_service_chain_token(token.into(), subject.into(), resource.into(), component)
623            .await
624            .map_err(|e| SdkError::Generic(e.to_string()))
625    }
626
627    /// Verify a service chain token locally using cached public keys
628    pub fn verify_service_chain_token_local(
629        &self,
630        token: String,
631        subject: impl AsRef<str>,
632        resource: impl AsRef<str>,
633        operation: impl AsRef<str>,
634        service_chain: &ServiceChain,
635        component: Option<String>,
636    ) -> Result<(), SdkError> {
637        let public_key_str = match &self.config.public_key {
638            Some(key) => key,
639            None => return Err(SdkError::Generic("Public key not configured".to_string())),
640        };
641
642        let public_key = PublicKey::from_pem(public_key_str.as_str())
643            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
644
645        // Convert token to Vec<u8>
646        let token_vec = decode_token(&token)?;
647
648        verify_service_chain_biscuit_local(
649            token_vec,
650            public_key,
651            subject.as_ref().to_string(),
652            resource.as_ref().to_string(),
653            operation.as_ref().to_string(),
654            service_chain.to_internal(),
655            component,
656        )
657        .map_err(SdkError::Token)
658    }
659
660    /// Attest a service chain token with a new service node attestation
661    /// Expects a base64 encoded token string and a service name
662    /// Returns a base64 encoded token string
663    pub fn attest_service_chain_token(
664        &self,
665        token: String,
666        service: impl Into<String>,
667    ) -> Result<String, SdkError> {
668        let keypair_str = match &self.config.personal_keypair {
669            Some(keypair) => keypair,
670            None => {
671                return Err(SdkError::Generic(
672                    "Personal keypair not configured".to_string(),
673                ))
674            }
675        };
676
677        let public_key_str = match &self.config.public_key {
678            Some(key) => key,
679            None => return Err(SdkError::Generic("Public key not configured".to_string())),
680        };
681
682        // Parse keypair from string to KeyPair
683        let keypair = KeyPair::from_private_key_pem(keypair_str.as_str())
684            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
685
686        // Parse public key from PEM string
687        let public_key = PublicKey::from_pem(public_key_str.as_str())
688            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
689
690        // Convert token to Vec<u8>
691        let token_vec = decode_token(&token)?;
692
693        // Convert service to String
694        let service_str = service.into();
695
696        let token_vec = add_service_node_attestation(token_vec, public_key, &service_str, &keypair)
697            .map_err(SdkError::Token)?;
698
699        Ok(encode_token(&token_vec))
700    }
701
702    /// Get the public key from the Hessra service
703    pub async fn get_public_key(&self) -> Result<String, SdkError> {
704        self.client
705            .get_public_key()
706            .await
707            .map_err(|e| SdkError::Generic(e.to_string()))
708    }
709
710    /// Request a new identity token from the authorization service
711    ///
712    /// This method requires mTLS authentication as it's the initial issuance of an identity token.
713    /// Once you have an identity token, you can use it for authentication in subsequent requests
714    /// instead of mTLS certificates.
715    ///
716    /// # Arguments
717    /// * `identifier` - Optional identifier for the identity. Can be derived from mTLS certificate if not provided.
718    pub async fn request_identity_token(
719        &self,
720        identifier: Option<String>,
721    ) -> Result<IdentityTokenResponse, SdkError> {
722        self.client
723            .request_identity_token(identifier)
724            .await
725            .map_err(|e| SdkError::Generic(e.to_string()))
726    }
727
728    /// Refresh an existing identity token
729    ///
730    /// This method can use either mTLS or the current identity token for authentication.
731    /// When the SDK client is configured without mTLS certificates, the current token
732    /// will be used for authentication and the identifier parameter is required.
733    ///
734    /// # Arguments
735    /// * `current_token` - The existing identity token to refresh
736    /// * `identifier` - Optional identifier. Required when not using mTLS authentication.
737    pub async fn refresh_identity_token(
738        &self,
739        current_token: impl Into<String>,
740        identifier: Option<String>,
741    ) -> Result<IdentityTokenResponse, SdkError> {
742        self.client
743            .refresh_identity_token(current_token.into(), identifier)
744            .await
745            .map_err(|e| SdkError::Generic(e.to_string()))
746    }
747
748    /// Mint a new domain-restricted identity token
749    ///
750    /// This method requires mTLS authentication from a "realm" identity (one without domain restriction).
751    /// The minted token will be restricted to the minting identity's domain and cannot mint further sub-identities.
752    /// Permissions for the minted identity are determined by domain roles configured on the server.
753    ///
754    /// # Arguments
755    /// * `subject` - The subject identifier for the new identity (e.g., "uri:urn:test:argo-cli1:user123")
756    /// * `duration` - Optional duration in seconds. If None, server uses configured default.
757    ///
758    /// # Example
759    /// ```no_run
760    /// # use hessra_sdk::Hessra;
761    /// # async fn example(sdk: &Hessra) -> Result<(), Box<dyn std::error::Error>> {
762    /// // Mint a domain-restricted identity token for a user
763    /// let response = sdk.mint_domain_restricted_identity_token(
764    ///     "uri:urn:test:argo-cli1:user123".to_string(),
765    ///     Some(3600) // 1 hour
766    /// ).await?;
767    ///
768    /// if let Some(token) = response.token {
769    ///     println!("Minted identity token: {}", token);
770    /// }
771    /// # Ok(())
772    /// # }
773    /// ```
774    pub async fn mint_domain_restricted_identity_token(
775        &self,
776        subject: impl Into<String>,
777        duration: Option<u64>,
778    ) -> Result<MintIdentityTokenResponse, SdkError> {
779        self.client
780            .mint_domain_restricted_identity_token(subject.into(), duration)
781            .await
782            .map_err(|e| SdkError::Generic(e.to_string()))
783    }
784
785    /// Request a stub token that requires prefix attestation before use.
786    ///
787    /// This method requires mTLS authentication from a "realm" identity.
788    /// The minted stub token will be for a target identity within the realm's domain
789    /// and will require a trusted third party (identified by prefix_attenuator_key)
790    /// to add a prefix restriction before the token can be used.
791    ///
792    /// # Arguments
793    /// * `target_identity` - The identity who will use this token (must be in minter's domain)
794    /// * `resource` - The resource the stub token grants access to
795    /// * `operation` - The operation allowed on the resource
796    /// * `prefix_attenuator_key` - Public key that will attest the prefix (format: "ed25519/..." or "secp256r1/...")
797    /// * `duration` - Optional token duration in seconds (defaults to minter's configured duration)
798    ///
799    /// # Example
800    /// ```no_run
801    /// # use hessra_sdk::Hessra;
802    /// # async fn example(sdk: &Hessra) -> Result<(), Box<dyn std::error::Error>> {
803    /// // Request a stub token for a user that requires prefix attestation
804    /// let response = sdk.request_stub_token(
805    ///     "uri:urn:test:argo-cli1:user123".to_string(),
806    ///     "files".to_string(),
807    ///     "read".to_string(),
808    ///     "ed25519/abcdef1234567890...".to_string(),
809    ///     Some(3600), // 1 hour
810    /// ).await?;
811    ///
812    /// if let Some(token) = response.token {
813    ///     println!("Got stub token: {}", token);
814    /// }
815    /// # Ok(())
816    /// # }
817    /// ```
818    pub async fn request_stub_token(
819        &self,
820        target_identity: impl Into<String>,
821        resource: impl Into<String>,
822        operation: impl Into<String>,
823        prefix_attenuator_key: impl Into<String>,
824        duration: Option<u64>,
825    ) -> Result<StubTokenResponse, SdkError> {
826        self.client
827            .request_stub_token(
828                target_identity.into(),
829                resource.into(),
830                operation.into(),
831                prefix_attenuator_key.into(),
832                duration,
833            )
834            .await
835            .map_err(|e| SdkError::Generic(e.to_string()))
836    }
837
838    /// Request a stub token using an identity token for authentication.
839    ///
840    /// This is similar to `request_stub_token` but uses an identity token
841    /// instead of mTLS for authentication. The identity token will be
842    /// automatically attenuated with a 5-second expiry for security.
843    ///
844    /// # Arguments
845    /// * `target_identity` - The identity who will use this token (must be in minter's domain)
846    /// * `resource` - The resource the stub token grants access to
847    /// * `operation` - The operation allowed on the resource
848    /// * `prefix_attenuator_key` - Public key that will attest the prefix (format: "ed25519/..." or "secp256r1/...")
849    /// * `identity_token` - The identity token to use for authentication
850    /// * `duration` - Optional token duration in seconds (defaults to minter's configured duration)
851    pub async fn request_stub_token_with_identity(
852        &self,
853        target_identity: impl Into<String>,
854        resource: impl Into<String>,
855        operation: impl Into<String>,
856        prefix_attenuator_key: impl Into<String>,
857        identity_token: impl Into<String>,
858        duration: Option<u64>,
859    ) -> Result<StubTokenResponse, SdkError> {
860        let token = identity_token.into();
861        // Apply JIT attenuation to the identity token
862        let attenuated_token = self.apply_jit_attenuation(token);
863
864        self.client
865            .request_stub_token_with_identity(
866                target_identity.into(),
867                resource.into(),
868                operation.into(),
869                prefix_attenuator_key.into(),
870                attenuated_token,
871                duration,
872            )
873            .await
874            .map_err(|e| SdkError::Generic(e.to_string()))
875    }
876
877    /// Verify an identity token locally using the configured public key
878    ///
879    /// This performs offline verification of an identity token without contacting the server.
880    /// Requires the public key to be configured in the SDK.
881    ///
882    /// # Arguments
883    /// * `token` - The identity token to verify
884    /// * `identity` - The identity URI to verify the token for (e.g., "urn:hessra:alice")
885    pub fn verify_identity_token_local(
886        &self,
887        token: impl Into<String>,
888        identity: impl Into<String>,
889    ) -> Result<(), SdkError> {
890        let public_key_str = match &self.config.public_key {
891            Some(key) => key,
892            None => return Err(SdkError::Generic("Public key not configured".to_string())),
893        };
894
895        let public_key = PublicKey::from_pem(public_key_str.as_str())
896            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
897
898        verify_identity_token(token.into(), public_key, identity.into())
899            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))
900    }
901
902    /// Attenuate an identity token by adding a delegated identity
903    ///
904    /// This creates a more restrictive token by delegating from the original identity to a sub-identity.
905    /// The resulting token can only be used by the delegated identity and its sub-hierarchies.
906    /// For example, attenuating "urn:hessra:alice" to "urn:hessra:alice:laptop" creates a token
907    /// that only the laptop identity can use.
908    ///
909    /// # Arguments
910    /// * `token` - The identity token to attenuate
911    /// * `delegated_identity` - The sub-identity to delegate to (must be hierarchically under the original)
912    /// * `expiration` - Optional expiration time for the attenuated token
913    pub fn attenuate_identity_token(
914        &self,
915        token: impl Into<String>,
916        delegated_identity: impl Into<String>,
917        duration: i64,
918    ) -> Result<String, SdkError> {
919        let public_key_str = match &self.config.public_key {
920            Some(key) => key,
921            None => return Err(SdkError::Generic("Public key not configured".to_string())),
922        };
923
924        let public_key = PublicKey::from_pem(public_key_str.as_str())
925            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
926
927        let time_config = hessra_token_core::TokenTimeConfig {
928            start_time: None,
929            duration,
930        };
931
932        add_identity_attenuation_to_token(
933            token.into(),
934            delegated_identity.into(),
935            public_key,
936            time_config,
937        )
938        .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))
939    }
940
941    /// Create a new identity token locally
942    ///
943    /// This creates an identity token without contacting the authorization service.
944    /// Typically used by services that need to create identity tokens for testing
945    /// or for use within their own trust domain.
946    ///
947    /// # Arguments
948    /// * `subject` - The identity URI for the token (e.g., "urn:hessra:service")
949    /// * `expiration` - Optional expiration time (defaults to 1 hour if not specified)
950    pub fn create_identity_token_local(
951        &self,
952        subject: impl Into<String>,
953        duration: i64,
954    ) -> Result<String, SdkError> {
955        let keypair_str = match &self.config.personal_keypair {
956            Some(keypair) => keypair,
957            None => {
958                return Err(SdkError::Generic(
959                    "Personal keypair not configured".to_string(),
960                ))
961            }
962        };
963
964        let keypair = KeyPair::from_private_key_pem(keypair_str.as_str())
965            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))?;
966
967        let time_config = hessra_token_core::TokenTimeConfig {
968            start_time: None,
969            duration,
970        };
971
972        create_identity_token(subject.into(), keypair, time_config)
973            .map_err(|e| SdkError::Token(TokenError::Generic(e.to_string())))
974    }
975
976    /// Get the client used by this SDK instance
977    pub fn client(&self) -> &HessraClient {
978        &self.client
979    }
980
981    /// Get the configuration used by this SDK instance
982    pub fn config(&self) -> &HessraConfig {
983        &self.config
984    }
985}
986
987/// Builder for Hessra SDK instances
988#[derive(Default)]
989pub struct HessraBuilder {
990    config_builder: hessra_config::HessraConfigBuilder,
991}
992
993impl HessraBuilder {
994    /// Create a new Hessra SDK builder
995    pub fn new() -> Self {
996        Self {
997            config_builder: HessraConfig::builder(),
998        }
999    }
1000
1001    /// Set the base URL for the Hessra service.
1002    ///
1003    /// Accepts various address formats including:
1004    /// - IP:Port (e.g., "127.0.0.1:4433")
1005    /// - IP alone (e.g., "127.0.0.1")
1006    /// - hostname:port (e.g., "test.hessra.net:443")
1007    /// - hostname alone (e.g., "test.hessra.net")
1008    ///
1009    /// If the address includes a port, it will be automatically extracted
1010    /// and used unless explicitly overridden by calling `.port()`.
1011    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
1012        let base_url_str = base_url.into();
1013        let (host, embedded_port) = parse_server_address(&base_url_str);
1014
1015        self.config_builder = self.config_builder.base_url(host);
1016
1017        // If an embedded port was found, set it (can be overridden by explicit .port() call)
1018        if let Some(port) = embedded_port {
1019            self.config_builder = self.config_builder.port(port);
1020        }
1021
1022        self
1023    }
1024
1025    /// Set the mTLS private key
1026    pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
1027        self.config_builder = self.config_builder.mtls_key(mtls_key);
1028        self
1029    }
1030
1031    /// Set the mTLS client certificate
1032    pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
1033        self.config_builder = self.config_builder.mtls_cert(mtls_cert);
1034        self
1035    }
1036
1037    /// Set the server CA certificate
1038    pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
1039        self.config_builder = self.config_builder.server_ca(server_ca);
1040        self
1041    }
1042
1043    /// Set the port for the Hessra service
1044    pub fn port(mut self, port: u16) -> Self {
1045        self.config_builder = self.config_builder.port(port);
1046        self
1047    }
1048
1049    /// Set the protocol to use
1050    pub fn protocol(mut self, protocol: Protocol) -> Self {
1051        self.config_builder = self.config_builder.protocol(protocol);
1052        self
1053    }
1054
1055    /// Set the public key for token verification
1056    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
1057        self.config_builder = self.config_builder.public_key(public_key);
1058        self
1059    }
1060
1061    /// Set the personal keypair for service chain attestation
1062    pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
1063        self.config_builder = self.config_builder.personal_keypair(keypair);
1064        self
1065    }
1066
1067    /// Build a Hessra SDK instance
1068    pub fn build(self) -> Result<Hessra, SdkError> {
1069        let config = self.config_builder.build()?;
1070        Hessra::new(config)
1071    }
1072}
1073
1074/// Fetch a public key from the Hessra service
1075///
1076/// This is a convenience function that doesn't require a fully configured client.
1077pub async fn fetch_public_key(
1078    base_url: impl Into<String>,
1079    port: Option<u16>,
1080    server_ca: impl Into<String>,
1081) -> Result<String, SdkError> {
1082    HessraClient::fetch_public_key(base_url, port, server_ca)
1083        .await
1084        .map_err(|e| SdkError::Generic(e.to_string()))
1085}
1086
1087/// Fetch a public key from the Hessra service using HTTP/3
1088///
1089/// This is a convenience function that doesn't require a fully configured client.
1090#[cfg(feature = "http3")]
1091pub async fn fetch_public_key_http3(
1092    base_url: impl Into<String>,
1093    port: Option<u16>,
1094    server_ca: impl Into<String>,
1095) -> Result<String, SdkError> {
1096    HessraClient::fetch_public_key_http3(base_url, port, server_ca)
1097        .await
1098        .map_err(|e| SdkError::Generic(e.to_string()))
1099}
1100
1101/// Fetch a CA certificate from the Hessra service
1102///
1103/// This is a convenience function that makes an unauthenticated request to the
1104/// `/ca_cert` endpoint to retrieve the server's CA certificate in PEM format.
1105/// This is useful for bootstrapping trust when setting up a new client.
1106///
1107/// Uses the system CA store for the initial connection.
1108pub async fn fetch_ca_cert(
1109    base_url: impl Into<String>,
1110    port: Option<u16>,
1111) -> Result<String, SdkError> {
1112    HessraClient::fetch_ca_cert(base_url, port)
1113        .await
1114        .map_err(|e| SdkError::Generic(e.to_string()))
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120
1121    #[test]
1122    fn test_service_chain_creation() {
1123        // Create a simple service chain with two nodes
1124        let json = r#"[
1125            {
1126                "component": "service1",
1127                "public_key": "ed25519/abcdef1234567890"
1128            },
1129            {
1130                "component": "service2",
1131                "public_key": "ed25519/0987654321fedcba"
1132            }
1133        ]"#;
1134
1135        let service_chain = ServiceChain::from_json(json).unwrap();
1136        assert_eq!(service_chain.nodes().len(), 2);
1137        assert_eq!(service_chain.nodes()[0].component, "service1");
1138        assert_eq!(
1139            service_chain.nodes()[0].public_key,
1140            "ed25519/abcdef1234567890"
1141        );
1142        assert_eq!(service_chain.nodes()[1].component, "service2");
1143        assert_eq!(
1144            service_chain.nodes()[1].public_key,
1145            "ed25519/0987654321fedcba"
1146        );
1147
1148        // Test adding a node
1149        let mut chain = ServiceChain::new();
1150        let node = ServiceNode {
1151            component: "service3".to_string(),
1152            public_key: "ed25519/1122334455667788".to_string(),
1153        };
1154        chain.add_node(node);
1155        assert_eq!(chain.nodes().len(), 1);
1156        assert_eq!(chain.nodes()[0].component, "service3");
1157    }
1158
1159    #[test]
1160    fn test_service_chain_builder() {
1161        let builder = ServiceChainBuilder::new();
1162        let node1 = ServiceNode {
1163            component: "auth".to_string(),
1164            public_key: "ed25519/auth123".to_string(),
1165        };
1166        let node2 = ServiceNode {
1167            component: "payment".to_string(),
1168            public_key: "ed25519/payment456".to_string(),
1169        };
1170
1171        let chain = builder.add_node(node1).add_node(node2).build();
1172
1173        assert_eq!(chain.nodes().len(), 2);
1174        assert_eq!(chain.nodes()[0].component, "auth");
1175        assert_eq!(chain.nodes()[1].component, "payment");
1176    }
1177
1178    #[test]
1179    fn test_parse_authorization_service_url() {
1180        // Test URL with https protocol and path
1181        let (base_url, port) =
1182            Hessra::parse_authorization_service_url("https://127.0.0.1:4433/sign_token").unwrap();
1183        assert_eq!(base_url, "127.0.0.1");
1184        assert_eq!(port, Some(4433));
1185
1186        // Test URL with http protocol
1187        let (base_url, port) =
1188            Hessra::parse_authorization_service_url("http://example.com:8080/api/sign").unwrap();
1189        assert_eq!(base_url, "example.com");
1190        assert_eq!(port, Some(8080));
1191
1192        // Test URL without protocol but with port and path
1193        let (base_url, port) =
1194            Hessra::parse_authorization_service_url("test.hessra.net:443/sign_token").unwrap();
1195        assert_eq!(base_url, "test.hessra.net");
1196        assert_eq!(port, Some(443));
1197
1198        // Test URL without protocol and without port
1199        let (base_url, port) =
1200            Hessra::parse_authorization_service_url("example.com/api/endpoint").unwrap();
1201        assert_eq!(base_url, "example.com");
1202        assert_eq!(port, None);
1203
1204        // Test URL with just hostname and port (no path)
1205        let (base_url, port) =
1206            Hessra::parse_authorization_service_url("https://localhost:8443").unwrap();
1207        assert_eq!(base_url, "localhost");
1208        assert_eq!(port, Some(8443));
1209
1210        // Test hostname only (no protocol, port, or path)
1211        let (base_url, port) = Hessra::parse_authorization_service_url("api.example.org").unwrap();
1212        assert_eq!(base_url, "api.example.org");
1213        assert_eq!(port, None);
1214    }
1215}