1use chrono::{DateTime, Utc};
34use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
35use serde::{Deserialize, Serialize};
36
37use crate::canonical::{
38 canonical_rotation_input, canonical_signing_input, AttestationPreimage,
39 SCHEMA_VERSION_ATTESTATION,
40};
41
42#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
48pub enum VerifyError {
49 #[error("unknown attestation schema_version: found {found}, expected {expected}")]
52 UnknownSchemaVersion {
53 found: u16,
55 expected: u16,
57 },
58 #[error("key_id mismatch: preimage says {preimage}, verifier expected {expected}")]
62 KeyIdMismatch {
63 preimage: String,
65 expected: String,
67 },
68 #[error("ed25519 signature verification failed")]
71 BadSignature,
72 #[error("malformed signature bytes")]
74 MalformedSignature,
75}
76
77pub trait Attestor: Send + Sync {
88 fn sign(&self, signing_input: &[u8]) -> Signature;
96
97 fn key_id(&self) -> &str;
101
102 fn verifying_key(&self) -> VerifyingKey;
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct Attestation {
118 pub key_id: String,
120 #[serde(with = "signature_serde")]
122 pub signature: [u8; 64],
123 pub signed_at: DateTime<Utc>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131pub struct RotationEnvelope {
132 pub schema_version: u16,
136 pub old_pubkey: [u8; 32],
138 pub new_pubkey: [u8; 32],
140 pub signed_at: DateTime<Utc>,
142 #[serde(with = "signature_serde")]
145 pub signature: [u8; 64],
146}
147
148#[must_use]
157pub fn attest(preimage: &AttestationPreimage, attestor: &dyn Attestor) -> Attestation {
158 let bytes = canonical_signing_input(preimage);
159 let sig = attestor.sign(&bytes);
160 Attestation {
161 key_id: preimage.key_id.clone(),
162 signature: sig.to_bytes(),
163 signed_at: preimage.signed_at,
164 }
165}
166
167pub fn verify(
185 preimage: &AttestationPreimage,
186 attestation: &Attestation,
187 public_key: &VerifyingKey,
188 expected_key_id: &str,
189) -> Result<(), VerifyError> {
190 if preimage.schema_version != SCHEMA_VERSION_ATTESTATION {
192 return Err(VerifyError::UnknownSchemaVersion {
193 found: preimage.schema_version,
194 expected: SCHEMA_VERSION_ATTESTATION,
195 });
196 }
197
198 if preimage.key_id != expected_key_id || attestation.key_id != expected_key_id {
202 return Err(VerifyError::KeyIdMismatch {
203 preimage: preimage.key_id.clone(),
204 expected: expected_key_id.to_string(),
205 });
206 }
207
208 if preimage.signed_at != attestation.signed_at {
212 return Err(VerifyError::BadSignature);
213 }
214
215 let signing_input = canonical_signing_input(preimage);
216 let sig = Signature::from_bytes(&attestation.signature);
217 public_key
218 .verify(&signing_input, &sig)
219 .map_err(|_| VerifyError::BadSignature)
220}
221
222#[must_use]
225pub fn sign_rotation(
226 old_pubkey: &VerifyingKey,
227 new_pubkey: &VerifyingKey,
228 signed_at: DateTime<Utc>,
229 attestor: &dyn Attestor,
230) -> RotationEnvelope {
231 let old_bytes = old_pubkey.to_bytes();
232 let new_bytes = new_pubkey.to_bytes();
233 let bytes = canonical_rotation_input(
234 SCHEMA_VERSION_ATTESTATION,
235 &old_bytes,
236 &new_bytes,
237 signed_at,
238 );
239 let sig = attestor.sign(&bytes);
240 RotationEnvelope {
241 schema_version: SCHEMA_VERSION_ATTESTATION,
242 old_pubkey: old_bytes,
243 new_pubkey: new_bytes,
244 signed_at,
245 signature: sig.to_bytes(),
246 }
247}
248
249pub fn verify_rotation(env: &RotationEnvelope) -> Result<(), VerifyError> {
252 if env.schema_version != SCHEMA_VERSION_ATTESTATION {
253 return Err(VerifyError::UnknownSchemaVersion {
254 found: env.schema_version,
255 expected: SCHEMA_VERSION_ATTESTATION,
256 });
257 }
258 let old_pk =
259 VerifyingKey::from_bytes(&env.old_pubkey).map_err(|_| VerifyError::MalformedSignature)?;
260 let bytes = canonical_rotation_input(
261 env.schema_version,
262 &env.old_pubkey,
263 &env.new_pubkey,
264 env.signed_at,
265 );
266 let sig = Signature::from_bytes(&env.signature);
267 old_pk
268 .verify(&bytes, &sig)
269 .map_err(|_| VerifyError::BadSignature)
270}
271
272pub struct InMemoryAttestor {
281 signing_key: SigningKey,
282 key_id: String,
283}
284
285impl std::fmt::Debug for InMemoryAttestor {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 f.debug_struct("InMemoryAttestor")
288 .field("key_id", &self.key_id)
289 .finish_non_exhaustive()
291 }
292}
293
294impl InMemoryAttestor {
295 pub fn from_signing_key(signing_key: SigningKey, key_id: impl Into<String>) -> Self {
297 Self {
298 signing_key,
299 key_id: key_id.into(),
300 }
301 }
302
303 #[must_use]
307 pub fn from_seed(seed: &[u8; 32]) -> Self {
308 let signing_key = SigningKey::from_bytes(seed);
309 let pk = signing_key.verifying_key();
310 let key_id = hex_lower(&pk.to_bytes());
311 Self {
312 signing_key,
313 key_id,
314 }
315 }
316}
317
318impl Attestor for InMemoryAttestor {
319 fn sign(&self, signing_input: &[u8]) -> Signature {
320 self.signing_key.sign(signing_input)
321 }
322
323 fn key_id(&self) -> &str {
324 &self.key_id
325 }
326
327 fn verifying_key(&self) -> VerifyingKey {
328 self.signing_key.verifying_key()
329 }
330}
331
332fn hex_lower(bytes: &[u8]) -> String {
335 let mut s = String::with_capacity(bytes.len() * 2);
336 for b in bytes {
337 s.push_str(&format!("{b:02x}"));
338 }
339 s
340}
341
342pub trait IdentityRotation: Attestor {
349 fn sign_rotation(&self, new_pubkey: &VerifyingKey, signed_at: DateTime<Utc>) -> RotationEnvelope
352 where
353 Self: Sized,
354 {
355 sign_rotation(&self.verifying_key(), new_pubkey, signed_at, self)
356 }
357}
358
359impl IdentityRotation for InMemoryAttestor {}
360
361#[cfg(target_os = "macos")]
364#[derive(Debug)]
365pub struct KeychainAttestor {
366 key_id: String,
367}
368
369#[cfg(target_os = "macos")]
370impl KeychainAttestor {
371 pub fn open(_key_id: impl Into<String>) -> Self {
374 unimplemented!(
375 "KeychainAttestor (macOS) not implemented in v0; use InMemoryAttestor (T-3.D.5/6)"
376 );
377 }
378
379 #[must_use]
382 pub fn key_id(&self) -> &str {
383 &self.key_id
384 }
385}
386
387#[cfg(target_os = "macos")]
388impl Attestor for KeychainAttestor {
389 fn sign(&self, _signing_input: &[u8]) -> Signature {
390 unimplemented!("KeychainAttestor::sign (macOS) — see T-3.D.5/6")
391 }
392 fn key_id(&self) -> &str {
393 &self.key_id
394 }
395 fn verifying_key(&self) -> VerifyingKey {
396 unimplemented!("KeychainAttestor::verifying_key (macOS) — see T-3.D.5/6")
397 }
398}
399
400#[cfg(target_os = "linux")]
402#[derive(Debug)]
403pub struct KeychainAttestor {
404 key_id: String,
405}
406
407#[cfg(target_os = "linux")]
408impl KeychainAttestor {
409 pub fn open(_key_id: impl Into<String>) -> Self {
412 unimplemented!(
413 "KeychainAttestor (Linux) not implemented in v0; use InMemoryAttestor (T-3.D.5/6)"
414 );
415 }
416
417 #[must_use]
419 pub fn key_id(&self) -> &str {
420 &self.key_id
421 }
422}
423
424#[cfg(target_os = "linux")]
425impl Attestor for KeychainAttestor {
426 fn sign(&self, _signing_input: &[u8]) -> Signature {
427 unimplemented!("KeychainAttestor::sign (Linux) — see T-3.D.5/6")
428 }
429 fn key_id(&self) -> &str {
430 &self.key_id
431 }
432 fn verifying_key(&self) -> VerifyingKey {
433 unimplemented!("KeychainAttestor::verifying_key (Linux) — see T-3.D.5/6")
434 }
435}
436
437#[cfg(target_os = "windows")]
439#[derive(Debug)]
440pub struct KeychainAttestor {
441 key_id: String,
442}
443
444#[cfg(target_os = "windows")]
445impl KeychainAttestor {
446 pub fn open(_key_id: impl Into<String>) -> Self {
449 unimplemented!(
450 "KeychainAttestor (Windows) not implemented in v0; use InMemoryAttestor (T-3.D.5/6)"
451 );
452 }
453
454 #[must_use]
456 pub fn key_id(&self) -> &str {
457 &self.key_id
458 }
459}
460
461#[cfg(target_os = "windows")]
462impl Attestor for KeychainAttestor {
463 fn sign(&self, _signing_input: &[u8]) -> Signature {
464 unimplemented!("KeychainAttestor::sign (Windows) — see T-3.D.5/6")
465 }
466 fn key_id(&self) -> &str {
467 &self.key_id
468 }
469 fn verifying_key(&self) -> VerifyingKey {
470 unimplemented!("KeychainAttestor::verifying_key (Windows) — see T-3.D.5/6")
471 }
472}
473
474mod signature_serde {
477 use serde::{Deserialize, Deserializer, Serializer};
478
479 pub fn serialize<S: Serializer>(bytes: &[u8; 64], s: S) -> Result<S::Ok, S::Error> {
480 s.serialize_bytes(bytes)
481 }
482
483 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> {
484 let v: Vec<u8> = Vec::deserialize(d)?;
485 if v.len() != 64 {
486 return Err(serde::de::Error::custom(format!(
487 "expected 64 signature bytes, got {}",
488 v.len()
489 )));
490 }
491 let mut out = [0u8; 64];
492 out.copy_from_slice(&v);
493 Ok(out)
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::canonical::{LineageBinding, SourceIdentity};
501 use chrono::TimeZone;
502 use ed25519_dalek::SigningKey;
503 use std::sync::atomic::{AtomicU8, Ordering};
504
505 static SEED_COUNTER: AtomicU8 = AtomicU8::new(1);
509 fn fresh_attestor() -> InMemoryAttestor {
510 let n = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
511 let seed = [n; 32];
512 InMemoryAttestor::from_seed(&seed)
513 }
514
515 fn fixture_preimage(attestor: &InMemoryAttestor) -> AttestationPreimage {
516 AttestationPreimage {
517 schema_version: SCHEMA_VERSION_ATTESTATION,
518 source: SourceIdentity::User,
519 event_id: "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".into(),
520 payload_hash: "deadbeef".into(),
521 session_id: "session-001".into(),
522 ledger_id: "ledger-main".into(),
523 lineage: LineageBinding::PreviousHash("aaaaaaaaaaaaaaaa".into()),
524 signed_at: Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap(),
525 key_id: attestor.key_id().to_string(),
526 }
527 }
528
529 #[test]
532 fn unknown_schema_version_fails_closed() {
533 let attestor = fresh_attestor();
534 let mut p = fixture_preimage(&attestor);
535 let att = attest(&p, &attestor);
536
537 p.schema_version = 999;
540
541 let result = verify(&p, &att, &attestor.verifying_key(), attestor.key_id());
542 match result {
543 Err(VerifyError::UnknownSchemaVersion {
544 found: 999,
545 expected: SCHEMA_VERSION_ATTESTATION,
546 }) => {}
547 other => panic!("expected UnknownSchemaVersion, got {other:?}"),
548 }
549 }
550
551 #[test]
556 fn field_reorder_does_not_change_signed_semantics() {
557 let attestor = fresh_attestor();
558 let p1 = fixture_preimage(&attestor);
559 let att = attest(&p1, &attestor);
560
561 let p2 = AttestationPreimage {
565 key_id: attestor.key_id().to_string(),
566 signed_at: chrono::Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap(),
567 lineage: LineageBinding::PreviousHash("aaaaaaaaaaaaaaaa".into()),
568 ledger_id: "ledger-main".into(),
569 session_id: "session-001".into(),
570 payload_hash: "deadbeef".into(),
571 event_id: "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".into(),
572 source: SourceIdentity::User,
573 schema_version: SCHEMA_VERSION_ATTESTATION,
574 };
575
576 verify(&p2, &att, &attestor.verifying_key(), attestor.key_id())
577 .expect("captured sig must verify under reordered preimage struct literal");
578 }
579
580 #[test]
582 fn wrong_prev_signature_fails() {
583 let attestor = fresh_attestor();
584 let p = fixture_preimage(&attestor);
585 let att = attest(&p, &attestor);
586
587 let mut tampered = p.clone();
588 tampered.lineage = LineageBinding::PreviousHash("bbbbbbbbbbbbbbbb".into());
589
590 let result = verify(
591 &tampered,
592 &att,
593 &attestor.verifying_key(),
594 attestor.key_id(),
595 );
596 assert_eq!(result, Err(VerifyError::BadSignature));
597 }
598
599 #[test]
602 fn replay_stale_row_after_chain_advance_fails() {
603 let attestor = fresh_attestor();
604
605 let mut p_old = fixture_preimage(&attestor);
606 p_old.lineage = LineageBinding::ChainPosition(10);
607 let att = attest(&p_old, &attestor);
608
609 let mut p_new = p_old.clone();
612 p_new.lineage = LineageBinding::ChainPosition(20);
613
614 let result = verify(&p_new, &att, &attestor.verifying_key(), attestor.key_id());
615 assert_eq!(result, Err(VerifyError::BadSignature));
616 }
617
618 #[test]
621 fn identity_rotate_accepted_when_envelope_verifies() {
622 let old = fresh_attestor();
623 let new = fresh_attestor();
624 let signed_at = Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap();
625
626 let env = sign_rotation(&old.verifying_key(), &new.verifying_key(), signed_at, &old);
627
628 verify_rotation(&env).expect("envelope signed by old key must verify");
629 }
630
631 #[test]
634 fn identity_rotate_tampered_envelope_fails() {
635 let old = fresh_attestor();
636 let new = fresh_attestor();
637 let attacker_new = fresh_attestor();
638 let signed_at = Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap();
639
640 let mut env = sign_rotation(&old.verifying_key(), &new.verifying_key(), signed_at, &old);
641
642 env.new_pubkey = attacker_new.verifying_key().to_bytes();
645
646 assert_eq!(verify_rotation(&env), Err(VerifyError::BadSignature));
647 }
648
649 #[test]
652 fn rotation_envelope_unknown_schema_version_fails_closed() {
653 let old = fresh_attestor();
654 let new = fresh_attestor();
655 let signed_at = Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap();
656 let mut env = sign_rotation(&old.verifying_key(), &new.verifying_key(), signed_at, &old);
657 env.schema_version = 999;
658 match verify_rotation(&env) {
659 Err(VerifyError::UnknownSchemaVersion {
660 found: 999,
661 expected: SCHEMA_VERSION_ATTESTATION,
662 }) => {}
663 other => panic!("expected UnknownSchemaVersion, got {other:?}"),
664 }
665 }
666
667 #[test]
671 fn malformed_payload_fails_closed() {
672 let attestor = fresh_attestor();
673 let p = fixture_preimage(&attestor);
674 let mut att = attest(&p, &attestor);
675 for byte in att.signature.iter_mut().take(8) {
678 *byte = 0;
679 }
680 let result = verify(&p, &att, &attestor.verifying_key(), attestor.key_id());
681 assert_eq!(result, Err(VerifyError::BadSignature));
682 }
683
684 #[test]
687 fn key_id_mismatch_fails_closed() {
688 let attestor = fresh_attestor();
689 let p = fixture_preimage(&attestor);
690 let att = attest(&p, &attestor);
691 let result = verify(&p, &att, &attestor.verifying_key(), "fp:wrong-key");
692 match result {
693 Err(VerifyError::KeyIdMismatch { .. }) => {}
694 other => panic!("expected KeyIdMismatch, got {other:?}"),
695 }
696 }
697
698 #[test]
700 fn fresh_attestation_verifies() {
701 let attestor = fresh_attestor();
702 let p = fixture_preimage(&attestor);
703 let att = attest(&p, &attestor);
704 verify(&p, &att, &attestor.verifying_key(), attestor.key_id())
705 .expect("freshly-signed attestation must verify");
706 }
707
708 #[test]
711 fn cross_source_replay_fails() {
712 let attestor = fresh_attestor();
713 let p = fixture_preimage(&attestor);
714 let att = attest(&p, &attestor);
715
716 let mut p2 = p.clone();
717 p2.source = SourceIdentity::Tool {
718 name: "user".into(),
719 };
720 let result = verify(&p2, &att, &attestor.verifying_key(), attestor.key_id());
721 assert_eq!(result, Err(VerifyError::BadSignature));
722 }
723
724 #[test]
727 fn different_key_does_not_verify() {
728 let a1 = fresh_attestor();
729 let p = fixture_preimage(&a1);
730 let att = attest(&p, &a1);
731
732 let other = (2u8..=u8::MAX)
737 .map(|n| SigningKey::from_bytes(&[n; 32]).verifying_key())
738 .find(|candidate| candidate != &a1.verifying_key())
739 .expect("finite seed range must contain a distinct test key");
740 assert_eq!(
741 verify(&p, &att, &other, a1.key_id()),
742 Err(VerifyError::BadSignature)
743 );
744 }
745}