1use ed25519_dalek::{SECRET_KEY_LENGTH, SigningKey, VerifyingKey};
7use rand_core::OsRng;
8use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11
12#[cfg(unix)]
13use std::os::unix::fs::PermissionsExt;
14
15#[derive(Debug, thiserror::Error)]
16pub enum IdentityError {
17 #[error("identity files not found")]
18 NotFound,
19 #[error("io error: {0}")]
20 Io(#[from] io::Error),
21 #[error("invalid key material: {0}")]
22 InvalidKey(String),
23 #[error("multibase decode error: {0}")]
24 Multibase(#[from] multibase::Error),
25}
26
27#[derive(Clone)]
28pub struct AgentIdentity {
29 signing: SigningKey,
30}
31
32impl std::fmt::Debug for AgentIdentity {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("AgentIdentity")
35 .field("verifying_key", &self.signing.verifying_key())
36 .finish()
37 }
38}
39
40impl AgentIdentity {
41 pub fn generate() -> Self {
43 Self {
44 signing: SigningKey::generate(&mut OsRng),
45 }
46 }
47
48 pub fn save(&self, dir: &Path) -> Result<(), IdentityError> {
51 fs::create_dir_all(dir)?;
52 let priv_path = dir.join("identity.key");
53 let pub_path = dir.join("identity.pub");
54
55 fs::write(&priv_path, self.signing.to_bytes())?;
56 #[cfg(unix)]
57 {
58 let mut perms = fs::metadata(&priv_path)?.permissions();
59 perms.set_mode(0o600);
60 fs::set_permissions(&priv_path, perms)?;
61 }
62
63 let pub_text = encode_pubkey(&self.signing.verifying_key());
64 fs::write(&pub_path, pub_text)?;
65 Ok(())
66 }
67
68 pub fn load(dir: &Path) -> Result<Self, IdentityError> {
72 let priv_path = dir.join("identity.key");
73 if !priv_path.exists() {
74 return Err(IdentityError::NotFound);
75 }
76 let bytes = fs::read(&priv_path)?;
77 if bytes.len() != SECRET_KEY_LENGTH {
78 return Err(IdentityError::InvalidKey(format!(
79 "expected {SECRET_KEY_LENGTH} bytes, got {}",
80 bytes.len()
81 )));
82 }
83 let arr: [u8; SECRET_KEY_LENGTH] = bytes.as_slice().try_into().unwrap();
84 let signing = SigningKey::from_bytes(&arr);
85
86 let pub_path = dir.join("identity.pub");
87 if pub_path.exists() {
88 let text = fs::read_to_string(&pub_path)?;
89 let loaded_pub = decode_pubkey(text.trim())?;
90 if loaded_pub != *signing.verifying_key().as_bytes() {
91 return Err(IdentityError::InvalidKey(
92 "identity.pub does not match identity.key".into(),
93 ));
94 }
95 }
96
97 Ok(Self { signing })
98 }
99
100 pub fn signing_key(&self) -> &SigningKey {
101 &self.signing
102 }
103
104 pub fn sign_bytes(&self, msg: &[u8]) -> [u8; 64] {
109 use ed25519_dalek::Signer;
110 self.signing.sign(msg).to_bytes()
111 }
112
113 pub fn verifying_key(&self) -> VerifyingKey {
114 self.signing.verifying_key()
115 }
116
117 pub fn verifying_key_bytes(&self) -> [u8; 32] {
118 *self.signing.verifying_key().as_bytes()
119 }
120
121 pub fn pubkey_text(&self) -> String {
122 encode_pubkey(&self.signing.verifying_key())
123 }
124
125 pub fn public_key_multibase(&self) -> String {
129 encode_pubkey(&self.signing.verifying_key())
130 }
131
132 pub fn to_x25519_static_secret(&self) -> x25519_dalek::StaticSecret {
138 let scalar_bytes = self.signing.to_scalar_bytes();
139 x25519_dalek::StaticSecret::from(scalar_bytes)
140 }
141}
142
143pub fn encode_pubkey(key: &VerifyingKey) -> String {
145 multibase::encode(multibase::Base::Base58Btc, key.as_bytes())
146}
147
148pub fn decode_pubkey(text: &str) -> Result<[u8; 32], IdentityError> {
150 let (_base, bytes) = multibase::decode(text)?;
151 if bytes.len() != 32 {
152 return Err(IdentityError::InvalidKey(format!(
153 "pubkey must be 32 bytes, got {}",
154 bytes.len()
155 )));
156 }
157 let mut out = [0u8; 32];
158 out.copy_from_slice(&bytes);
159 Ok(out)
160}
161
162pub fn default_dir(agent_home: &Path) -> PathBuf {
164 agent_home.to_path_buf()
165}
166
167use serde::{Deserialize, Serialize};
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum RotationReason {
179 Scheduled,
180 SuspectCompromise,
181 OwnerChange,
182 Emergency,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct RotationAttestation {
194 pub schema: u32,
196 pub uuid: String,
198 pub algorithm: String,
200 pub old_pubkey: String,
202 pub new_pubkey: String,
204 pub old_key_version: u32,
205 pub new_key_version: u32,
207 pub rotated_at: String,
209 pub reason: RotationReason,
210 #[serde(default, skip_serializing_if = "String::is_empty")]
213 pub signature: String,
214 #[serde(default, skip_serializing_if = "is_false")]
216 pub bootstrap: bool,
217}
218
219fn is_false(b: &bool) -> bool {
220 !*b
221}
222
223impl RotationAttestation {
224 pub fn new(
226 uuid: impl Into<String>,
227 old_pubkey: impl Into<String>,
228 new_pubkey: impl Into<String>,
229 old_key_version: u32,
230 new_key_version: u32,
231 rotated_at: impl Into<String>,
232 reason: RotationReason,
233 ) -> Self {
234 Self {
235 schema: 1,
236 uuid: uuid.into(),
237 algorithm: "ed25519".into(),
238 old_pubkey: old_pubkey.into(),
239 new_pubkey: new_pubkey.into(),
240 old_key_version,
241 new_key_version,
242 rotated_at: rotated_at.into(),
243 reason,
244 signature: String::new(),
245 bootstrap: false,
246 }
247 }
248
249 pub fn into_bootstrap(mut self) -> Self {
253 self.bootstrap = true;
254 self.old_pubkey = String::new();
255 self.signature = String::new();
256 self
257 }
258
259 pub fn canonical_bytes(&self) -> Vec<u8> {
263 let mut clone = self.clone();
264 clone.signature = String::new();
265 canonical_json(&clone)
266 }
267
268 pub fn sign(&mut self, signing: &ed25519_dalek::SigningKey) {
271 use ed25519_dalek::Signer;
272 let sig = signing.sign(&self.canonical_bytes());
273 self.signature = multibase::encode(multibase::Base::Base58Btc, sig.to_bytes());
274 }
275
276 pub fn verify(&self, old_pubkey: &str) -> Result<(), IdentityError> {
285 if self.bootstrap {
286 return Ok(());
287 }
288 if self.signature.is_empty() {
289 return Err(IdentityError::InvalidKey(
290 "attestation signature is empty".into(),
291 ));
292 }
293 let pub_bytes = decode_pubkey(old_pubkey)?;
294 let verifying = ed25519_dalek::VerifyingKey::from_bytes(&pub_bytes)
295 .map_err(|e| IdentityError::InvalidKey(format!("verifying key: {e}")))?;
296 let (_base, sig_bytes) = multibase::decode(&self.signature)?;
297 let sig_arr: [u8; 64] = sig_bytes
298 .as_slice()
299 .try_into()
300 .map_err(|_| IdentityError::InvalidKey("signature length != 64".into()))?;
301 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
302 verifying
303 .verify_strict(&self.canonical_bytes(), &sig)
304 .map_err(|e| IdentityError::InvalidKey(format!("signature: {e}")))?;
305 Ok(())
306 }
307
308 pub fn verify_or_emergency(&self, old_pubkey: &str) -> Result<(), IdentityError> {
311 if self.reason == RotationReason::Emergency && self.signature.is_empty() {
312 return Ok(());
313 }
314 self.verify(old_pubkey)
315 }
316}
317
318#[derive(Debug, Clone, Copy, Default)]
324pub struct ChainOptions {
325 pub allow_emergency: bool,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct ChainOutcome {
335 pub head_key_version: u32,
337 pub head_pubkey: String,
339 pub length: usize,
341}
342
343#[derive(Debug)]
345pub enum ChainError {
346 MissingBootstrap,
348 VersionSkip { expected: u32, got: u32 },
350 PubkeyDiscontinuity { at_version: u32 },
352 DuplicateVersion(u32),
354 BadSignature { at_version: u32, detail: String },
356 EmergencyDisallowed { at_version: u32 },
358}
359
360impl std::fmt::Display for ChainError {
361 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362 match self {
363 Self::MissingBootstrap => {
364 write!(
365 f,
366 "chain must start with a bootstrap entry (bootstrap=true, key_version=0)"
367 )
368 }
369 Self::VersionSkip { expected, got } => {
370 write!(f, "version skip: expected {expected}, got {got}")
371 }
372 Self::PubkeyDiscontinuity { at_version } => {
373 write!(
374 f,
375 "pubkey discontinuity at key_version {at_version}: old_pubkey does not match prior new_pubkey"
376 )
377 }
378 Self::DuplicateVersion(v) => write!(f, "duplicate key_version {v}"),
379 Self::BadSignature { at_version, detail } => {
380 write!(f, "bad signature at key_version {at_version}: {detail}")
381 }
382 Self::EmergencyDisallowed { at_version } => {
383 write!(
384 f,
385 "emergency attestation at key_version {at_version} requires allow_emergency=true"
386 )
387 }
388 }
389 }
390}
391
392impl std::error::Error for ChainError {}
393
394pub fn verify_chain(
397 chain: &[RotationAttestation],
398 opts: ChainOptions,
399) -> std::result::Result<ChainOutcome, ChainError> {
400 if chain.is_empty() {
401 return Err(ChainError::MissingBootstrap);
402 }
403 let first = &chain[0];
404 if !first.bootstrap || first.new_key_version != 0 {
405 return Err(ChainError::MissingBootstrap);
406 }
407
408 let mut prev_pubkey = first.new_pubkey.clone();
409 let mut prev_version = 0u32;
410 let mut seen_versions = std::collections::HashSet::new();
411 seen_versions.insert(0u32);
412
413 for (i, a) in chain.iter().enumerate().skip(1) {
414 if !seen_versions.insert(a.new_key_version) {
416 return Err(ChainError::DuplicateVersion(a.new_key_version));
417 }
418 let expected = prev_version + 1;
420 if a.old_key_version != prev_version || a.new_key_version != expected {
421 return Err(ChainError::VersionSkip {
422 expected,
423 got: a.new_key_version,
424 });
425 }
426 if a.old_pubkey != prev_pubkey {
428 return Err(ChainError::PubkeyDiscontinuity {
429 at_version: a.new_key_version,
430 });
431 }
432 if a.reason == RotationReason::Emergency {
434 if !opts.allow_emergency {
435 return Err(ChainError::EmergencyDisallowed {
436 at_version: a.new_key_version,
437 });
438 }
439 if let Err(e) = a.verify_or_emergency(&a.old_pubkey) {
441 return Err(ChainError::BadSignature {
442 at_version: a.new_key_version,
443 detail: e.to_string(),
444 });
445 }
446 } else if let Err(e) = a.verify(&a.old_pubkey) {
447 return Err(ChainError::BadSignature {
448 at_version: a.new_key_version,
449 detail: e.to_string(),
450 });
451 }
452
453 prev_pubkey = a.new_pubkey.clone();
454 prev_version = a.new_key_version;
455 let _ = i; }
457
458 Ok(ChainOutcome {
459 head_key_version: prev_version,
460 head_pubkey: prev_pubkey,
461 length: chain.len(),
462 })
463}
464
465fn canonical_json<T: serde::Serialize>(value: &T) -> Vec<u8> {
469 let v: serde_json::Value =
472 serde_json::to_value(value).expect("serialize should not fail for our types");
473 let mut out = Vec::new();
474 write_canonical(&mut out, &v);
475 out
476}
477
478fn write_canonical(out: &mut Vec<u8>, v: &serde_json::Value) {
479 use serde_json::Value;
480 match v {
481 Value::Null => out.extend_from_slice(b"null"),
482 Value::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }),
483 Value::Number(n) => out.extend_from_slice(n.to_string().as_bytes()),
484 Value::String(s) => {
485 let escaped = serde_json::to_string(s).unwrap();
487 out.extend_from_slice(escaped.as_bytes());
488 }
489 Value::Array(arr) => {
490 out.push(b'[');
491 for (i, item) in arr.iter().enumerate() {
492 if i > 0 {
493 out.push(b',');
494 }
495 write_canonical(out, item);
496 }
497 out.push(b']');
498 }
499 Value::Object(map) => {
500 let mut keys: Vec<&String> = map.keys().collect();
502 keys.sort();
503 out.push(b'{');
504 for (i, k) in keys.iter().enumerate() {
505 if i > 0 {
506 out.push(b',');
507 }
508 let kesc = serde_json::to_string(k).unwrap();
509 out.extend_from_slice(kesc.as_bytes());
510 out.push(b':');
511 write_canonical(out, &map[*k]);
512 }
513 out.push(b'}');
514 }
515 }
516}