lib

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

receipt.rs (7517B)


      1 //! Cryptographically verified exact-plan signing receipts.
      2 
      3 use core::fmt;
      4 use radroots_event::SignedEvent;
      5 use radroots_event_codec::verify::{self, Nip01SignatureVerifier, RawEvent};
      6 use radroots_protocol::runtime::v1::OperationId;
      7 
      8 use crate::{Error, SignRequest, SignerRequestId, SigningIntentId, error::Kind};
      9 
     10 /// Successful signer output with exact request and artifact provenance.
     11 #[non_exhaustive]
     12 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
     13 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
     14 #[derive(Clone, PartialEq, Eq)]
     15 pub struct SignReceipt {
     16     operation_kind: OperationId,
     17     intent_id: SigningIntentId,
     18     signer_request_id: SignerRequestId,
     19     signed_event: SignedEvent,
     20     completed_at_unix_ms: u64,
     21 }
     22 
     23 impl fmt::Debug for SignReceipt {
     24     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     25         formatter
     26             .debug_struct("SignReceipt")
     27             .field("operation_kind", &self.operation_kind)
     28             .field("intent_id", &self.intent_id)
     29             .field("signer_request_id", &self.signer_request_id)
     30             .field("signed_event_id", &self.signed_event.id_str())
     31             .field("completed_at_unix_ms", &self.completed_at_unix_ms)
     32             .finish()
     33     }
     34 }
     35 
     36 impl SignReceipt {
     37     /// Verifies exact plan fields, raw/wire coherence, event ID, signature,
     38     /// deadline, cancellation, and request identity before success exists.
     39     pub fn from_signed_event(
     40         request: &SignRequest,
     41         signed_event: SignedEvent,
     42         completed_at_unix_ms: u64,
     43     ) -> Result<Self, Error> {
     44         request.ensure_active(completed_at_unix_ms)?;
     45         verify_signed_event(&signed_event, request)?;
     46         Ok(Self {
     47             operation_kind: request.operation_kind(),
     48             intent_id: request.intent_id(),
     49             signer_request_id: request.signer_request_id(),
     50             signed_event,
     51             completed_at_unix_ms,
     52         })
     53     }
     54 
     55     #[must_use]
     56     pub const fn operation_kind(&self) -> OperationId {
     57         self.operation_kind
     58     }
     59 
     60     #[must_use]
     61     pub const fn intent_id(&self) -> SigningIntentId {
     62         self.intent_id
     63     }
     64 
     65     #[must_use]
     66     pub const fn signer_request_id(&self) -> SignerRequestId {
     67         self.signer_request_id
     68     }
     69 
     70     #[must_use]
     71     pub const fn signed_event(&self) -> &SignedEvent {
     72         &self.signed_event
     73     }
     74 
     75     #[must_use]
     76     pub const fn completed_at_unix_ms(&self) -> u64 {
     77         self.completed_at_unix_ms
     78     }
     79 }
     80 
     81 fn verify_exact_plan(event: &SignedEvent, request: &SignRequest) -> Result<(), Error> {
     82     if event.pubkey() != request.expected_author()
     83         || event.created_at() != request.created_at()
     84         || event.kind() != request.kind()
     85         || event.tags_as_vec() != request.tags()
     86         || event.content() != request.content()
     87         || event.id() != request.expected_event_id()
     88     {
     89         return Err(Error::new(Kind::SignerOutputInvalid));
     90     }
     91     // `SignedEvent` construction proves its retained raw JSON parses to this
     92     // exact wire value; the checks above bind that wire to the request plan.
     93     Ok(())
     94 }
     95 
     96 /// Verified authored signature facts, independent of whether a caller is waiting.
     97 ///
     98 /// This is neither an active success receipt nor authority to sign, admit or
     99 /// deliver an event. It cannot represent an expiring Blossom credential.
    100 /// Persistence callers must use [`Self::revalidate`] with their retained request
    101 /// and locally observed time before recording the evidence.
    102 ///
    103 /// Evidence cannot be deserialized without cryptographic verification:
    104 /// ```compile_fail
    105 /// fn decode<'a, T: serde::Deserialize<'a>>() {}
    106 /// decode::<radroots_signing::AuthoredSignEvidence>();
    107 /// ```
    108 #[non_exhaustive]
    109 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
    110 #[derive(Clone, PartialEq, Eq)]
    111 pub struct AuthoredSignEvidence {
    112     operation_kind: OperationId,
    113     intent_id: SigningIntentId,
    114     signer_request_id: SignerRequestId,
    115     signed_event: SignedEvent,
    116     observed_at_unix_ms: u64,
    117 }
    118 
    119 impl AuthoredSignEvidence {
    120     /// Verifies an already-created authored event against its originating request.
    121     ///
    122     /// Deadline and cancellation affect scheduling, not the truth of retained
    123     /// signature facts. Observation time must be positive and is not proof of
    124     /// when the signature was created. This constructor performs no signing.
    125     pub fn from_signed_event(
    126         request: &SignRequest,
    127         signed_event: SignedEvent,
    128         observed_at_unix_ms: u64,
    129     ) -> Result<Self, Error> {
    130         if request.authored_plan().is_none() || observed_at_unix_ms == 0 {
    131             return Err(Error::new(Kind::InvalidArgument));
    132         }
    133         verify_signed_event(&signed_event, request)?;
    134         Ok(Self {
    135             operation_kind: request.operation_kind(),
    136             intent_id: request.intent_id(),
    137             signer_request_id: request.signer_request_id(),
    138             signed_event,
    139             observed_at_unix_ms,
    140         })
    141     }
    142 
    143     /// Rebinds verified evidence to a caller's exact retained request and clock.
    144     ///
    145     /// A signature over the same event under a different operation or artifact
    146     /// is not evidence for this request. The original raw event bytes survive.
    147     pub fn revalidate(
    148         &self,
    149         request: &SignRequest,
    150         observed_at_unix_ms: u64,
    151     ) -> Result<Self, Error> {
    152         verify_identity(
    153             self.operation_kind,
    154             self.intent_id,
    155             self.signer_request_id,
    156             request,
    157         )?;
    158         Self::from_signed_event(request, self.signed_event.clone(), observed_at_unix_ms)
    159     }
    160 
    161     #[must_use]
    162     pub const fn operation_kind(&self) -> OperationId {
    163         self.operation_kind
    164     }
    165 
    166     #[must_use]
    167     pub const fn intent_id(&self) -> SigningIntentId {
    168         self.intent_id
    169     }
    170 
    171     #[must_use]
    172     pub const fn signer_request_id(&self) -> SignerRequestId {
    173         self.signer_request_id
    174     }
    175 
    176     #[must_use]
    177     pub const fn signed_event(&self) -> &SignedEvent {
    178         &self.signed_event
    179     }
    180 
    181     #[must_use]
    182     pub const fn observed_at_unix_ms(&self) -> u64 {
    183         self.observed_at_unix_ms
    184     }
    185 }
    186 
    187 impl fmt::Debug for AuthoredSignEvidence {
    188     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    189         formatter
    190             .debug_struct("AuthoredSignEvidence")
    191             .field("operation_kind", &self.operation_kind)
    192             .field("intent_id", &self.intent_id)
    193             .field("signer_request_id", &self.signer_request_id)
    194             .field("signed_event_id", &self.signed_event.id_str())
    195             .field("observed_at_unix_ms", &self.observed_at_unix_ms)
    196             .finish()
    197     }
    198 }
    199 
    200 pub(crate) fn verify_identity(
    201     operation_kind: OperationId,
    202     intent_id: SigningIntentId,
    203     signer_request_id: SignerRequestId,
    204     request: &SignRequest,
    205 ) -> Result<(), Error> {
    206     if operation_kind != request.operation_kind()
    207         || intent_id != request.intent_id()
    208         || signer_request_id != request.signer_request_id()
    209     {
    210         return Err(Error::new(Kind::SignerOutputInvalid));
    211     }
    212     Ok(())
    213 }
    214 
    215 fn verify_signed_event(event: &SignedEvent, request: &SignRequest) -> Result<(), Error> {
    216     verify_exact_plan(event, request)?;
    217     let id_verified = verify::id(RawEvent::new(event.envelope().clone()))
    218         .map_err(|_| Error::new(Kind::SignerOutputInvalid))?;
    219     verify::signature(id_verified, &Nip01SignatureVerifier)
    220         .map_err(|_| Error::new(Kind::SignerOutputInvalid))?;
    221     Ok(())
    222 }