1use std::{marker::PhantomData, sync::Arc};
10
11use sha2::{Digest as _, Sha256};
12
13use crate::{Signer, verify};
14
15mod private {
16 pub trait Sealed {}
17}
18
19pub trait SigningRole: private::Sealed + Send + Sync + 'static {
21 const ISSUER: &'static str;
23}
24
25macro_rules! role {
26 ($(#[$meta:meta])* $name:ident, $issuer:literal) => {
27 $(#[$meta])*
28 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
29 pub struct $name;
30 impl private::Sealed for $name {}
31 impl SigningRole for $name {
32 const ISSUER: &'static str = $issuer;
33 }
34 };
35}
36
37role!(
38 ApprovalRole,
40 "polychrome.control.approval"
41);
42role!(
43 SessionRole,
45 "polychrome.control.session"
46);
47role!(
48 TurnReadRole,
50 "polychrome.control.turn-read"
51);
52role!(
53 WebSessionGrantRole,
55 "polychrome.control.web-session-grant"
56);
57role!(
58 JournalAttestationRole,
60 "polychrome.state.journal-attestation"
61);
62
63pub const CONTROL_APPROVAL_KEY_REF: &str = "control-plane/approval-signer";
65pub const CONTROL_APPROVAL_HISTORY_REF: &str = "control-plane/approval-signer-history";
67pub const CONTROL_SESSION_KEY_REF: &str = "control-plane/session-signer";
69pub const CONTROL_SESSION_HISTORY_REF: &str = "control-plane/session-signer-history";
71pub const CONTROL_TURN_READ_KEY_REF: &str = "control-plane/turn-read-signer";
73pub const CONTROL_TURN_READ_HISTORY_REF: &str = "control-plane/turn-read-signer-history";
75pub const CONTROL_WEB_SESSION_GRANT_KEY_REF: &str = "control-plane/web-session-grant-signer";
77pub const CONTROL_WEB_SESSION_GRANT_HISTORY_REF: &str =
79 "control-plane/web-session-grant-signer-history";
80pub const CONTROL_MEMORY_JOURNAL_KEY_REF: &str = "control-plane/memory-journal-attestation-signer";
82pub const CONTROL_MEMORY_JOURNAL_HISTORY_REF: &str =
84 "control-plane/memory-journal-attestation-signer-history";
85
86#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct SigningKeyIdentity {
90 issuer: String,
91 key_id: String,
92 public_key: Vec<u8>,
93}
94
95impl SigningKeyIdentity {
96 pub fn for_public_key<R: SigningRole>(
102 public_key: Vec<u8>,
103 ) -> Result<Self, SigningIdentityError> {
104 if public_key.len() != 32 {
105 return Err(SigningIdentityError::InvalidPublicKey);
106 }
107 Ok(Self {
108 issuer: R::ISSUER.to_owned(),
109 key_id: key_id(R::ISSUER, &public_key),
110 public_key,
111 })
112 }
113
114 pub fn checked<R: SigningRole>(
121 issuer: impl Into<String>,
122 claimed_key_id: impl Into<String>,
123 public_key: Vec<u8>,
124 ) -> Result<Self, SigningIdentityError> {
125 let identity = Self {
126 issuer: issuer.into(),
127 key_id: claimed_key_id.into(),
128 public_key,
129 };
130 if identity.issuer != R::ISSUER {
131 return Err(SigningIdentityError::WrongIssuer);
132 }
133 if identity.public_key.len() != 32 {
134 return Err(SigningIdentityError::InvalidPublicKey);
135 }
136 if identity.key_id != key_id(R::ISSUER, &identity.public_key) {
137 return Err(SigningIdentityError::WrongKeyId);
138 }
139 Ok(identity)
140 }
141
142 #[must_use]
144 pub fn issuer(&self) -> &str {
145 &self.issuer
146 }
147
148 #[must_use]
150 pub fn key_id(&self) -> &str {
151 &self.key_id
152 }
153
154 #[must_use]
156 pub fn public_key(&self) -> &[u8] {
157 &self.public_key
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
163pub enum SigningIdentityError {
164 #[error("signing-key issuer does not match its role")]
166 WrongIssuer,
167 #[error("signing public key is not an encoded ed25519 key")]
169 InvalidPublicKey,
170 #[error("signing key id does not match its issuer and public key")]
172 WrongKeyId,
173}
174
175pub struct RoleSigner<R: SigningRole> {
177 inner: Arc<Signer>,
178 role: PhantomData<R>,
179}
180
181impl<R: SigningRole> Clone for RoleSigner<R> {
182 fn clone(&self) -> Self {
183 Self {
184 inner: Arc::clone(&self.inner),
185 role: PhantomData,
186 }
187 }
188}
189
190impl<R: SigningRole> std::fmt::Debug for RoleSigner<R> {
191 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 formatter
193 .debug_struct("RoleSigner")
194 .field("identity", &self.identity())
195 .finish_non_exhaustive()
196 }
197}
198
199impl<R: SigningRole> RoleSigner<R> {
200 #[cfg(any(test, feature = "test-util"))]
202 #[must_use]
203 pub fn from_seed(seed: u64) -> Self {
204 Self {
205 inner: Arc::new(Signer::from_seed(seed)),
206 role: PhantomData,
207 }
208 }
209
210 pub fn from_key_bytes(bytes: &[u8]) -> Result<Self, crate::SignerError> {
216 Ok(Self {
217 inner: Arc::new(Signer::from_key_bytes(bytes)?),
218 role: PhantomData,
219 })
220 }
221
222 #[must_use]
224 pub fn public_key_bytes(&self) -> Vec<u8> {
225 self.inner.public_key_bytes()
226 }
227
228 #[must_use]
230 pub fn identity(&self) -> SigningKeyIdentity {
231 let public_key = self.public_key_bytes();
232 SigningKeyIdentity {
233 issuer: R::ISSUER.to_owned(),
234 key_id: key_id(R::ISSUER, &public_key),
235 public_key,
236 }
237 }
238
239 #[must_use]
244 pub(crate) fn sign(&self, canonical_bytes: &[u8]) -> Vec<u8> {
245 self.inner.sign(canonical_bytes)
246 }
247
248 #[must_use]
253 pub(crate) fn as_signer(&self) -> &Signer {
254 &self.inner
255 }
256
257 #[cfg(any(test, feature = "test-util"))]
262 #[must_use]
263 pub fn relabel_for_test<S: SigningRole>(&self) -> RoleSigner<S> {
264 RoleSigner {
265 inner: Arc::clone(&self.inner),
266 role: PhantomData,
267 }
268 }
269}
270
271impl RoleSigner<TurnReadRole> {
272 #[must_use]
277 pub fn sign_turn_read_capability(&self, canonical_bytes: &[u8]) -> Vec<u8> {
278 self.inner
279 .sign(&role_scoped_message::<TurnReadRole>(canonical_bytes))
280 }
281}
282
283impl RoleSigner<JournalAttestationRole> {
284 #[must_use]
289 pub fn sign_journal_root(&self, canonical_bytes: &[u8]) -> Vec<u8> {
290 self.inner
291 .sign(&role_scoped_message::<JournalAttestationRole>(
292 canonical_bytes,
293 ))
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct RoleTrustSet<R: SigningRole> {
300 keys: Vec<SigningKeyIdentity>,
301 role: PhantomData<R>,
302}
303
304#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
310#[serde(deny_unknown_fields)]
311pub struct SigningKeyHistory {
312 current: SigningKeyIdentity,
313 retired: Vec<SigningKeyIdentity>,
314}
315
316impl SigningKeyHistory {
317 #[must_use]
319 pub fn current<R: SigningRole>(signer: &RoleSigner<R>) -> Self {
320 Self {
321 current: signer.identity(),
322 retired: Vec::new(),
323 }
324 }
325
326 #[must_use]
328 pub const fn current_identity(&self) -> &SigningKeyIdentity {
329 &self.current
330 }
331
332 #[must_use]
334 pub fn retired_identities(&self) -> &[SigningKeyIdentity] {
335 &self.retired
336 }
337
338 pub fn reconcile<R: SigningRole>(
350 &mut self,
351 current: &SigningKeyIdentity,
352 ) -> Result<(RoleTrustSet<R>, bool), SigningIdentityError> {
353 let current = SigningKeyIdentity::checked::<R>(
354 current.issuer.clone(),
355 current.key_id.clone(),
356 current.public_key.clone(),
357 )?;
358 let prior_current = SigningKeyIdentity::checked::<R>(
359 self.current.issuer.clone(),
360 self.current.key_id.clone(),
361 self.current.public_key.clone(),
362 )?;
363 let mut retired = self
364 .retired
365 .iter()
366 .map(|identity| {
367 SigningKeyIdentity::checked::<R>(
368 identity.issuer.clone(),
369 identity.key_id.clone(),
370 identity.public_key.clone(),
371 )
372 })
373 .collect::<Result<Vec<_>, _>>()?;
374 for (index, identity) in retired.iter().enumerate() {
375 if retired[..index]
376 .iter()
377 .any(|prior| prior.key_id == identity.key_id)
378 {
379 return Err(SigningIdentityError::WrongKeyId);
380 }
381 }
382
383 let rotated = prior_current != current;
384 if rotated
385 && !retired
386 .iter()
387 .any(|identity| identity.key_id == prior_current.key_id)
388 {
389 retired.push(prior_current);
390 }
391 self.current = current.clone();
392 self.retired.clone_from(&retired);
393
394 let mut identities = vec![current.clone()];
395 identities.extend(
396 retired
397 .into_iter()
398 .filter(|identity| identity.key_id != current.key_id),
399 );
400 Ok((RoleTrustSet::checked(identities)?, rotated))
401 }
402}
403
404impl<R: SigningRole> RoleTrustSet<R> {
405 pub fn from_public_keys(keys: Vec<Vec<u8>>) -> Result<Self, SigningIdentityError> {
411 let identities = keys
412 .into_iter()
413 .map(SigningKeyIdentity::for_public_key::<R>)
414 .collect::<Result<Vec<_>, _>>()?;
415 Self::checked(identities)
416 }
417
418 pub fn checked(keys: Vec<SigningKeyIdentity>) -> Result<Self, SigningIdentityError> {
424 if keys.is_empty() {
425 return Err(SigningIdentityError::InvalidPublicKey);
426 }
427 let mut checked = Vec::with_capacity(keys.len());
428 for key in keys {
429 let key = SigningKeyIdentity::checked::<R>(key.issuer, key.key_id, key.public_key)?;
430 if checked
431 .iter()
432 .any(|known: &SigningKeyIdentity| known.key_id == key.key_id)
433 {
434 return Err(SigningIdentityError::WrongKeyId);
435 }
436 checked.push(key);
437 }
438 Ok(Self {
439 keys: checked,
440 role: PhantomData,
441 })
442 }
443
444 #[must_use]
446 pub fn current(signer: &RoleSigner<R>) -> Self {
447 Self {
448 keys: vec![signer.identity()],
449 role: PhantomData,
450 }
451 }
452
453 #[must_use]
455 pub fn keys(&self) -> &[SigningKeyIdentity] {
456 &self.keys
457 }
458
459 #[must_use]
461 pub(crate) fn verify(&self, key_id: &str, message: &[u8], signature: &[u8]) -> bool {
462 self.keys
463 .iter()
464 .find(|key| key.key_id == key_id)
465 .is_some_and(|key| verify(&key.public_key, message, signature))
466 }
467}
468
469impl RoleTrustSet<TurnReadRole> {
470 #[must_use]
472 pub fn verify_turn_read_capability(
473 &self,
474 key_id: &str,
475 canonical_bytes: &[u8],
476 signature: &[u8],
477 ) -> bool {
478 self.verify(
479 key_id,
480 &role_scoped_message::<TurnReadRole>(canonical_bytes),
481 signature,
482 )
483 }
484}
485
486impl RoleTrustSet<JournalAttestationRole> {
487 #[must_use]
489 pub fn verify_journal_root(
490 &self,
491 key_id: &str,
492 canonical_bytes: &[u8],
493 signature: &[u8],
494 ) -> bool {
495 self.verify(
496 key_id,
497 &role_scoped_message::<JournalAttestationRole>(canonical_bytes),
498 signature,
499 )
500 }
501}
502
503pub type ApprovalSigner = RoleSigner<ApprovalRole>;
505pub type SessionSigner = RoleSigner<SessionRole>;
507pub type TurnReadSigner = RoleSigner<TurnReadRole>;
509pub type WebSessionGrantSigner = RoleSigner<WebSessionGrantRole>;
511pub type JournalAttestationSigner = RoleSigner<JournalAttestationRole>;
513
514fn key_id(issuer: &str, public_key: &[u8]) -> String {
515 let mut hash = Sha256::new();
516 hash.update(b"polychrome.signing-key-id.v1\0");
517 hash.update(
518 u64::try_from(issuer.len())
519 .unwrap_or(u64::MAX)
520 .to_be_bytes(),
521 );
522 hash.update(issuer.as_bytes());
523 hash.update(public_key);
524 crate::hex::lower(&hash.finalize())
525}
526
527fn role_scoped_message<R: SigningRole>(canonical_bytes: &[u8]) -> Vec<u8> {
528 let mut message = Vec::with_capacity(40 + R::ISSUER.len() + canonical_bytes.len());
529 message.extend_from_slice(b"polychrome.signing-role.v1\0");
530 message.extend_from_slice(
531 &u64::try_from(R::ISSUER.len())
532 .unwrap_or(u64::MAX)
533 .to_be_bytes(),
534 );
535 message.extend_from_slice(R::ISSUER.as_bytes());
536 message.extend_from_slice(canonical_bytes);
537 message
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn roles_have_distinct_issuers_and_key_ids() {
546 let approval = ApprovalSigner::from_seed(7);
547 let session = SessionSigner::from_seed(7);
548 assert_ne!(approval.identity().issuer(), session.identity().issuer());
549 assert_ne!(approval.identity().key_id(), session.identity().key_id());
550 assert_eq!(approval.public_key_bytes(), session.public_key_bytes());
551 }
552
553 #[test]
554 fn a_cross_role_identity_is_refused() {
555 let identity = ApprovalSigner::from_seed(7).identity();
556 assert_eq!(
557 SigningKeyIdentity::checked::<SessionRole>(
558 identity.issuer,
559 identity.key_id,
560 identity.public_key,
561 ),
562 Err(SigningIdentityError::WrongIssuer)
563 );
564 }
565
566 #[test]
567 fn role_trust_requires_the_named_key() {
568 let current = SessionSigner::from_seed(7);
569 let retired = SessionSigner::from_seed(8);
570 let trust =
571 RoleTrustSet::<SessionRole>::checked(vec![current.identity(), retired.identity()])
572 .expect("valid role history");
573 let message = b"session";
574 assert!(trust.verify(current.identity().key_id(), message, ¤t.sign(message)));
575 assert!(trust.verify(retired.identity().key_id(), message, &retired.sign(message)));
576 assert!(!trust.verify("unknown", message, ¤t.sign(message)));
577 }
578
579 #[test]
580 fn role_trust_survives_key_cycling_without_cross_role_acceptance() {
581 let first = SessionSigner::from_seed(7);
582 let second = SessionSigner::from_seed(8);
583 let third = SessionSigner::from_seed(9);
584 let trust = RoleTrustSet::<SessionRole>::checked(vec![
585 third.identity(),
586 second.identity(),
587 first.identity(),
588 ])
589 .expect("valid cycled history");
590 let message = b"session";
591
592 for signer in [&first, &second, &third] {
593 assert!(trust.verify(signer.identity().key_id(), message, &signer.sign(message)));
594 }
595
596 let approval = ApprovalSigner::from_seed(7);
597 assert!(!trust.verify(
598 approval.identity().key_id(),
599 message,
600 &approval.sign(message),
601 ));
602 }
603
604 #[test]
605 fn history_rotation_and_cycling_preserve_each_public_identity_once() {
606 let first = SessionSigner::from_seed(7);
607 let second = SessionSigner::from_seed(8);
608 let mut history = SigningKeyHistory::current(&first);
609
610 let (trust, rotated) = history
611 .reconcile::<SessionRole>(&second.identity())
612 .expect("first rotation is valid");
613 assert!(rotated);
614 assert_eq!(trust.keys(), &[second.identity(), first.identity()]);
615
616 let (trust, rotated) = history
617 .reconcile::<SessionRole>(&first.identity())
618 .expect("cycling back is valid");
619 assert!(rotated);
620 assert_eq!(trust.keys(), &[first.identity(), second.identity()]);
621
622 let (trust, rotated) = history
623 .reconcile::<SessionRole>(&second.identity())
624 .expect("cycling forward is valid");
625 assert!(rotated);
626 assert_eq!(trust.keys(), &[second.identity(), first.identity()]);
627 assert_eq!(
628 history.retired_identities(),
629 &[first.identity(), second.identity()]
630 );
631 }
632
633 #[test]
634 fn history_rejects_duplicate_retired_identity() {
635 let first = SessionSigner::from_seed(7);
636 let second = SessionSigner::from_seed(8);
637 let mut history = SigningKeyHistory {
638 current: second.identity(),
639 retired: vec![first.identity(), first.identity()],
640 };
641
642 assert!(matches!(
643 history.reconcile::<SessionRole>(&second.identity()),
644 Err(SigningIdentityError::WrongKeyId)
645 ));
646 }
647
648 #[test]
649 fn history_schema_rejects_unrecognized_fields() {
650 let signer = SessionSigner::from_seed(7);
651 let mut value =
652 serde_json::to_value(SigningKeyHistory::current(&signer)).expect("history serializes");
653 value
654 .as_object_mut()
655 .expect("history is an object")
656 .insert("private_key".to_owned(), serde_json::json!("must-not-pass"));
657
658 assert!(serde_json::from_value::<SigningKeyHistory>(value).is_err());
659 }
660
661 #[test]
662 fn public_role_protocols_reject_the_same_key_under_the_wrong_role() {
663 let turn_read = TurnReadSigner::from_seed(17);
664 let journal: JournalAttestationSigner = turn_read.relabel_for_test();
665 let turn_read_trust = RoleTrustSet::<TurnReadRole>::current(&turn_read);
666 let journal_trust = RoleTrustSet::<JournalAttestationRole>::current(&journal);
667 let canonical = b"same canonical bytes";
668 let turn_read_signature = turn_read.sign_turn_read_capability(canonical);
669 let journal_signature = journal.sign_journal_root(canonical);
670
671 assert!(turn_read_trust.verify_turn_read_capability(
672 turn_read.identity().key_id(),
673 canonical,
674 &turn_read_signature,
675 ));
676 assert!(journal_trust.verify_journal_root(
677 journal.identity().key_id(),
678 canonical,
679 &journal_signature,
680 ));
681 assert!(!turn_read_trust.verify_turn_read_capability(
682 turn_read.identity().key_id(),
683 canonical,
684 &journal_signature,
685 ));
686 assert!(!journal_trust.verify_journal_root(
687 journal.identity().key_id(),
688 canonical,
689 &turn_read_signature,
690 ));
691 }
692
693 #[test]
694 fn signer_debug_never_prints_private_material() {
695 let signer = SessionSigner::from_key_bytes(&[0x0cu8; 32]).expect("valid seed");
696 let debug = format!("{signer:?}");
697 assert!(debug.contains(SessionRole::ISSUER));
698 assert!(!debug.contains("0c0c0c0c"));
699 }
700
701 #[test]
702 fn native_smoke_bootstrap_vectors_are_stable() {
703 fn assert_identity<R: SigningRole>(
704 seed: u8,
705 expected_public_key: &str,
706 expected_key_id: &str,
707 ) {
708 let signer = RoleSigner::<R>::from_key_bytes(&[seed; 32]).expect("valid seed");
709 assert_eq!(
710 crate::hex::lower(&signer.public_key_bytes()),
711 expected_public_key
712 );
713 assert_eq!(signer.identity().key_id(), expected_key_id);
714 }
715
716 assert_identity::<ApprovalRole>(
717 0x0b,
718 "66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
719 "3957902d0fa1c0870ea038f7a4b11e3285138f241eb25ca7461252ffa32302dd",
720 );
721 assert_identity::<SessionRole>(
722 0x0c,
723 "0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d",
724 "b8123f51278253aa6f42d45c176c5dbcc28de411185c4c4cf3a37b09be8afacc",
725 );
726 assert_identity::<TurnReadRole>(
727 0x0d,
728 "91a28a0b74381593a4d9469579208926afc8ad82c8839b7644359b9eba9a4b3a",
729 "5f9b2a2076cbde4a81150c4d8b164b95053f35bcaae5780310a2936590e44672",
730 );
731 assert_identity::<WebSessionGrantRole>(
732 0x0e,
733 "0beef5a9e679e6a3e134fe27837bff32c7cb5f5d44ea09bcb0e542bad6a4c0cc",
734 "69650566262e58960bc2aa618913468aed27aa5e60d0badb3906935065ba63d6",
735 );
736 assert_identity::<JournalAttestationRole>(
737 0x0f,
738 "d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c",
739 "430dad21c1b44d5872905d82907089116378d6866bb7ac5858c96090d7314435",
740 );
741 }
742}