lib

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

signing.rs (8544B)


      1 //! Concrete local Nostr implementation of the generic signing SPI.
      2 //!
      3 //! Signing is local and in-memory. Success returns a cryptographically verified
      4 //! exact-plan receipt without persistence or publication.
      5 
      6 use core::fmt;
      7 use std::{
      8     time::{SystemTime, UNIX_EPOCH},
      9     vec,
     10 };
     11 
     12 use radroots_identity::PublicKey;
     13 use radroots_signing::{
     14     Error as SigningError, SignReceipt, SignRequest, Signer, SignerStatus,
     15     capability::{CancellationSupport, SignerCapability, SignerKind},
     16     error::Kind,
     17     recovery::ReplayCapability,
     18     signer::BoxFuture,
     19     status::{SignProgress, SignProgressStage, SignerAvailability},
     20 };
     21 
     22 use crate::{Error as NostrError, key::SecretKey};
     23 
     24 type Clock = fn() -> Result<u64, SigningError>;
     25 
     26 /// A local Nostr key-backed signer adapter.
     27 pub struct LocalSigner {
     28     keys: nostr::Keys,
     29     public_key: PublicKey,
     30     clock: Clock,
     31 }
     32 
     33 impl LocalSigner {
     34     pub fn new(secret_key: SecretKey) -> Result<Self, crate::Error> {
     35         Self::with_clock(secret_key, system_time_unix_ms)
     36     }
     37 
     38     pub fn generate() -> Result<Self, crate::Error> {
     39         Self::new(SecretKey::generate())
     40     }
     41 
     42     #[must_use]
     43     pub const fn public_key(&self) -> PublicKey {
     44         self.public_key
     45     }
     46 
     47     fn with_clock(secret_key: SecretKey, clock: Clock) -> Result<Self, crate::Error> {
     48         let public_key = secret_key.public_key()?;
     49         Ok(Self {
     50             keys: secret_key.into_keys(),
     51             public_key,
     52             clock,
     53         })
     54     }
     55 }
     56 
     57 impl fmt::Debug for LocalSigner {
     58     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     59         formatter
     60             .debug_struct("LocalSigner")
     61             .field("public_key", &self.public_key)
     62             .field("secret_key", &"[redacted]")
     63             .finish()
     64     }
     65 }
     66 
     67 impl Signer for LocalSigner {
     68     fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> {
     69         Box::pin(async {
     70             Ok(SignerStatus::new(
     71                 SignerAvailability::Ready,
     72                 vec![SignerCapability::new(
     73                     SignerKind::Local,
     74                     ReplayCapability::LocalReplaySafe,
     75                     CancellationSupport::BeforePublication,
     76                     true,
     77                     false,
     78                 )],
     79                 None,
     80             ))
     81         })
     82     }
     83 
     84     fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> {
     85         Box::pin(async move {
     86             request.ensure_active((self.clock)()?)?;
     87             request.report_progress(
     88                 &SignProgress::stage(SignProgressStage::Validating)
     89                     .expect("validating has no challenge"),
     90             );
     91             if request.expected_author() != &self.public_key {
     92                 return Err(SigningError::new(Kind::AuthorizationDenied));
     93             }
     94             let signed_event = crate::plan_signing::sign_request(&self.keys, &request)
     95                 .map_err(normalize_nostr_error)?;
     96             request.report_progress(
     97                 &SignProgress::stage(SignProgressStage::VerifyingOutput)
     98                     .expect("verification has no challenge"),
     99             );
    100             let completed_at_unix_ms = (self.clock)()?;
    101             request.ensure_active(completed_at_unix_ms)?;
    102             let receipt =
    103                 SignReceipt::from_signed_event(&request, signed_event, completed_at_unix_ms)?;
    104             request.report_progress(
    105                 &SignProgress::stage(SignProgressStage::Complete)
    106                     .expect("completion has no challenge"),
    107             );
    108             Ok(receipt)
    109         })
    110     }
    111 }
    112 
    113 fn system_time_unix_ms() -> Result<u64, SigningError> {
    114     SystemTime::now()
    115         .duration_since(UNIX_EPOCH)
    116         .map_err(|source| SigningError::with_source(Kind::InternalError, source))
    117         .and_then(|duration| {
    118             u64::try_from(duration.as_millis())
    119                 .map_err(|source| SigningError::with_source(Kind::InternalError, source))
    120         })
    121 }
    122 
    123 fn normalize_nostr_error(source: NostrError) -> SigningError {
    124     let kind = match &source {
    125         NostrError::ExternalSigningAuthorMismatch { .. } => Kind::AuthorizationDenied,
    126         NostrError::ExternalSigningEventIdMismatch { .. }
    127         | NostrError::ExternalSigningEventInvalid(_)
    128         | NostrError::ExternalSigningPlanMismatch { .. } => Kind::SignerOutputInvalid,
    129         _ => Kind::InternalError,
    130     };
    131     SigningError::with_source(kind, source)
    132 }
    133 
    134 #[cfg(test)]
    135 mod tests {
    136     use super::*;
    137     use radroots_event::{GenericEventDraft, contract::AuthorRole};
    138     use radroots_event_codec::authoring::AuthoredEventPlan;
    139     use radroots_protocol::runtime::v1::OperationId;
    140     use radroots_signing::{
    141         Actor, AuthoredArtifactId, SigningIntentId, SigningOperationId,
    142         actor::ActorSource,
    143         request::{CancellationPolicy, CancellationSignal, SignPolicy},
    144     };
    145 
    146     use crate::{
    147         key::parse_secret_key,
    148         test_fixtures::{FIXTURE_ALICE, FIXTURE_BOB},
    149     };
    150 
    151     const CREATED_AT: u64 = 1_700_000_000;
    152     const DEADLINE_MS: u64 = 1_700_000_100_000;
    153 
    154     fn before_deadline() -> Result<u64, SigningError> {
    155         Ok(DEADLINE_MS - 1)
    156     }
    157 
    158     fn at_deadline() -> Result<u64, SigningError> {
    159         Ok(DEADLINE_MS)
    160     }
    161 
    162     fn fixture_secret(value: &str) -> SecretKey {
    163         parse_secret_key(value).expect("secret fixture")
    164     }
    165 
    166     fn request() -> SignRequest {
    167         let actor = Actor::from_public_key_hex(
    168             FIXTURE_ALICE.public_key_hex,
    169             ActorSource::ExplicitPublicKey,
    170             [AuthorRole::Any],
    171         )
    172         .unwrap();
    173         let draft = GenericEventDraft::new(
    174             "radroots.social.geochat.v1",
    175             20_000,
    176             CREATED_AT,
    177             Vec::new(),
    178             "private-fixture-content",
    179             FIXTURE_ALICE.public_key_hex,
    180         )
    181         .unwrap();
    182         SignRequest::new(
    183             OperationId::SyncPush,
    184             SigningIntentId::new(
    185                 SigningOperationId::new([1; 16]).unwrap(),
    186                 AuthoredArtifactId::new([2; 16]).unwrap(),
    187             ),
    188             actor,
    189             AuthoredEventPlan::from_generic(draft).unwrap(),
    190             SignPolicy::new(DEADLINE_MS, CancellationPolicy::LocalCooperative).unwrap(),
    191         )
    192         .unwrap()
    193     }
    194 
    195     #[tokio::test]
    196     async fn local_signer_reports_safe_replay_and_returns_verified_exact_plan() {
    197         let signer = LocalSigner::with_clock(
    198             fixture_secret(FIXTURE_ALICE.secret_key_hex),
    199             before_deadline,
    200         )
    201         .unwrap();
    202         let status = signer.status().await.unwrap();
    203         assert_eq!(
    204             status.capabilities()[0].replay(),
    205             ReplayCapability::LocalReplaySafe
    206         );
    207         let request = request();
    208         let expected_id = request.expected_event_id().to_hex();
    209         let receipt = signer.sign(request).await.unwrap();
    210         assert_eq!(receipt.signed_event().id_str(), expected_id);
    211         assert_eq!(receipt.completed_at_unix_ms(), DEADLINE_MS - 1);
    212     }
    213 
    214     #[tokio::test]
    215     async fn wrong_key_deadline_and_cancellation_fail_closed() {
    216         let wrong =
    217             LocalSigner::with_clock(fixture_secret(FIXTURE_BOB.secret_key_hex), before_deadline)
    218                 .unwrap();
    219         assert_eq!(
    220             wrong.sign(request()).await.unwrap_err().kind(),
    221             Kind::AuthorizationDenied
    222         );
    223         let expired =
    224             LocalSigner::with_clock(fixture_secret(FIXTURE_ALICE.secret_key_hex), at_deadline)
    225                 .unwrap();
    226         assert_eq!(
    227             expired.sign(request()).await.unwrap_err().kind(),
    228             Kind::DeadlineExceeded
    229         );
    230         let signal = CancellationSignal::new();
    231         let cancelled = request().with_cancellation_signal(signal.clone());
    232         signal.cancel();
    233         assert_eq!(
    234             LocalSigner::with_clock(
    235                 fixture_secret(FIXTURE_ALICE.secret_key_hex),
    236                 before_deadline,
    237             )
    238             .unwrap()
    239             .sign(cancelled)
    240             .await
    241             .unwrap_err()
    242             .kind(),
    243             Kind::SignerCancelled
    244         );
    245     }
    246 
    247     #[test]
    248     fn debug_never_exposes_local_secret_material() {
    249         let signer = LocalSigner::with_clock(
    250             fixture_secret(FIXTURE_ALICE.secret_key_hex),
    251             before_deadline,
    252         )
    253         .unwrap();
    254         let debug = format!("{signer:?}");
    255         assert!(!debug.contains(FIXTURE_ALICE.secret_key_hex));
    256         assert!(!debug.contains(FIXTURE_ALICE.nsec));
    257     }
    258 }