use std::{marker::PhantomData, sync::Arc};
use sha2::{Digest as _, Sha256};
use crate::{Signer, verify};
mod private {
pub trait Sealed {}
}
pub trait SigningRole: private::Sealed + Send + Sync + 'static {
const ISSUER: &'static str;
}
macro_rules! role {
($(#[$meta:meta])* $name:ident, $issuer:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct $name;
impl private::Sealed for $name {}
impl SigningRole for $name {
const ISSUER: &'static str = $issuer;
}
};
}
role!(
ApprovalRole,
"polychrome.control.approval"
);
role!(
SessionRole,
"polychrome.control.session"
);
role!(
TurnReadRole,
"polychrome.control.turn-read"
);
role!(
WebSessionGrantRole,
"polychrome.control.web-session-grant"
);
role!(
JournalAttestationRole,
"polychrome.state.journal-attestation"
);
pub const CONTROL_APPROVAL_KEY_REF: &str = "control-plane/approval-signer";
pub const CONTROL_APPROVAL_HISTORY_REF: &str = "control-plane/approval-signer-history";
pub const CONTROL_SESSION_KEY_REF: &str = "control-plane/session-signer";
pub const CONTROL_SESSION_HISTORY_REF: &str = "control-plane/session-signer-history";
pub const CONTROL_TURN_READ_KEY_REF: &str = "control-plane/turn-read-signer";
pub const CONTROL_TURN_READ_HISTORY_REF: &str = "control-plane/turn-read-signer-history";
pub const CONTROL_WEB_SESSION_GRANT_KEY_REF: &str = "control-plane/web-session-grant-signer";
pub const CONTROL_WEB_SESSION_GRANT_HISTORY_REF: &str =
"control-plane/web-session-grant-signer-history";
pub const CONTROL_MEMORY_JOURNAL_KEY_REF: &str = "control-plane/memory-journal-attestation-signer";
pub const CONTROL_MEMORY_JOURNAL_HISTORY_REF: &str =
"control-plane/memory-journal-attestation-signer-history";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SigningKeyIdentity {
issuer: String,
key_id: String,
public_key: Vec<u8>,
}
impl SigningKeyIdentity {
pub fn for_public_key<R: SigningRole>(
public_key: Vec<u8>,
) -> Result<Self, SigningIdentityError> {
if public_key.len() != 32 {
return Err(SigningIdentityError::InvalidPublicKey);
}
Ok(Self {
issuer: R::ISSUER.to_owned(),
key_id: key_id(R::ISSUER, &public_key),
public_key,
})
}
pub fn checked<R: SigningRole>(
issuer: impl Into<String>,
claimed_key_id: impl Into<String>,
public_key: Vec<u8>,
) -> Result<Self, SigningIdentityError> {
let identity = Self {
issuer: issuer.into(),
key_id: claimed_key_id.into(),
public_key,
};
if identity.issuer != R::ISSUER {
return Err(SigningIdentityError::WrongIssuer);
}
if identity.public_key.len() != 32 {
return Err(SigningIdentityError::InvalidPublicKey);
}
if identity.key_id != key_id(R::ISSUER, &identity.public_key) {
return Err(SigningIdentityError::WrongKeyId);
}
Ok(identity)
}
#[must_use]
pub fn issuer(&self) -> &str {
&self.issuer
}
#[must_use]
pub fn key_id(&self) -> &str {
&self.key_id
}
#[must_use]
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum SigningIdentityError {
#[error("signing-key issuer does not match its role")]
WrongIssuer,
#[error("signing public key is not an encoded ed25519 key")]
InvalidPublicKey,
#[error("signing key id does not match its issuer and public key")]
WrongKeyId,
}
pub struct RoleSigner<R: SigningRole> {
inner: Arc<Signer>,
role: PhantomData<R>,
}
impl<R: SigningRole> Clone for RoleSigner<R> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
role: PhantomData,
}
}
}
impl<R: SigningRole> std::fmt::Debug for RoleSigner<R> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RoleSigner")
.field("identity", &self.identity())
.finish_non_exhaustive()
}
}
impl<R: SigningRole> RoleSigner<R> {
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub fn from_seed(seed: u64) -> Self {
Self {
inner: Arc::new(Signer::from_seed(seed)),
role: PhantomData,
}
}
pub fn from_key_bytes(bytes: &[u8]) -> Result<Self, crate::SignerError> {
Ok(Self {
inner: Arc::new(Signer::from_key_bytes(bytes)?),
role: PhantomData,
})
}
#[must_use]
pub fn public_key_bytes(&self) -> Vec<u8> {
self.inner.public_key_bytes()
}
#[must_use]
pub fn identity(&self) -> SigningKeyIdentity {
let public_key = self.public_key_bytes();
SigningKeyIdentity {
issuer: R::ISSUER.to_owned(),
key_id: key_id(R::ISSUER, &public_key),
public_key,
}
}
#[must_use]
pub(crate) fn sign(&self, canonical_bytes: &[u8]) -> Vec<u8> {
self.inner.sign(canonical_bytes)
}
#[must_use]
pub(crate) fn as_signer(&self) -> &Signer {
&self.inner
}
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub fn relabel_for_test<S: SigningRole>(&self) -> RoleSigner<S> {
RoleSigner {
inner: Arc::clone(&self.inner),
role: PhantomData,
}
}
}
impl RoleSigner<TurnReadRole> {
#[must_use]
pub fn sign_turn_read_capability(&self, canonical_bytes: &[u8]) -> Vec<u8> {
self.inner
.sign(&role_scoped_message::<TurnReadRole>(canonical_bytes))
}
}
impl RoleSigner<JournalAttestationRole> {
#[must_use]
pub fn sign_journal_root(&self, canonical_bytes: &[u8]) -> Vec<u8> {
self.inner
.sign(&role_scoped_message::<JournalAttestationRole>(
canonical_bytes,
))
}
}
#[derive(Debug, Clone)]
pub struct RoleTrustSet<R: SigningRole> {
keys: Vec<SigningKeyIdentity>,
role: PhantomData<R>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SigningKeyHistory {
current: SigningKeyIdentity,
retired: Vec<SigningKeyIdentity>,
}
impl SigningKeyHistory {
#[must_use]
pub fn current<R: SigningRole>(signer: &RoleSigner<R>) -> Self {
Self {
current: signer.identity(),
retired: Vec::new(),
}
}
#[must_use]
pub const fn current_identity(&self) -> &SigningKeyIdentity {
&self.current
}
#[must_use]
pub fn retired_identities(&self) -> &[SigningKeyIdentity] {
&self.retired
}
pub fn reconcile<R: SigningRole>(
&mut self,
current: &SigningKeyIdentity,
) -> Result<(RoleTrustSet<R>, bool), SigningIdentityError> {
let current = SigningKeyIdentity::checked::<R>(
current.issuer.clone(),
current.key_id.clone(),
current.public_key.clone(),
)?;
let prior_current = SigningKeyIdentity::checked::<R>(
self.current.issuer.clone(),
self.current.key_id.clone(),
self.current.public_key.clone(),
)?;
let mut retired = self
.retired
.iter()
.map(|identity| {
SigningKeyIdentity::checked::<R>(
identity.issuer.clone(),
identity.key_id.clone(),
identity.public_key.clone(),
)
})
.collect::<Result<Vec<_>, _>>()?;
for (index, identity) in retired.iter().enumerate() {
if retired[..index]
.iter()
.any(|prior| prior.key_id == identity.key_id)
{
return Err(SigningIdentityError::WrongKeyId);
}
}
let rotated = prior_current != current;
if rotated
&& !retired
.iter()
.any(|identity| identity.key_id == prior_current.key_id)
{
retired.push(prior_current);
}
self.current = current.clone();
self.retired.clone_from(&retired);
let mut identities = vec![current.clone()];
identities.extend(
retired
.into_iter()
.filter(|identity| identity.key_id != current.key_id),
);
Ok((RoleTrustSet::checked(identities)?, rotated))
}
}
impl<R: SigningRole> RoleTrustSet<R> {
pub fn from_public_keys(keys: Vec<Vec<u8>>) -> Result<Self, SigningIdentityError> {
let identities = keys
.into_iter()
.map(SigningKeyIdentity::for_public_key::<R>)
.collect::<Result<Vec<_>, _>>()?;
Self::checked(identities)
}
pub fn checked(keys: Vec<SigningKeyIdentity>) -> Result<Self, SigningIdentityError> {
if keys.is_empty() {
return Err(SigningIdentityError::InvalidPublicKey);
}
let mut checked = Vec::with_capacity(keys.len());
for key in keys {
let key = SigningKeyIdentity::checked::<R>(key.issuer, key.key_id, key.public_key)?;
if checked
.iter()
.any(|known: &SigningKeyIdentity| known.key_id == key.key_id)
{
return Err(SigningIdentityError::WrongKeyId);
}
checked.push(key);
}
Ok(Self {
keys: checked,
role: PhantomData,
})
}
#[must_use]
pub fn current(signer: &RoleSigner<R>) -> Self {
Self {
keys: vec![signer.identity()],
role: PhantomData,
}
}
#[must_use]
pub fn keys(&self) -> &[SigningKeyIdentity] {
&self.keys
}
#[must_use]
pub(crate) fn verify(&self, key_id: &str, message: &[u8], signature: &[u8]) -> bool {
self.keys
.iter()
.find(|key| key.key_id == key_id)
.is_some_and(|key| verify(&key.public_key, message, signature))
}
}
impl RoleTrustSet<TurnReadRole> {
#[must_use]
pub fn verify_turn_read_capability(
&self,
key_id: &str,
canonical_bytes: &[u8],
signature: &[u8],
) -> bool {
self.verify(
key_id,
&role_scoped_message::<TurnReadRole>(canonical_bytes),
signature,
)
}
}
impl RoleTrustSet<JournalAttestationRole> {
#[must_use]
pub fn verify_journal_root(
&self,
key_id: &str,
canonical_bytes: &[u8],
signature: &[u8],
) -> bool {
self.verify(
key_id,
&role_scoped_message::<JournalAttestationRole>(canonical_bytes),
signature,
)
}
}
pub type ApprovalSigner = RoleSigner<ApprovalRole>;
pub type SessionSigner = RoleSigner<SessionRole>;
pub type TurnReadSigner = RoleSigner<TurnReadRole>;
pub type WebSessionGrantSigner = RoleSigner<WebSessionGrantRole>;
pub type JournalAttestationSigner = RoleSigner<JournalAttestationRole>;
fn key_id(issuer: &str, public_key: &[u8]) -> String {
let mut hash = Sha256::new();
hash.update(b"polychrome.signing-key-id.v1\0");
hash.update(
u64::try_from(issuer.len())
.unwrap_or(u64::MAX)
.to_be_bytes(),
);
hash.update(issuer.as_bytes());
hash.update(public_key);
crate::hex::lower(&hash.finalize())
}
fn role_scoped_message<R: SigningRole>(canonical_bytes: &[u8]) -> Vec<u8> {
let mut message = Vec::with_capacity(40 + R::ISSUER.len() + canonical_bytes.len());
message.extend_from_slice(b"polychrome.signing-role.v1\0");
message.extend_from_slice(
&u64::try_from(R::ISSUER.len())
.unwrap_or(u64::MAX)
.to_be_bytes(),
);
message.extend_from_slice(R::ISSUER.as_bytes());
message.extend_from_slice(canonical_bytes);
message
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roles_have_distinct_issuers_and_key_ids() {
let approval = ApprovalSigner::from_seed(7);
let session = SessionSigner::from_seed(7);
assert_ne!(approval.identity().issuer(), session.identity().issuer());
assert_ne!(approval.identity().key_id(), session.identity().key_id());
assert_eq!(approval.public_key_bytes(), session.public_key_bytes());
}
#[test]
fn a_cross_role_identity_is_refused() {
let identity = ApprovalSigner::from_seed(7).identity();
assert_eq!(
SigningKeyIdentity::checked::<SessionRole>(
identity.issuer,
identity.key_id,
identity.public_key,
),
Err(SigningIdentityError::WrongIssuer)
);
}
#[test]
fn role_trust_requires_the_named_key() {
let current = SessionSigner::from_seed(7);
let retired = SessionSigner::from_seed(8);
let trust =
RoleTrustSet::<SessionRole>::checked(vec![current.identity(), retired.identity()])
.expect("valid role history");
let message = b"session";
assert!(trust.verify(current.identity().key_id(), message, ¤t.sign(message)));
assert!(trust.verify(retired.identity().key_id(), message, &retired.sign(message)));
assert!(!trust.verify("unknown", message, ¤t.sign(message)));
}
#[test]
fn role_trust_survives_key_cycling_without_cross_role_acceptance() {
let first = SessionSigner::from_seed(7);
let second = SessionSigner::from_seed(8);
let third = SessionSigner::from_seed(9);
let trust = RoleTrustSet::<SessionRole>::checked(vec![
third.identity(),
second.identity(),
first.identity(),
])
.expect("valid cycled history");
let message = b"session";
for signer in [&first, &second, &third] {
assert!(trust.verify(signer.identity().key_id(), message, &signer.sign(message)));
}
let approval = ApprovalSigner::from_seed(7);
assert!(!trust.verify(
approval.identity().key_id(),
message,
&approval.sign(message),
));
}
#[test]
fn history_rotation_and_cycling_preserve_each_public_identity_once() {
let first = SessionSigner::from_seed(7);
let second = SessionSigner::from_seed(8);
let mut history = SigningKeyHistory::current(&first);
let (trust, rotated) = history
.reconcile::<SessionRole>(&second.identity())
.expect("first rotation is valid");
assert!(rotated);
assert_eq!(trust.keys(), &[second.identity(), first.identity()]);
let (trust, rotated) = history
.reconcile::<SessionRole>(&first.identity())
.expect("cycling back is valid");
assert!(rotated);
assert_eq!(trust.keys(), &[first.identity(), second.identity()]);
let (trust, rotated) = history
.reconcile::<SessionRole>(&second.identity())
.expect("cycling forward is valid");
assert!(rotated);
assert_eq!(trust.keys(), &[second.identity(), first.identity()]);
assert_eq!(
history.retired_identities(),
&[first.identity(), second.identity()]
);
}
#[test]
fn history_rejects_duplicate_retired_identity() {
let first = SessionSigner::from_seed(7);
let second = SessionSigner::from_seed(8);
let mut history = SigningKeyHistory {
current: second.identity(),
retired: vec![first.identity(), first.identity()],
};
assert!(matches!(
history.reconcile::<SessionRole>(&second.identity()),
Err(SigningIdentityError::WrongKeyId)
));
}
#[test]
fn history_schema_rejects_unrecognized_fields() {
let signer = SessionSigner::from_seed(7);
let mut value =
serde_json::to_value(SigningKeyHistory::current(&signer)).expect("history serializes");
value
.as_object_mut()
.expect("history is an object")
.insert("private_key".to_owned(), serde_json::json!("must-not-pass"));
assert!(serde_json::from_value::<SigningKeyHistory>(value).is_err());
}
#[test]
fn public_role_protocols_reject_the_same_key_under_the_wrong_role() {
let turn_read = TurnReadSigner::from_seed(17);
let journal: JournalAttestationSigner = turn_read.relabel_for_test();
let turn_read_trust = RoleTrustSet::<TurnReadRole>::current(&turn_read);
let journal_trust = RoleTrustSet::<JournalAttestationRole>::current(&journal);
let canonical = b"same canonical bytes";
let turn_read_signature = turn_read.sign_turn_read_capability(canonical);
let journal_signature = journal.sign_journal_root(canonical);
assert!(turn_read_trust.verify_turn_read_capability(
turn_read.identity().key_id(),
canonical,
&turn_read_signature,
));
assert!(journal_trust.verify_journal_root(
journal.identity().key_id(),
canonical,
&journal_signature,
));
assert!(!turn_read_trust.verify_turn_read_capability(
turn_read.identity().key_id(),
canonical,
&journal_signature,
));
assert!(!journal_trust.verify_journal_root(
journal.identity().key_id(),
canonical,
&turn_read_signature,
));
}
#[test]
fn signer_debug_never_prints_private_material() {
let signer = SessionSigner::from_key_bytes(&[0x0cu8; 32]).expect("valid seed");
let debug = format!("{signer:?}");
assert!(debug.contains(SessionRole::ISSUER));
assert!(!debug.contains("0c0c0c0c"));
}
#[test]
fn native_smoke_bootstrap_vectors_are_stable() {
fn assert_identity<R: SigningRole>(
seed: u8,
expected_public_key: &str,
expected_key_id: &str,
) {
let signer = RoleSigner::<R>::from_key_bytes(&[seed; 32]).expect("valid seed");
assert_eq!(
crate::hex::lower(&signer.public_key_bytes()),
expected_public_key
);
assert_eq!(signer.identity().key_id(), expected_key_id);
}
assert_identity::<ApprovalRole>(
0x0b,
"66be7e332c7a453332bd9d0a7f7db055f5c5ef1a06ada66d98b39fb6810c473a",
"3957902d0fa1c0870ea038f7a4b11e3285138f241eb25ca7461252ffa32302dd",
);
assert_identity::<SessionRole>(
0x0c,
"0b513ad9b4924015ca0902ed079044d3ac5dbec2306f06948c10da8eb6e39f2d",
"b8123f51278253aa6f42d45c176c5dbcc28de411185c4c4cf3a37b09be8afacc",
);
assert_identity::<TurnReadRole>(
0x0d,
"91a28a0b74381593a4d9469579208926afc8ad82c8839b7644359b9eba9a4b3a",
"5f9b2a2076cbde4a81150c4d8b164b95053f35bcaae5780310a2936590e44672",
);
assert_identity::<WebSessionGrantRole>(
0x0e,
"0beef5a9e679e6a3e134fe27837bff32c7cb5f5d44ea09bcb0e542bad6a4c0cc",
"69650566262e58960bc2aa618913468aed27aa5e60d0badb3906935065ba63d6",
);
assert_identity::<JournalAttestationRole>(
0x0f,
"d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c",
"430dad21c1b44d5872905d82907089116378d6866bb7ac5858c96090d7314435",
);
}
}