lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

identity.rs (5239B)


      1 //! Stable signing operation, artifact, and signer-request identities.
      2 
      3 use core::fmt;
      4 use radroots_event_codec::authoring::PlanDigest;
      5 use sha2::{Digest, Sha256};
      6 
      7 use crate::{Error, error::Kind};
      8 
      9 const REQUEST_ID_DOMAIN: &[u8] = b"radroots.signer_request.v1";
     10 
     11 macro_rules! nonzero_id {
     12     ($name:ident, $label:literal) => {
     13         #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     14         #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     15         pub struct $name([u8; 16]);
     16 
     17         impl $name {
     18             pub fn new(bytes: [u8; 16]) -> Result<Self, Error> {
     19                 if bytes == [0; 16] {
     20                     return Err(Error::new(Kind::InvalidArgument));
     21                 }
     22                 Ok(Self(bytes))
     23             }
     24 
     25             #[must_use]
     26             pub const fn as_bytes(&self) -> &[u8; 16] {
     27                 &self.0
     28             }
     29 
     30             #[must_use]
     31             pub fn to_hex(self) -> alloc_or_std::String {
     32                 hex::encode(self.0)
     33             }
     34         }
     35 
     36         impl fmt::Debug for $name {
     37             fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     38                 formatter.debug_tuple($label).field(&self.to_hex()).finish()
     39             }
     40         }
     41     };
     42 }
     43 
     44 #[cfg(not(feature = "std"))]
     45 mod alloc_or_std {
     46     pub use alloc::string::String;
     47 }
     48 #[cfg(feature = "std")]
     49 mod alloc_or_std {
     50     pub use std::string::String;
     51 }
     52 
     53 nonzero_id!(SigningOperationId, "SigningOperationId");
     54 nonzero_id!(AuthoredArtifactId, "AuthoredArtifactId");
     55 
     56 /// Stable parent/child identity for one authored artifact signing operation.
     57 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     58 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
     59 pub struct SigningIntentId {
     60     operation_id: SigningOperationId,
     61     artifact_id: AuthoredArtifactId,
     62 }
     63 
     64 impl SigningIntentId {
     65     #[must_use]
     66     pub const fn new(operation_id: SigningOperationId, artifact_id: AuthoredArtifactId) -> Self {
     67         Self {
     68             operation_id,
     69             artifact_id,
     70         }
     71     }
     72 
     73     #[must_use]
     74     pub const fn operation_id(self) -> SigningOperationId {
     75         self.operation_id
     76     }
     77 
     78     #[must_use]
     79     pub const fn artifact_id(self) -> AuthoredArtifactId {
     80         self.artifact_id
     81     }
     82 }
     83 
     84 /// Deterministic identity a replay-capable remote signer must deduplicate.
     85 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     86 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
     87 pub struct SignerRequestId([u8; 32]);
     88 
     89 impl SignerRequestId {
     90     #[must_use]
     91     pub fn derive(artifact_id: AuthoredArtifactId, plan_digest: PlanDigest) -> Self {
     92         let mut digest = Sha256::new();
     93         digest.update(REQUEST_ID_DOMAIN);
     94         digest.update(artifact_id.as_bytes());
     95         digest.update(plan_digest.as_bytes());
     96         Self(digest.finalize().into())
     97     }
     98 
     99     #[must_use]
    100     pub const fn as_bytes(&self) -> &[u8; 32] {
    101         &self.0
    102     }
    103 
    104     #[must_use]
    105     pub fn to_hex(self) -> alloc_or_std::String {
    106         hex::encode(self.0)
    107     }
    108 }
    109 
    110 impl fmt::Debug for SignerRequestId {
    111     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    112         formatter
    113             .debug_tuple("SignerRequestId")
    114             .field(&self.to_hex())
    115             .finish()
    116     }
    117 }
    118 
    119 #[cfg(test)]
    120 mod tests {
    121     use super::*;
    122     #[cfg(not(feature = "std"))]
    123     use alloc::format;
    124 
    125     #[test]
    126     fn stable_identities_reject_zero_and_expose_exact_bytes() {
    127         assert_eq!(
    128             SigningOperationId::new([0; 16])
    129                 .expect_err("zero operation ID must fail")
    130                 .kind(),
    131             Kind::InvalidArgument
    132         );
    133         assert_eq!(
    134             AuthoredArtifactId::new([0; 16])
    135                 .expect_err("zero artifact ID must fail")
    136                 .kind(),
    137             Kind::InvalidArgument
    138         );
    139 
    140         let operation = SigningOperationId::new([1; 16]).expect("operation ID");
    141         let artifact = AuthoredArtifactId::new([2; 16]).expect("artifact ID");
    142         assert_eq!(operation.as_bytes(), &[1; 16]);
    143         assert_eq!(artifact.as_bytes(), &[2; 16]);
    144         assert_eq!(operation.to_hex(), "01".repeat(16));
    145         assert_eq!(artifact.to_hex(), "02".repeat(16));
    146         assert_eq!(
    147             format!("{operation:?}"),
    148             format!("SigningOperationId(\"{}\")", "01".repeat(16))
    149         );
    150         assert_eq!(
    151             format!("{artifact:?}"),
    152             format!("AuthoredArtifactId(\"{}\")", "02".repeat(16))
    153         );
    154 
    155         let intent = SigningIntentId::new(operation, artifact);
    156         assert_eq!(intent.operation_id(), operation);
    157         assert_eq!(intent.artifact_id(), artifact);
    158     }
    159 
    160     #[test]
    161     fn signer_request_identity_is_deterministic_and_domain_separated() {
    162         let artifact = AuthoredArtifactId::new([3; 16]).expect("artifact ID");
    163         let digest = PlanDigest::from_bytes([4; 32]);
    164         let request = SignerRequestId::derive(artifact, digest);
    165         assert_eq!(request.as_bytes().len(), 32);
    166         assert_eq!(request.to_hex().len(), 64);
    167         assert_eq!(request, SignerRequestId::derive(artifact, digest));
    168         assert_eq!(
    169             format!("{request:?}"),
    170             format!("SignerRequestId(\"{}\")", request.to_hex())
    171         );
    172     }
    173 }