volt_client_grpc/
credential.rs1use crate::config::{CredentialCache, VoltClientConfig, VoltConfig};
6use crate::constants::AUTH_TOKEN_NAME;
7use crate::crypto::{fingerprint_from_key, public_key_from_pem, SigningKeyPair};
8use crate::error::{Result, VoltError};
9use tonic::metadata::MetadataMap;
10
11#[derive(Debug, Clone)]
13pub struct IdentityToken {
14 pub token: String,
16 pub metadata: MetadataMap,
18 pub shared_key: Option<crate::crypto::KeyExchange>,
20}
21
22pub struct VoltCredential {
24 config: VoltClientConfig,
25 config_path: Option<String>,
26 persist_cache: bool,
27 key_pair: Option<SigningKeyPair>,
28 volt_public_key: Option<String>,
29 volt_fingerprint: Option<String>,
30}
31
32impl VoltCredential {
33 pub fn new(config: VoltClientConfig, config_path: Option<String>) -> Result<Self> {
35 let persist_cache = config_path.is_some();
36
37 let (volt_public_key, volt_fingerprint) = Self::extract_volt_key(&config.volt)?;
39
40 let mut credential = Self {
41 config,
42 config_path,
43 persist_cache,
44 key_pair: None,
45 volt_public_key,
46 volt_fingerprint,
47 };
48
49 if let Some(ref cache) = credential.config.credential {
51 if let Some(ref key_pem) = cache.key {
52 credential.key_pair = Some(SigningKeyPair::from_pem(key_pem)?);
53 }
54 }
55
56 Ok(credential)
57 }
58
59 fn extract_volt_key(volt_config: &VoltConfig) -> Result<(Option<String>, Option<String>)> {
60 if let Some(ref public_key) = volt_config.public_key {
62 let key = public_key_from_pem(public_key)?;
63 let fingerprint = fingerprint_from_key(&key);
64 return Ok((Some(public_key.clone()), Some(fingerprint)));
65 }
66
67 Ok((None, None))
71 }
72
73 pub async fn initialise(&mut self) -> Result<()> {
75 self.create_key().await?;
76 self.save_cache().await?;
77 Ok(())
78 }
79
80 pub async fn create_key(&mut self) -> Result<&SigningKeyPair> {
82 if self.key_pair.is_none() {
83 tracing::debug!("Creating new signing key");
84 let key = SigningKeyPair::generate();
85
86 let cache = self
88 .config
89 .credential
90 .get_or_insert_with(CredentialCache::default);
91 cache.key = Some(key.private_key_pem());
92
93 self.key_pair = Some(key);
94 }
95
96 Ok(self.key_pair.as_ref().unwrap())
97 }
98
99 pub fn get_key(&self) -> Result<&SigningKeyPair> {
101 self.key_pair
102 .as_ref()
103 .ok_or_else(|| VoltError::key("No key available"))
104 }
105
106 pub fn public_key_pem(&self) -> Result<String> {
108 Ok(self.get_key()?.public_key_pem())
109 }
110
111 pub fn is_bound(&self) -> bool {
113 self.config
114 .credential
115 .as_ref()
116 .and_then(|c| c.cert.as_ref())
117 .is_some()
118 }
119
120 pub fn certificate(&self) -> Option<&str> {
122 self.config
123 .credential
124 .as_ref()
125 .and_then(|c| c.cert.as_deref())
126 }
127
128 pub fn session_id(&self) -> Option<&str> {
130 self.config
131 .credential
132 .as_ref()
133 .and_then(|c| c.session_id.as_deref())
134 }
135
136 pub fn identity_did(&self) -> Option<&str> {
138 self.config
139 .credential
140 .as_ref()
141 .and_then(|c| c.identity_did.as_deref())
142 }
143
144 pub fn cache(&self) -> Option<&CredentialCache> {
146 self.config.credential.as_ref()
147 }
148
149 pub fn cache_mut(&mut self) -> &mut CredentialCache {
151 self.config
152 .credential
153 .get_or_insert_with(CredentialCache::default)
154 }
155
156 pub fn volt_public_key(&self) -> Option<&str> {
158 self.volt_public_key.as_deref()
159 }
160
161 pub fn volt_fingerprint(&self) -> Option<&str> {
163 self.volt_fingerprint.as_deref()
164 }
165
166 pub fn config(&self) -> &VoltClientConfig {
168 &self.config
169 }
170
171 pub fn config_mut(&mut self) -> &mut VoltClientConfig {
173 &mut self.config
174 }
175
176 pub fn get_identity_metadata(
178 &self,
179 audience: Option<&str>,
180 relay_public_key: Option<&str>,
181 tunnelling: bool,
182 ttl: u64,
183 ) -> Result<IdentityToken> {
184 let key = self.get_key()?;
185 let session_id = self
186 .session_id()
187 .ok_or_else(|| VoltError::session("No session ID"))?;
188
189 let aud = audience.unwrap_or(&self.config.volt.id);
190 tracing::info!(
191 "Creating JWT: sub={}, aud={}, tunnelling={}",
192 session_id,
193 aud,
194 tunnelling
195 );
196
197 let token = create_identity_token(session_id, key, aud, relay_public_key, tunnelling, ttl)?;
198
199 tracing::info!("JWT full token: {}", token);
200
201 let mut metadata = MetadataMap::new();
202 metadata.insert(
203 AUTH_TOKEN_NAME,
204 token
205 .parse()
206 .map_err(|_| VoltError::internal("Invalid token format"))?,
207 );
208
209 let shared_key = if tunnelling {
211 Some(crate::crypto::KeyExchange::new())
212 } else {
213 None
214 };
215
216 Ok(IdentityToken {
217 token,
218 metadata,
219 shared_key,
220 })
221 }
222
223 pub async fn save_cache(&self) -> Result<VoltClientConfig> {
225 let config = self.config.clone();
226
227 if self.persist_cache {
228 if let Some(ref path) = self.config_path {
229 config.save_to_file(path).await?;
230 }
231 }
232
233 Ok(config)
234 }
235}
236
237fn create_identity_token(
239 session_id: &str,
240 key: &SigningKeyPair,
241 audience: &str,
242 relay_public_key: Option<&str>,
243 tunnelling: bool,
244 ttl: u64,
245) -> Result<String> {
246 use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
247 use serde::{Deserialize, Serialize};
248
249 #[derive(Debug, Serialize, Deserialize)]
250 struct Claims {
251 sub: String,
253 aud: String,
255 iat: u64,
257 exp: u64,
259 #[serde(skip_serializing_if = "Option::is_none")]
261 k: Option<String>,
262 }
263
264 let now = std::time::SystemTime::now()
265 .duration_since(std::time::UNIX_EPOCH)
266 .unwrap()
267 .as_secs();
268
269 let claims = Claims {
271 sub: session_id.to_string(),
272 aud: audience.to_string(),
273 iat: now.saturating_sub(ttl),
274 exp: now + ttl,
275 k: if tunnelling {
276 relay_public_key.map(|k| k.to_string())
277 } else {
278 None
279 },
280 };
281
282 let header = Header::new(Algorithm::EdDSA);
284
285 let pkcs8_pem = key.private_key_pkcs8_pem();
288 let encoding_key =
289 EncodingKey::from_ed_pem(pkcs8_pem.as_bytes()).map_err(VoltError::JwtError)?;
290
291 encode(&header, &claims, &encoding_key).map_err(VoltError::from)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::config::VoltConfig;
298
299 #[tokio::test]
300 async fn test_credential_creation() {
301 let config = VoltClientConfig {
302 client_name: "test".to_string(),
303 volt: VoltConfig {
304 id: "did:volt:test".to_string(),
305 ..Default::default()
306 },
307 ..Default::default()
308 };
309
310 let mut cred = VoltCredential::new(config, None).unwrap();
311 cred.initialise().await.unwrap();
312
313 assert!(cred.key_pair.is_some());
314 assert!(cred.config.credential.is_some());
315 }
316}