Skip to main content

agentid_core/
server.rs

1//! Optional gRPC server for centralised AgentID management.
2//!
3//! Wraps a [`Vault`] and exposes mint/verify/list RPCs. The server only
4//! holds private keys decrypted in memory while servicing a request — the
5//! vault password is supplied at startup (typically via
6//! `AGENTID_VAULT_PASSWORD`) and held in a [`zeroize`]-on-drop wrapper.
7
8use crate::token::{verify as verify_token, TokenBuilder};
9use crate::vault::{Vault, VaultError};
10use std::net::SocketAddr;
11use std::sync::Arc;
12use tonic::{transport::Server, Request, Response, Status};
13use zeroize::Zeroizing;
14
15pub mod proto {
16    tonic::include_proto!("agentid.v1");
17}
18
19use proto::agent_id_service_server::{AgentIdService, AgentIdServiceServer};
20use proto::*;
21
22pub struct AgentIdImpl {
23    vault: Arc<Vault>,
24    password: Arc<Zeroizing<String>>,
25}
26
27impl AgentIdImpl {
28    pub fn new(vault: Vault, password: String) -> Self {
29        Self {
30            vault: Arc::new(vault),
31            password: Arc::new(Zeroizing::new(password)),
32        }
33    }
34}
35
36#[tonic::async_trait]
37impl AgentIdService for AgentIdImpl {
38    async fn mint_token(
39        &self,
40        req: Request<MintTokenRequest>,
41    ) -> Result<Response<MintTokenResponse>, Status> {
42        let r = req.into_inner();
43        let identity = self
44            .vault
45            .load(&r.fingerprint, self.password.as_str())
46            .map_err(map_vault_err)?;
47        let token = TokenBuilder::new(&identity)
48            .scopes(r.scopes)
49            .ttl_seconds(if r.ttl_seconds == 0 { 900 } else { r.ttl_seconds })
50            .max_calls(r.max_calls)
51            .build()
52            .map_err(|e| Status::invalid_argument(format!("mint failed: {e}")))?;
53        Ok(Response::new(MintTokenResponse {
54            token,
55            fingerprint: identity.fingerprint(),
56        }))
57    }
58
59    async fn verify_token(
60        &self,
61        req: Request<VerifyTokenRequest>,
62    ) -> Result<Response<VerifyTokenResponse>, Status> {
63        let r = req.into_inner();
64        let expected_pk = match r.expected_pubkey.len() {
65            0 => None,
66            32 => {
67                let mut a = [0u8; 32];
68                a.copy_from_slice(&r.expected_pubkey);
69                Some(a)
70            }
71            n => return Err(Status::invalid_argument(format!("expected_pubkey must be 32 bytes, got {n}"))),
72        };
73        match verify_token(&r.token, expected_pk.as_ref()) {
74            Ok(claims) => Ok(Response::new(VerifyTokenResponse {
75                valid: true,
76                error: String::new(),
77                name: claims.name.clone(),
78                project: claims.project.clone(),
79                scopes: claims.scopes.clone(),
80                issued_at: claims.issued_at,
81                expires_at: claims.expires_at,
82                max_calls: claims.max_calls,
83                issuer: claims.issuer.to_vec(),
84                fingerprint: claims.fingerprint(),
85            })),
86            Err(e) => Ok(Response::new(VerifyTokenResponse {
87                valid: false,
88                error: format!("{e}"),
89                ..Default::default()
90            })),
91        }
92    }
93
94    async fn list_identities(
95        &self,
96        _req: Request<ListIdentitiesRequest>,
97    ) -> Result<Response<ListIdentitiesResponse>, Status> {
98        let entries = self.vault.list().map_err(map_vault_err)?;
99        let identities = entries
100            .into_iter()
101            .map(|e| Identity {
102                name: e.name,
103                project: e.project,
104                fingerprint: e.fingerprint,
105                public_key: e.public_key,
106                created_at: e.created_at,
107            })
108            .collect();
109        Ok(Response::new(ListIdentitiesResponse { identities }))
110    }
111
112    async fn health(
113        &self,
114        _req: Request<HealthRequest>,
115    ) -> Result<Response<HealthResponse>, Status> {
116        Ok(Response::new(HealthResponse {
117            status: "ok".into(),
118            version: crate::VERSION.into(),
119        }))
120    }
121}
122
123fn map_vault_err(e: VaultError) -> Status {
124    match e {
125        VaultError::NotFound(s) => Status::not_found(s),
126        VaultError::DecryptionFailed => Status::permission_denied("vault password incorrect"),
127        VaultError::NotInitialized(p) => {
128            Status::failed_precondition(format!("vault not initialized at {}", p.display()))
129        }
130        e => Status::internal(format!("{e}")),
131    }
132}
133
134/// Bind and serve the AgentID gRPC service on `addr` until the future is
135/// dropped. The `password` is held in memory as long as the server is alive.
136pub async fn serve(
137    addr: SocketAddr,
138    vault: Vault,
139    password: String,
140) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
141    let svc = AgentIdImpl::new(vault, password);
142    eprintln!(
143        "agentid-server v{} listening on {addr}",
144        crate::VERSION
145    );
146    Server::builder()
147        .add_service(AgentIdServiceServer::new(svc))
148        .serve(addr)
149        .await?;
150    Ok(())
151}