1use std::{marker::PhantomData, sync::Arc};
14
15use sha2::{Digest as _, Sha256};
16
17use crate::{Signer, verify};
18
19mod private {
20 pub trait Sealed {}
21}
22
23pub trait SigningRole: private::Sealed + Send + Sync + 'static {
25 const ISSUER: &'static str;
27}
28
29macro_rules! role {
30 ($(#[$meta:meta])* $name:ident, $issuer:literal) => {
31 $(#[$meta])*
32 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33 pub struct $name;
34 impl private::Sealed for $name {}
35 impl SigningRole for $name {
36 const ISSUER: &'static str = $issuer;
37 }
38 };
39}
40
41role!(
42 ApprovalRole,
44 "polychrome.control.approval"
45);
46role!(
47 SessionRole,
49 "polychrome.control.session"
50);
51role!(
52 TurnReadRole,
54 "polychrome.control.turn-read"
55);
56role!(
57 WebSessionGrantRole,
59 "polychrome.control.web-session-grant"
60);
61role!(
62 JournalAttestationRole,
64 "polychrome.state.journal-attestation"
65);
66role!(
67 HandoffRole,
69 "polychrome.control.handoff"
70);
71role!(
72 SubagentRole,
74 "polychrome.control.subagent"
75);
76
77pub const CONTROL_APPROVAL_KEY_REF: &str = "control-plane/approval-signer";
79pub const CONTROL_APPROVAL_HISTORY_REF: &str = "control-plane/approval-signer-history";
81pub const CONTROL_SESSION_KEY_REF: &str = "control-plane/session-signer";
83pub const CONTROL_SESSION_HISTORY_REF: &str = "control-plane/session-signer-history";
85pub const CONTROL_TURN_READ_KEY_REF: &str = "control-plane/turn-read-signer";
87pub const CONTROL_TURN_READ_HISTORY_REF: &str = "control-plane/turn-read-signer-history";
89pub const CONTROL_WEB_SESSION_GRANT_KEY_REF: &str = "control-plane/web-session-grant-signer";
91pub const CONTROL_WEB_SESSION_GRANT_HISTORY_REF: &str =
93 "control-plane/web-session-grant-signer-history";
94pub const CONTROL_MEMORY_JOURNAL_KEY_REF: &str = "control-plane/memory-journal-attestation-signer";
96pub const CONTROL_MEMORY_JOURNAL_HISTORY_REF: &str =
98 "control-plane/memory-journal-attestation-signer-history";
99pub const CONTROL_HANDOFF_KEY_REF: &str = "control-plane/handoff-signer";
101pub const CONTROL_HANDOFF_HISTORY_REF: &str = "control-plane/handoff-signer-history";
103pub const CONTROL_SUBAGENT_KEY_REF: &str = "control-plane/subagent-signer";
105pub const CONTROL_SUBAGENT_HISTORY_REF: &str = "control-plane/subagent-signer-history";
107
108#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct SigningKeyIdentity {
112 issuer: String,
113 key_id: String,
114 public_key: Vec<u8>,
115}
116
117impl SigningKeyIdentity {
118 pub fn for_public_key<R: SigningRole>(
124 public_key: Vec<u8>,
125 ) -> Result<Self, SigningIdentityError> {
126 if public_key.len() != 32 {
127 return Err(SigningIdentityError::InvalidPublicKey);
128 }
129 Ok(Self {
130 issuer: R::ISSUER.to_owned(),
131 key_id: key_id(R::ISSUER, &public_key),
132 public_key,
133 })
134 }
135
136 pub fn checked<R: SigningRole>(
143 issuer: impl Into<String>,
144 claimed_key_id: impl Into<String>,
145 public_key: Vec<u8>,
146 ) -> Result<Self, SigningIdentityError> {
147 let identity = Self {
148 issuer: issuer.into(),
149 key_id: claimed_key_id.into(),
150 public_key,
151 };
152 if identity.issuer != R::ISSUER {
153 return Err(SigningIdentityError::WrongIssuer);
154 }
155 if identity.public_key.len() != 32 {
156 return Err(SigningIdentityError::InvalidPublicKey);
157 }
158 if identity.key_id != key_id(R::ISSUER, &identity.public_key) {
159 return Err(SigningIdentityError::WrongKeyId);
160 }
161 Ok(identity)
162 }
163
164 #[must_use]
166 pub fn issuer(&self) -> &str {
167 &self.issuer
168 }
169
170 #[must_use]
172 pub fn key_id(&self) -> &str {
173 &self.key_id
174 }
175
176 #[must_use]
178 pub fn public_key(&self) -> &[u8] {
179 &self.public_key
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
185pub enum SigningIdentityError {
186 #[error("signing-key issuer does not match its role")]
188 WrongIssuer,
189 #[error("signing public key is not an encoded ed25519 key")]
191 InvalidPublicKey,
192 #[error("signing key id does not match its issuer and public key")]
194 WrongKeyId,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
203pub enum SignatureVerdict {
204 Verified,
206 Invalid,
208 Untrusted,
210}
211
212impl SignatureVerdict {
213 #[must_use]
215 pub const fn as_str(self) -> &'static str {
216 match self {
217 Self::Verified => "verified",
218 Self::Invalid => "invalid",
219 Self::Untrusted => "untrusted",
220 }
221 }
222}
223
224impl std::fmt::Display for SignatureVerdict {
225 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 formatter.write_str(self.as_str())
227 }
228}
229
230pub struct RoleSigner<R: SigningRole> {
232 inner: Arc<Signer>,
233 role: PhantomData<R>,
234}
235
236impl<R: SigningRole> Clone for RoleSigner<R> {
237 fn clone(&self) -> Self {
238 Self {
239 inner: Arc::clone(&self.inner),
240 role: PhantomData,
241 }
242 }
243}
244
245impl<R: SigningRole> std::fmt::Debug for RoleSigner<R> {
246 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 formatter
248 .debug_struct("RoleSigner")
249 .field("identity", &self.identity())
250 .finish_non_exhaustive()
251 }
252}
253
254impl<R: SigningRole> RoleSigner<R> {
255 #[cfg(any(test, feature = "test-util"))]
257 #[must_use]
258 pub fn from_seed(seed: u64) -> Self {
259 Self {
260 inner: Arc::new(Signer::from_seed(seed)),
261 role: PhantomData,
262 }
263 }
264
265 pub fn from_key_bytes(bytes: &[u8]) -> Result<Self, crate::SignerError> {
271 Ok(Self {
272 inner: Arc::new(Signer::from_key_bytes(bytes)?),
273 role: PhantomData,
274 })
275 }
276
277 #[must_use]
279 pub fn public_key_bytes(&self) -> Vec<u8> {
280 self.inner.public_key_bytes()
281 }
282
283 #[must_use]
285 pub fn identity(&self) -> SigningKeyIdentity {
286 let public_key = self.public_key_bytes();
287 SigningKeyIdentity {
288 issuer: R::ISSUER.to_owned(),
289 key_id: key_id(R::ISSUER, &public_key),
290 public_key,
291 }
292 }
293
294 #[must_use]
299 pub(crate) fn sign(&self, canonical_bytes: &[u8]) -> Vec<u8> {
300 self.inner.sign(canonical_bytes)
301 }
302
303 #[must_use]
308 pub(crate) fn as_signer(&self) -> &Signer {
309 &self.inner
310 }
311
312 #[cfg(any(test, feature = "test-util"))]
317 #[must_use]
318 pub fn relabel_for_test<S: SigningRole>(&self) -> RoleSigner<S> {
319 RoleSigner {
320 inner: Arc::clone(&self.inner),
321 role: PhantomData,
322 }
323 }
324}
325
326impl RoleSigner<TurnReadRole> {
327 #[must_use]
332 pub fn sign_turn_read_capability(&self, canonical_bytes: &[u8]) -> Vec<u8> {
333 self.inner
334 .sign(&role_scoped_message::<TurnReadRole>(canonical_bytes))
335 }
336}
337
338impl RoleSigner<JournalAttestationRole> {
339 #[must_use]
344 pub fn sign_journal_root(&self, canonical_bytes: &[u8]) -> Vec<u8> {
345 self.inner
346 .sign(&role_scoped_message::<JournalAttestationRole>(
347 canonical_bytes,
348 ))
349 }
350}
351
352impl RoleSigner<HandoffRole> {
353 #[must_use]
358 pub fn sign_handoff(&self, canonical_bytes: &[u8]) -> Vec<u8> {
359 self.inner.sign(&artifact_scoped_message::<HandoffRole>(
360 HANDOFF_ARTIFACT,
361 canonical_bytes,
362 ))
363 }
364
365 #[must_use]
370 pub fn sign_handoff_denied(&self, canonical_bytes: &[u8]) -> Vec<u8> {
371 self.inner.sign(&artifact_scoped_message::<HandoffRole>(
372 HANDOFF_DENIED_ARTIFACT,
373 canonical_bytes,
374 ))
375 }
376}
377
378impl RoleSigner<SubagentRole> {
379 #[must_use]
384 pub fn sign_subagent_spawn(&self, canonical_bytes: &[u8]) -> Vec<u8> {
385 self.inner.sign(&artifact_scoped_message::<SubagentRole>(
386 SUBAGENT_SPAWN_ARTIFACT,
387 canonical_bytes,
388 ))
389 }
390
391 #[must_use]
396 pub fn sign_subagent_result(&self, canonical_bytes: &[u8]) -> Vec<u8> {
397 self.inner.sign(&artifact_scoped_message::<SubagentRole>(
398 SUBAGENT_RESULT_ARTIFACT,
399 canonical_bytes,
400 ))
401 }
402}
403
404#[derive(Debug, Clone)]
406pub struct RoleTrustSet<R: SigningRole> {
407 keys: Vec<SigningKeyIdentity>,
408 role: PhantomData<R>,
409}
410
411#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
417#[serde(deny_unknown_fields)]
418pub struct SigningKeyHistory {
419 current: SigningKeyIdentity,
420 retired: Vec<SigningKeyIdentity>,
421}
422
423impl SigningKeyHistory {
424 #[must_use]
426 pub fn current<R: SigningRole>(signer: &RoleSigner<R>) -> Self {
427 Self {
428 current: signer.identity(),
429 retired: Vec::new(),
430 }
431 }
432
433 #[must_use]
435 pub const fn current_identity(&self) -> &SigningKeyIdentity {
436 &self.current
437 }
438
439 #[must_use]
441 pub fn retired_identities(&self) -> &[SigningKeyIdentity] {
442 &self.retired
443 }
444
445 pub fn reconcile<R: SigningRole>(
457 &mut self,
458 current: &SigningKeyIdentity,
459 ) -> Result<(RoleTrustSet<R>, bool), SigningIdentityError> {
460 let current = SigningKeyIdentity::checked::<R>(
461 current.issuer.clone(),
462 current.key_id.clone(),
463 current.public_key.clone(),
464 )?;
465 let prior_current = SigningKeyIdentity::checked::<R>(
466 self.current.issuer.clone(),
467 self.current.key_id.clone(),
468 self.current.public_key.clone(),
469 )?;
470 let mut retired = self
471 .retired
472 .iter()
473 .map(|identity| {
474 SigningKeyIdentity::checked::<R>(
475 identity.issuer.clone(),
476 identity.key_id.clone(),
477 identity.public_key.clone(),
478 )
479 })
480 .collect::<Result<Vec<_>, _>>()?;
481 for (index, identity) in retired.iter().enumerate() {
482 if retired[..index]
483 .iter()
484 .any(|prior| prior.key_id == identity.key_id)
485 {
486 return Err(SigningIdentityError::WrongKeyId);
487 }
488 }
489
490 let rotated = prior_current != current;
491 if rotated
492 && !retired
493 .iter()
494 .any(|identity| identity.key_id == prior_current.key_id)
495 {
496 retired.push(prior_current);
497 }
498 self.current = current.clone();
499 self.retired.clone_from(&retired);
500
501 let mut identities = vec![current.clone()];
502 identities.extend(
503 retired
504 .into_iter()
505 .filter(|identity| identity.key_id != current.key_id),
506 );
507 Ok((RoleTrustSet::checked(identities)?, rotated))
508 }
509}
510
511impl<R: SigningRole> RoleTrustSet<R> {
512 pub fn from_public_keys(keys: Vec<Vec<u8>>) -> Result<Self, SigningIdentityError> {
518 let identities = keys
519 .into_iter()
520 .map(SigningKeyIdentity::for_public_key::<R>)
521 .collect::<Result<Vec<_>, _>>()?;
522 Self::checked(identities)
523 }
524
525 pub fn checked(keys: Vec<SigningKeyIdentity>) -> Result<Self, SigningIdentityError> {
531 if keys.is_empty() {
532 return Err(SigningIdentityError::InvalidPublicKey);
533 }
534 let mut checked = Vec::with_capacity(keys.len());
535 for key in keys {
536 let key = SigningKeyIdentity::checked::<R>(key.issuer, key.key_id, key.public_key)?;
537 if checked
538 .iter()
539 .any(|known: &SigningKeyIdentity| known.key_id == key.key_id)
540 {
541 return Err(SigningIdentityError::WrongKeyId);
542 }
543 checked.push(key);
544 }
545 Ok(Self {
546 keys: checked,
547 role: PhantomData,
548 })
549 }
550
551 #[must_use]
553 pub fn current(signer: &RoleSigner<R>) -> Self {
554 Self {
555 keys: vec![signer.identity()],
556 role: PhantomData,
557 }
558 }
559
560 #[must_use]
562 pub fn keys(&self) -> &[SigningKeyIdentity] {
563 &self.keys
564 }
565
566 #[must_use]
568 pub(crate) fn verify(&self, key_id: &str, message: &[u8], signature: &[u8]) -> bool {
569 self.keys
570 .iter()
571 .find(|key| key.key_id == key_id)
572 .is_some_and(|key| verify(&key.public_key, message, signature))
573 }
574
575 fn classify(
582 &self,
583 kind: &str,
584 signed_by: &[u8],
585 canonical_bytes: &[u8],
586 signature: &[u8],
587 ) -> SignatureVerdict {
588 let message = artifact_scoped_message::<R>(kind, canonical_bytes);
589 if !verify(signed_by, &message, signature) {
590 return SignatureVerdict::Invalid;
591 }
592 let Ok(identity) = SigningKeyIdentity::for_public_key::<R>(signed_by.to_vec()) else {
593 return SignatureVerdict::Invalid;
594 };
595 if !self.keys.iter().any(|key| key.key_id == identity.key_id) {
596 return SignatureVerdict::Untrusted;
597 }
598 if self.verify(&identity.key_id, &message, signature) {
599 SignatureVerdict::Verified
600 } else {
601 SignatureVerdict::Invalid
602 }
603 }
604}
605
606impl RoleTrustSet<TurnReadRole> {
607 #[must_use]
609 pub fn verify_turn_read_capability(
610 &self,
611 key_id: &str,
612 canonical_bytes: &[u8],
613 signature: &[u8],
614 ) -> bool {
615 self.verify(
616 key_id,
617 &role_scoped_message::<TurnReadRole>(canonical_bytes),
618 signature,
619 )
620 }
621}
622
623impl RoleTrustSet<JournalAttestationRole> {
624 #[must_use]
626 pub fn verify_journal_root(
627 &self,
628 key_id: &str,
629 canonical_bytes: &[u8],
630 signature: &[u8],
631 ) -> bool {
632 self.verify(
633 key_id,
634 &role_scoped_message::<JournalAttestationRole>(canonical_bytes),
635 signature,
636 )
637 }
638}
639
640impl RoleTrustSet<HandoffRole> {
641 #[must_use]
643 pub fn classify_handoff(
644 &self,
645 signed_by: &[u8],
646 canonical_bytes: &[u8],
647 signature: &[u8],
648 ) -> SignatureVerdict {
649 self.classify(HANDOFF_ARTIFACT, signed_by, canonical_bytes, signature)
650 }
651
652 #[must_use]
654 pub fn classify_handoff_denied(
655 &self,
656 signed_by: &[u8],
657 canonical_bytes: &[u8],
658 signature: &[u8],
659 ) -> SignatureVerdict {
660 self.classify(
661 HANDOFF_DENIED_ARTIFACT,
662 signed_by,
663 canonical_bytes,
664 signature,
665 )
666 }
667}
668
669impl RoleTrustSet<SubagentRole> {
670 #[must_use]
672 pub fn classify_subagent_spawn(
673 &self,
674 signed_by: &[u8],
675 canonical_bytes: &[u8],
676 signature: &[u8],
677 ) -> SignatureVerdict {
678 self.classify(
679 SUBAGENT_SPAWN_ARTIFACT,
680 signed_by,
681 canonical_bytes,
682 signature,
683 )
684 }
685
686 #[must_use]
688 pub fn classify_subagent_result(
689 &self,
690 signed_by: &[u8],
691 canonical_bytes: &[u8],
692 signature: &[u8],
693 ) -> SignatureVerdict {
694 self.classify(
695 SUBAGENT_RESULT_ARTIFACT,
696 signed_by,
697 canonical_bytes,
698 signature,
699 )
700 }
701}
702
703pub type ApprovalSigner = RoleSigner<ApprovalRole>;
705pub type SessionSigner = RoleSigner<SessionRole>;
707pub type TurnReadSigner = RoleSigner<TurnReadRole>;
709pub type WebSessionGrantSigner = RoleSigner<WebSessionGrantRole>;
711pub type JournalAttestationSigner = RoleSigner<JournalAttestationRole>;
713pub type HandoffSigner = RoleSigner<HandoffRole>;
715pub type SubagentSigner = RoleSigner<SubagentRole>;
717
718const HANDOFF_ARTIFACT: &str = "handoff";
720const HANDOFF_DENIED_ARTIFACT: &str = "handoff-denied";
722const SUBAGENT_SPAWN_ARTIFACT: &str = "subagent-spawn";
724const SUBAGENT_RESULT_ARTIFACT: &str = "subagent-result";
726
727fn key_id(issuer: &str, public_key: &[u8]) -> String {
728 let mut hash = Sha256::new();
729 hash.update(b"polychrome.signing-key-id.v1\0");
730 hash.update(
731 u64::try_from(issuer.len())
732 .unwrap_or(u64::MAX)
733 .to_be_bytes(),
734 );
735 hash.update(issuer.as_bytes());
736 hash.update(public_key);
737 crate::hex::lower(&hash.finalize())
738}
739
740fn role_scoped_message<R: SigningRole>(canonical_bytes: &[u8]) -> Vec<u8> {
741 let mut message = Vec::with_capacity(40 + R::ISSUER.len() + canonical_bytes.len());
742 message.extend_from_slice(b"polychrome.signing-role.v1\0");
743 message.extend_from_slice(
744 &u64::try_from(R::ISSUER.len())
745 .unwrap_or(u64::MAX)
746 .to_be_bytes(),
747 );
748 message.extend_from_slice(R::ISSUER.as_bytes());
749 message.extend_from_slice(canonical_bytes);
750 message
751}
752
753fn artifact_scoped_message<R: SigningRole>(kind: &str, canonical_bytes: &[u8]) -> Vec<u8> {
759 let mut message = Vec::with_capacity(48 + R::ISSUER.len() + kind.len() + canonical_bytes.len());
760 message.extend_from_slice(b"polychrome.signed-artifact.v1\0");
761 message.extend_from_slice(&length_prefix(R::ISSUER.len()));
762 message.extend_from_slice(R::ISSUER.as_bytes());
763 message.extend_from_slice(&length_prefix(kind.len()));
764 message.extend_from_slice(kind.as_bytes());
765 message.extend_from_slice(canonical_bytes);
766 message
767}
768
769fn length_prefix(length: usize) -> [u8; 8] {
770 u64::try_from(length).unwrap_or(u64::MAX).to_be_bytes()
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn roles_have_distinct_issuers_and_key_ids() {
779 let approval = ApprovalSigner::from_seed(7);
780 let session = SessionSigner::from_seed(7);
781 assert_ne!(approval.identity().issuer(), session.identity().issuer());
782 assert_ne!(approval.identity().key_id(), session.identity().key_id());
783 assert_eq!(approval.public_key_bytes(), session.public_key_bytes());
784 }
785
786 #[test]
787 fn a_cross_role_identity_is_refused() {
788 let identity = ApprovalSigner::from_seed(7).identity();
789 assert_eq!(
790 SigningKeyIdentity::checked::<SessionRole>(
791 identity.issuer,
792 identity.key_id,
793 identity.public_key,
794 ),
795 Err(SigningIdentityError::WrongIssuer)
796 );
797 }
798
799 #[test]
800 fn role_trust_requires_the_named_key() {
801 let current = SessionSigner::from_seed(7);
802 let retired = SessionSigner::from_seed(8);
803 let trust =
804 RoleTrustSet::<SessionRole>::checked(vec![current.identity(), retired.identity()])
805 .expect("valid role history");
806 let message = b"session";
807 assert!(trust.verify(current.identity().key_id(), message, ¤t.sign(message)));
808 assert!(trust.verify(retired.identity().key_id(), message, &retired.sign(message)));
809 assert!(!trust.verify("unknown", message, ¤t.sign(message)));
810 }
811
812 #[test]
813 fn role_trust_survives_key_cycling_without_cross_role_acceptance() {
814 let first = SessionSigner::from_seed(7);
815 let second = SessionSigner::from_seed(8);
816 let third = SessionSigner::from_seed(9);
817 let trust = RoleTrustSet::<SessionRole>::checked(vec![
818 third.identity(),
819 second.identity(),
820 first.identity(),
821 ])
822 .expect("valid cycled history");
823 let message = b"session";
824
825 for signer in [&first, &second, &third] {
826 assert!(trust.verify(signer.identity().key_id(), message, &signer.sign(message)));
827 }
828
829 let approval = ApprovalSigner::from_seed(7);
830 assert!(!trust.verify(
831 approval.identity().key_id(),
832 message,
833 &approval.sign(message),
834 ));
835 }
836
837 #[test]
838 fn history_rotation_and_cycling_preserve_each_public_identity_once() {
839 let first = SessionSigner::from_seed(7);
840 let second = SessionSigner::from_seed(8);
841 let mut history = SigningKeyHistory::current(&first);
842
843 let (trust, rotated) = history
844 .reconcile::<SessionRole>(&second.identity())
845 .expect("first rotation is valid");
846 assert!(rotated);
847 assert_eq!(trust.keys(), &[second.identity(), first.identity()]);
848
849 let (trust, rotated) = history
850 .reconcile::<SessionRole>(&first.identity())
851 .expect("cycling back is valid");
852 assert!(rotated);
853 assert_eq!(trust.keys(), &[first.identity(), second.identity()]);
854
855 let (trust, rotated) = history
856 .reconcile::<SessionRole>(&second.identity())
857 .expect("cycling forward is valid");
858 assert!(rotated);
859 assert_eq!(trust.keys(), &[second.identity(), first.identity()]);
860 assert_eq!(
861 history.retired_identities(),
862 &[first.identity(), second.identity()]
863 );
864 }
865
866 #[test]
867 fn history_rejects_duplicate_retired_identity() {
868 let first = SessionSigner::from_seed(7);
869 let second = SessionSigner::from_seed(8);
870 let mut history = SigningKeyHistory {
871 current: second.identity(),
872 retired: vec![first.identity(), first.identity()],
873 };
874
875 assert!(matches!(
876 history.reconcile::<SessionRole>(&second.identity()),
877 Err(SigningIdentityError::WrongKeyId)
878 ));
879 }
880
881 #[test]
882 fn history_schema_rejects_unrecognized_fields() {
883 let signer = SessionSigner::from_seed(7);
884 let mut value =
885 serde_json::to_value(SigningKeyHistory::current(&signer)).expect("history serializes");
886 value
887 .as_object_mut()
888 .expect("history is an object")
889 .insert("private_key".to_owned(), serde_json::json!("must-not-pass"));
890
891 assert!(serde_json::from_value::<SigningKeyHistory>(value).is_err());
892 }
893
894 #[test]
895 fn public_role_protocols_reject_the_same_key_under_the_wrong_role() {
896 let turn_read = TurnReadSigner::from_seed(17);
897 let journal: JournalAttestationSigner = turn_read.relabel_for_test();
898 let turn_read_trust = RoleTrustSet::<TurnReadRole>::current(&turn_read);
899 let journal_trust = RoleTrustSet::<JournalAttestationRole>::current(&journal);
900 let canonical = b"same canonical bytes";
901 let turn_read_signature = turn_read.sign_turn_read_capability(canonical);
902 let journal_signature = journal.sign_journal_root(canonical);
903
904 assert!(turn_read_trust.verify_turn_read_capability(
905 turn_read.identity().key_id(),
906 canonical,
907 &turn_read_signature,
908 ));
909 assert!(journal_trust.verify_journal_root(
910 journal.identity().key_id(),
911 canonical,
912 &journal_signature,
913 ));
914 assert!(!turn_read_trust.verify_turn_read_capability(
915 turn_read.identity().key_id(),
916 canonical,
917 &journal_signature,
918 ));
919 assert!(!journal_trust.verify_journal_root(
920 journal.identity().key_id(),
921 canonical,
922 &turn_read_signature,
923 ));
924 }
925
926 #[test]
927 fn signer_debug_never_prints_private_material() {
928 let signer = SessionSigner::from_key_bytes(&[0x0cu8; 32]).expect("valid seed");
929 let debug = format!("{signer:?}");
930 assert!(debug.contains(SessionRole::ISSUER));
931 assert!(!debug.contains("0c0c0c0c"));
932 }
933
934 #[test]
935 fn native_smoke_bootstrap_vectors_are_stable() {
936 fn assert_identity<R: SigningRole>(
937 seed: u8,
938 expected_public_key: &str,
939 expected_key_id: &str,
940 ) {
941 let signer = RoleSigner::<R>::from_key_bytes(&[seed; 32]).expect("valid seed");
942 assert_eq!(
943 crate::hex::lower(&signer.public_key_bytes()),
944 expected_public_key
945 );
946 assert_eq!(signer.identity().key_id(), expected_key_id);
947 }
948
949 assert_identity::<ApprovalRole>(
950 0x0b,
951 "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
952 "3957902d0fa1c0870ea038f7a4b11e3285138f241eb25ca7461252ffa32302dd",
953 );
954 assert_identity::<SessionRole>(
955 0x0c,
956 "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d",
957 "b8123f51278253aa6f42d45c176c5dbcc28de411185c4c4cf3a37b09be8afacc",
958 );
959 assert_identity::<TurnReadRole>(
960 0x0d,
961 "91a28a0b74381593a4d9469579208926afc8ad82c8839b7644359b9eba9a4b3a",
962 "5f9b2a2076cbde4a81150c4d8b164b95053f35bcaae5780310a2936590e44672",
963 );
964 assert_identity::<WebSessionGrantRole>(
965 0x0e,
966 "0beef5a9e679e6a3e134fe27837bff32c7cb5f5d44ea09bcb0e542bad6a4c0cc",
967 "69650566262e58960bc2aa618913468aed27aa5e60d0badb3906935065ba63d6",
968 );
969 assert_identity::<JournalAttestationRole>(
970 0x0f,
971 "d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c",
972 "430dad21c1b44d5872905d82907089116378d6866bb7ac5858c96090d7314435",
973 );
974 assert_identity::<HandoffRole>(
975 0x10,
976 "5c9c6df261c9cb840475776aaefcd944b405328fab28f9b3a95ef40490d3de84",
977 "c46c8e3f82b40e38adc7ad36620d22dd245cfe62a398b85b294ee9dd288abc7a",
978 );
979 assert_identity::<SubagentRole>(
980 0x11,
981 "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737",
982 "3eb64fb1252c81f8faa7fa15f4e5ecb4acf5b2dc119a3795d7576465dab22180",
983 );
984 }
985
986 #[test]
992 fn each_signed_artifact_kind_rejects_every_other_kind() {
993 type Sign = fn(&HandoffSigner, &SubagentSigner, &[u8]) -> Vec<u8>;
994 type Classify = fn(
995 &RoleTrustSet<HandoffRole>,
996 &RoleTrustSet<SubagentRole>,
997 &[u8],
998 &[u8],
999 &[u8],
1000 ) -> SignatureVerdict;
1001
1002 let handoff_signer = HandoffSigner::from_seed(31);
1003 let subagent_signer: SubagentSigner = handoff_signer.relabel_for_test();
1004 let handoff_trust = RoleTrustSet::<HandoffRole>::current(&handoff_signer);
1005 let subagent_trust = RoleTrustSet::<SubagentRole>::current(&subagent_signer);
1006 let canonical = b"identical canonical bytes";
1007 let kinds: [(&str, Sign, Classify); 4] = [
1008 (
1009 "handoff",
1010 |handoff, _, bytes| handoff.sign_handoff(bytes),
1011 |handoff, _, key, bytes, signature| handoff.classify_handoff(key, bytes, signature),
1012 ),
1013 (
1014 "handoff-denied",
1015 |handoff, _, bytes| handoff.sign_handoff_denied(bytes),
1016 |handoff, _, key, bytes, signature| {
1017 handoff.classify_handoff_denied(key, bytes, signature)
1018 },
1019 ),
1020 (
1021 "subagent-spawn",
1022 |_, subagent, bytes| subagent.sign_subagent_spawn(bytes),
1023 |_, subagent, key, bytes, signature| {
1024 subagent.classify_subagent_spawn(key, bytes, signature)
1025 },
1026 ),
1027 (
1028 "subagent-result",
1029 |_, subagent, bytes| subagent.sign_subagent_result(bytes),
1030 |_, subagent, key, bytes, signature| {
1031 subagent.classify_subagent_result(key, bytes, signature)
1032 },
1033 ),
1034 ];
1035
1036 let public_key = handoff_signer.public_key_bytes();
1037 for (source_name, sign, _) in &kinds {
1038 let signature = sign(&handoff_signer, &subagent_signer, canonical);
1039 for (target_name, _, classify) in &kinds {
1040 let verdict = classify(
1041 &handoff_trust,
1042 &subagent_trust,
1043 &public_key,
1044 canonical,
1045 &signature,
1046 );
1047 let expected = if source_name == target_name {
1048 SignatureVerdict::Verified
1049 } else {
1050 SignatureVerdict::Invalid
1051 };
1052 assert_eq!(
1053 verdict, expected,
1054 "a {source_name} signature checked as {target_name}",
1055 );
1056 }
1057 }
1058 }
1059
1060 #[test]
1061 fn a_signature_from_an_untrusted_key_classifies_as_untrusted() {
1062 let deployment = HandoffSigner::from_seed(41);
1063 let stranger = HandoffSigner::from_seed(42);
1064 let trust = RoleTrustSet::<HandoffRole>::current(&deployment);
1065 let canonical = b"handoff canonical bytes";
1066
1067 assert_eq!(
1068 trust.classify_handoff(
1069 &deployment.public_key_bytes(),
1070 canonical,
1071 &deployment.sign_handoff(canonical),
1072 ),
1073 SignatureVerdict::Verified
1074 );
1075 assert_eq!(
1076 trust.classify_handoff(
1077 &stranger.public_key_bytes(),
1078 canonical,
1079 &stranger.sign_handoff(canonical),
1080 ),
1081 SignatureVerdict::Untrusted
1082 );
1083 assert_eq!(
1084 trust.classify_handoff(
1085 &deployment.public_key_bytes(),
1086 b"tampered canonical bytes",
1087 &deployment.sign_handoff(canonical),
1088 ),
1089 SignatureVerdict::Invalid
1090 );
1091 }
1092
1093 #[test]
1094 fn a_retired_key_stays_trusted_for_its_role() {
1095 let current = SubagentSigner::from_seed(43);
1096 let retired = SubagentSigner::from_seed(44);
1097 let trust =
1098 RoleTrustSet::<SubagentRole>::checked(vec![current.identity(), retired.identity()])
1099 .expect("valid role history");
1100 let canonical = b"subagent result canonical bytes";
1101
1102 for signer in [¤t, &retired] {
1103 assert_eq!(
1104 trust.classify_subagent_result(
1105 &signer.public_key_bytes(),
1106 canonical,
1107 &signer.sign_subagent_result(canonical),
1108 ),
1109 SignatureVerdict::Verified
1110 );
1111 }
1112 }
1113
1114 #[test]
1115 fn a_malformed_embedded_key_classifies_as_invalid() {
1116 let signer = HandoffSigner::from_seed(45);
1117 let trust = RoleTrustSet::<HandoffRole>::current(&signer);
1118 let canonical = b"handoff canonical bytes";
1119
1120 assert_eq!(
1121 trust.classify_handoff(b"not-a-key", canonical, &signer.sign_handoff(canonical)),
1122 SignatureVerdict::Invalid
1123 );
1124 }
1125
1126 #[test]
1127 fn verdict_names_are_stable() {
1128 assert_eq!(SignatureVerdict::Verified.as_str(), "verified");
1129 assert_eq!(SignatureVerdict::Invalid.as_str(), "invalid");
1130 assert_eq!(SignatureVerdict::Untrusted.as_str(), "untrusted");
1131 assert_eq!(SignatureVerdict::Untrusted.to_string(), "untrusted");
1132 }
1133}