Skip to main content

appcore_peer_rpc/
authentication.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: authentication.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/24 13:18:47 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use super::*;
12
13/// Issues short-lived credentials bound to peer requests.
14pub trait PeerRpcTokenIssuer: Send + Sync {
15    /// Issues a token for a request identifier and optional request hash.
16    fn issue_peer_token(
17        &self,
18        request_id: &str,
19        request_hash: Option<&str>,
20        now_ms: u64,
21        ttl_ms: u64,
22    ) -> Result<String, PeerRpcError>;
23}
24
25/// Token issuer backed by AppCore's signed local token provider.
26#[derive(Debug, Clone)]
27pub struct HashTokenPeerTokenIssuer<P = HashTokenProvider> {
28    provider: P,
29    claims: TokenClaims,
30}
31
32/// Token issuer for explicitly configured static credentials.
33///
34/// The credential is zeroized on drop and redacted from debug output.
35#[cfg(any(test, feature = "insecure-testing"))]
36#[derive(Clone)]
37pub struct StaticPeerRpcTokenIssuer {
38    token: Vec<u8>,
39}
40
41#[cfg(any(test, feature = "insecure-testing"))]
42impl std::fmt::Debug for StaticPeerRpcTokenIssuer {
43    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        formatter
45            .debug_struct("StaticPeerRpcTokenIssuer")
46            .field("token", &"REDACTED")
47            .finish()
48    }
49}
50
51#[cfg(any(test, feature = "insecure-testing"))]
52impl Drop for StaticPeerRpcTokenIssuer {
53    fn drop(&mut self) {
54        use zeroize::Zeroize;
55        self.token.zeroize();
56    }
57}
58
59/// Host integration contract for application-owned peer query and command handling.
60pub trait PeerRpcDispatcher: Send + Sync {
61    /// Dispatches a validated peer query envelope.
62    fn dispatch_peer_query(
63        &self,
64        envelope: PeerRpcEnvelope,
65    ) -> Result<PeerRpcResponse, PeerRpcError>;
66
67    /// Dispatches a validated peer command envelope.
68    fn dispatch_peer_command(
69        &self,
70        envelope: PeerRpcEnvelope,
71    ) -> Result<PeerRpcResponse, PeerRpcError>;
72}
73
74/// Validates credentials supplied to peer RPC endpoints.
75pub trait PeerRpcAuthenticator: Send + Sync {
76    /// Authenticates a token and optionally binds it to the expected request hash.
77    fn authenticate(
78        &self,
79        token: Option<&str>,
80        expected_request_hash: Option<&str>,
81        now_ms: u64,
82    ) -> Result<(), PeerRpcError>;
83}
84
85/// Test-only authenticator that accepts every request.
86#[cfg(any(test, feature = "insecure-testing"))]
87#[derive(Debug, Clone, Copy, Default)]
88pub struct AllowPeerAuthenticator;
89
90#[cfg(any(test, feature = "insecure-testing"))]
91impl PeerRpcAuthenticator for AllowPeerAuthenticator {
92    fn authenticate(
93        &self,
94        _token: Option<&str>,
95        _expected_request_hash: Option<&str>,
96        _now_ms: u64,
97    ) -> Result<(), PeerRpcError> {
98        Ok(())
99    }
100}
101
102/// Peer authenticator backed by AppCore's signed local token provider.
103#[derive(Debug, Clone)]
104pub struct HashTokenPeerAuthenticator<P = HashTokenProvider> {
105    provider: P,
106    claims: TokenClaims,
107}
108
109impl<P> HashTokenPeerAuthenticator<P>
110where
111    P: TokenProvider,
112{
113    /// Creates an authenticator and scopes its claims to peer RPC.
114    pub fn new(provider: P, mut claims: TokenClaims) -> Self {
115        claims.salt = "peer".to_string();
116        Self { provider, claims }
117    }
118}
119
120impl<P> PeerRpcAuthenticator for HashTokenPeerAuthenticator<P>
121where
122    P: TokenProvider + Send + Sync,
123{
124    fn authenticate(
125        &self,
126        token: Option<&str>,
127        expected_request_hash: Option<&str>,
128        now_ms: u64,
129    ) -> Result<(), PeerRpcError> {
130        let token = token.ok_or(PeerRpcError::Unauthorized)?;
131        let token = token
132            .strip_prefix("Bearer ")
133            .or_else(|| token.strip_prefix("bearer "))
134            .unwrap_or(token);
135        let claims = CommandTokenValidator::new(&self.provider, self.claims.clone())
136            .validate_and_get_claims(token, "peer", None, now_ms, expected_request_hash)
137            .map_err(peer_auth_error)?;
138        if expected_request_hash.is_some() && claims.request_hash.is_none() {
139            return Err(PeerRpcError::Forbidden);
140        }
141        Ok(())
142    }
143}
144
145impl<P> HashTokenPeerTokenIssuer<P>
146where
147    P: TokenProvider,
148{
149    /// Creates an issuer and scopes its claims to peer RPC.
150    pub fn new(provider: P, mut claims: TokenClaims) -> Self {
151        claims.salt = "peer".to_string();
152        Self { provider, claims }
153    }
154}
155
156impl<P> PeerRpcTokenIssuer for HashTokenPeerTokenIssuer<P>
157where
158    P: TokenProvider + Send + Sync,
159{
160    fn issue_peer_token(
161        &self,
162        request_id: &str,
163        request_hash: Option<&str>,
164        now_ms: u64,
165        ttl_ms: u64,
166    ) -> Result<String, PeerRpcError> {
167        CommandTokenFactory::new(&self.provider, self.claims.clone())
168            .create_v1_with_jti_and_hash(
169                "peer",
170                None,
171                Some("*"),
172                Some(LOCAL_ADMIN_SUBJECT),
173                now_ms,
174                ttl_ms,
175                Some(request_id.to_string()),
176                request_hash.map(ToOwned::to_owned),
177            )
178            .map_err(peer_auth_error)
179    }
180}
181
182#[cfg(any(test, feature = "insecure-testing"))]
183impl StaticPeerRpcTokenIssuer {
184    /// Creates an issuer from an explicitly configured static token.
185    pub fn new(token: impl Into<String>) -> Self {
186        Self {
187            token: token.into().into_bytes(),
188        }
189    }
190}
191
192#[cfg(any(test, feature = "insecure-testing"))]
193impl PeerRpcTokenIssuer for StaticPeerRpcTokenIssuer {
194    fn issue_peer_token(
195        &self,
196        _request_id: &str,
197        _request_hash: Option<&str>,
198        _now_ms: u64,
199        _ttl_ms: u64,
200    ) -> Result<String, PeerRpcError> {
201        String::from_utf8(self.token.clone()).map_err(|_| PeerRpcError::Unauthorized)
202    }
203}
204fn peer_auth_error(error: CommandTokenError) -> PeerRpcError {
205    match error {
206        CommandTokenError::Forbidden => PeerRpcError::Forbidden,
207        CommandTokenError::InvalidFormat | CommandTokenError::Unauthorized => {
208            PeerRpcError::Unauthorized
209        }
210    }
211}