use core::fmt;
use std::time::SystemTime;
use smallvec::SmallVec;
use crate::proto::Did;
use crate::sealed;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TraceId([u8; 16]);
impl TraceId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
TraceId(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyId([u8; 32]);
impl KeyId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
KeyId(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct PublicKey {
pub algorithm: SignatureAlgorithm,
pub bytes: [u8; 32],
}
impl PublicKey {
#[must_use]
pub const fn new(algorithm: SignatureAlgorithm, bytes: [u8; 32]) -> Self {
PublicKey { algorithm, bytes }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SignatureAlgorithm {
Ed25519,
Es256,
Es256K,
}
#[derive(Debug, Clone)]
pub struct ServiceIdentity {
service_did: Did,
key_id: KeyId,
key_material: PublicKey,
rotation_evidence: Option<RotationChain>,
_private: core::marker::PhantomData<sealed::Token>,
}
impl ServiceIdentity {
#[must_use]
pub(crate) fn new_internal(
service_did: Did,
key_id: KeyId,
key_material: PublicKey,
rotation_evidence: Option<RotationChain>,
) -> Self {
ServiceIdentity {
service_did,
key_id,
key_material,
rotation_evidence,
_private: core::marker::PhantomData,
}
}
#[cfg(feature = "test-support")]
#[must_use]
pub fn new_for_test(
service_did: Did,
key_id: KeyId,
key_material: PublicKey,
rotation_evidence: Option<RotationChain>,
) -> Self {
Self::new_internal(service_did, key_id, key_material, rotation_evidence)
}
#[must_use]
pub fn service_did(&self) -> &Did {
&self.service_did
}
#[must_use]
pub fn key_id(&self) -> KeyId {
self.key_id
}
#[must_use]
pub fn key_material(&self) -> &PublicKey {
&self.key_material
}
#[must_use]
pub fn rotation_evidence(&self) -> Option<&RotationChain> {
self.rotation_evidence.as_ref()
}
}
impl PartialEq for ServiceIdentity {
fn eq(&self, other: &Self) -> bool {
self.service_did == other.service_did && self.key_id == other.key_id
}
}
impl Eq for ServiceIdentity {}
impl core::hash::Hash for ServiceIdentity {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.service_did.hash(state);
self.key_id.hash(state);
}
}
pub const MAX_ROTATION_DEPTH: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RotationChain {
entries: SmallVec<[RotationEntry; MAX_ROTATION_DEPTH]>,
}
impl RotationChain {
pub fn new<I: IntoIterator<Item = RotationEntry>>(
entries: I,
) -> Result<Self, RotationChainError> {
let mut sv = SmallVec::new();
for entry in entries {
if sv.len() >= MAX_ROTATION_DEPTH {
return Err(RotationChainError::TooDeep {
max: MAX_ROTATION_DEPTH,
});
}
sv.push(entry);
}
Ok(RotationChain { entries: sv })
}
#[must_use]
pub fn entries(&self) -> &[RotationEntry] {
&self.entries
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RotationChainError {
#[error("rotation chain exceeds MAX_ROTATION_DEPTH = {max}")]
TooDeep {
max: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RotationEntry {
pub old_key: PublicKey,
pub new_key: PublicKey,
pub rotation_signature: crate::wire::ClaimSignature,
pub rotated_at: SystemTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionId([u8; 32]);
impl SessionId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
SessionId(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionDigest([u8; 32]);
impl SessionDigest {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
SessionDigest(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn compute(session_id: &SessionId, correlation_key: &CorrelationKey) -> Self {
let hash = blake3::keyed_hash(correlation_key.as_bytes(), session_id.as_bytes());
SessionDigest(*hash.as_bytes())
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct CorrelationKey([u8; 32]);
impl CorrelationKey {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
CorrelationKey(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for CorrelationKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CorrelationKey").field("redacted", &true).finish()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct SubstrateSessionDerivationKey([u8; 32]);
impl SubstrateSessionDerivationKey {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
SubstrateSessionDerivationKey(bytes)
}
#[must_use]
pub fn generate() -> Self {
let mut bytes = [0u8; 32];
getrandom::getrandom(&mut bytes)
.expect("OS CSPRNG must be available at substrate startup");
SubstrateSessionDerivationKey(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Debug for SubstrateSessionDerivationKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SubstrateSessionDerivationKey")
.field("redacted", &true)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_id_round_trip() {
let bytes = [0xAB; 16];
let id = TraceId::from_bytes(bytes);
assert_eq!(id.as_bytes(), &bytes);
}
#[test]
fn correlation_key_debug_does_not_leak() {
let key = CorrelationKey::from_bytes([0xFF; 32]);
let s = format!("{key:?}");
assert!(!s.contains("FF"), "Debug must not leak bytes");
assert!(s.contains("redacted"));
}
#[test]
fn max_rotation_depth_constant_is_16() {
assert_eq!(MAX_ROTATION_DEPTH, 16);
}
#[test]
fn substrate_session_derivation_key_generate_produces_distinct_keys() {
let k1 = SubstrateSessionDerivationKey::generate();
let k2 = SubstrateSessionDerivationKey::generate();
assert_ne!(k1.as_bytes(), k2.as_bytes());
}
#[test]
fn substrate_session_derivation_key_debug_does_not_leak() {
let key = SubstrateSessionDerivationKey::from_bytes([0xCC; 32]);
let s = format!("{key:?}");
assert!(!s.contains("CC"), "Debug must not leak key bytes");
assert!(s.contains("redacted"));
}
#[test]
fn session_digest_compute_is_deterministic_and_key_separated() {
let session = SessionId::from_bytes([0xAB; 32]);
let key_a = CorrelationKey::from_bytes([0x11; 32]);
let key_b = CorrelationKey::from_bytes([0x22; 32]);
let d_a1 = SessionDigest::compute(&session, &key_a);
let d_a2 = SessionDigest::compute(&session, &key_a);
let d_b = SessionDigest::compute(&session, &key_b);
assert_eq!(d_a1, d_a2, "deterministic for the same key");
assert_ne!(
d_a1, d_b,
"different keys must produce non-correlatable digests"
);
}
#[test]
fn session_digest_compute_separates_by_session() {
let key = CorrelationKey::from_bytes([0x33; 32]);
let s1 = SessionId::from_bytes([0x44; 32]);
let s2 = SessionId::from_bytes([0x45; 32]);
let d1 = SessionDigest::compute(&s1, &key);
let d2 = SessionDigest::compute(&s2, &key);
assert_ne!(d1, d2);
}
}