myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

nip46_verification.rs (19737B)


      1 //! Cryptographic and typed verification for structurally admitted NIP-46 input.
      2 
      3 use core::fmt;
      4 use std::error::Error;
      5 
      6 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
      7 use nostr::{Event, Kind};
      8 use radroots_nostr_connect::{
      9     message::{RPC_KIND, RequestMessage},
     10     method::METHOD_MAX_BYTES,
     11 };
     12 use serde::Deserialize;
     13 
     14 use crate::{
     15     MycBoundedNip46Event, MycBoundedNip46Request, MycNip46ClientPublicKey, MycNip46EventId,
     16     MycNip46RequestId, MycProviderBinding, MycProviderPublicIdentity, MycProviderRole,
     17 };
     18 
     19 const NIP04_IV_BYTES: usize = 16;
     20 const NIP04_BLOCK_BYTES: usize = 16;
     21 const NIP44_VERSION_V2: u8 = 2;
     22 const NIP44_NONCE_BYTES: usize = 32;
     23 const NIP44_LENGTH_PREFIX_BYTES: usize = 2;
     24 const NIP44_HMAC_BYTES: usize = 32;
     25 const NIP44_MIN_PADDED_BYTES: usize = 32;
     26 const NIP44_MAX_PADDED_BYTES: usize = 65_536;
     27 
     28 /// Encryption envelope declared by one verified NIP-46 event.
     29 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
     30 pub enum MycNip46EncryptionContext {
     31     Nip04,
     32     Nip44V2,
     33 }
     34 
     35 /// Injected UTC second at which an inbound event is verified.
     36 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     37 pub struct MycNip46ObservedAtUnixSeconds(u64);
     38 
     39 impl MycNip46ObservedAtUnixSeconds {
     40     /// Validates a positive instant representable by SQLite's signed integer.
     41     pub fn new(value: u64) -> Result<Self, MycNip46VerificationError> {
     42         if value == 0 || i64::try_from(value).is_err() {
     43             return Err(verification_error(
     44                 MycNip46VerificationErrorKind::InvalidObservationTime,
     45             ));
     46         }
     47         Ok(Self(value))
     48     }
     49 
     50     /// Returns the injected UTC second.
     51     #[must_use]
     52     pub const fn get(self) -> u64 {
     53         self.0
     54     }
     55 }
     56 
     57 /// Explicit caller-selected authored-time window with no implicit default.
     58 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
     59 pub struct MycNip46AuthoredTimePolicy {
     60     maximum_past_seconds: u64,
     61     maximum_future_seconds: u64,
     62 }
     63 
     64 impl MycNip46AuthoredTimePolicy {
     65     /// Validates inclusive past and future tolerances.
     66     pub fn new(
     67         maximum_past_seconds: u64,
     68         maximum_future_seconds: u64,
     69     ) -> Result<Self, MycNip46VerificationError> {
     70         if i64::try_from(maximum_past_seconds).is_err()
     71             || i64::try_from(maximum_future_seconds).is_err()
     72         {
     73             return Err(verification_error(
     74                 MycNip46VerificationErrorKind::InvalidTimePolicy,
     75             ));
     76         }
     77         Ok(Self {
     78             maximum_past_seconds,
     79             maximum_future_seconds,
     80         })
     81     }
     82 
     83     /// Returns the inclusive maximum event age.
     84     #[must_use]
     85     pub const fn maximum_past_seconds(self) -> u64 {
     86         self.maximum_past_seconds
     87     }
     88 
     89     /// Returns the inclusive maximum future skew.
     90     #[must_use]
     91     pub const fn maximum_future_seconds(self) -> u64 {
     92         self.maximum_future_seconds
     93     }
     94 }
     95 
     96 /// Stable source-free classification for NIP-46 verification failures.
     97 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     98 pub enum MycNip46VerificationErrorKind {
     99     InvalidObservationTime,
    100     InvalidTimePolicy,
    101     InvalidTransportBinding,
    102     MalformedEvent,
    103     NonCanonicalEvent,
    104     InvalidKind,
    105     InvalidSender,
    106     InvalidReceiver,
    107     InvalidEventId,
    108     InvalidSignature,
    109     AuthoredTimeRejected,
    110     InvalidNip04Envelope,
    111     InvalidNip44Envelope,
    112     InvalidTypedRequest,
    113 }
    114 
    115 impl MycNip46VerificationErrorKind {
    116     const fn message(self) -> &'static str {
    117         match self {
    118             Self::InvalidObservationTime => "NIP-46 observation time is invalid",
    119             Self::InvalidTimePolicy => "NIP-46 authored-time policy is invalid",
    120             Self::InvalidTransportBinding => "NIP-46 transport binding is invalid",
    121             Self::MalformedEvent => "NIP-46 event cannot be decoded",
    122             Self::NonCanonicalEvent => "NIP-46 event encoding is not canonical",
    123             Self::InvalidKind => "NIP-46 event kind is invalid",
    124             Self::InvalidSender => "NIP-46 event sender is invalid",
    125             Self::InvalidReceiver => "NIP-46 event receiver is invalid",
    126             Self::InvalidEventId => "NIP-46 event identifier is invalid",
    127             Self::InvalidSignature => "NIP-46 event signature is invalid",
    128             Self::AuthoredTimeRejected => "NIP-46 event authored time is outside policy",
    129             Self::InvalidNip04Envelope => "NIP-46 NIP-04 envelope is invalid",
    130             Self::InvalidNip44Envelope => "NIP-46 NIP-44 envelope is invalid",
    131             Self::InvalidTypedRequest => "NIP-46 typed request is invalid",
    132         }
    133     }
    134 }
    135 
    136 /// One redacted NIP-46 verification failure.
    137 #[derive(Clone, Copy, PartialEq, Eq)]
    138 pub struct MycNip46VerificationError {
    139     kind: MycNip46VerificationErrorKind,
    140 }
    141 
    142 impl MycNip46VerificationError {
    143     /// Returns the stable failure classification.
    144     #[must_use]
    145     pub const fn kind(self) -> MycNip46VerificationErrorKind {
    146         self.kind
    147     }
    148 }
    149 
    150 impl fmt::Debug for MycNip46VerificationError {
    151     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    152         formatter
    153             .debug_struct("MycNip46VerificationError")
    154             .field("kind", &self.kind)
    155             .finish()
    156     }
    157 }
    158 
    159 impl fmt::Display for MycNip46VerificationError {
    160     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    161         formatter.write_str(self.kind.message())
    162     }
    163 }
    164 
    165 impl Error for MycNip46VerificationError {}
    166 
    167 /// One cryptographically verified encrypted NIP-46 event.
    168 pub struct MycVerifiedNip46Event {
    169     original: Box<[u8]>,
    170     encrypted_content: Box<str>,
    171     event_id: MycNip46EventId,
    172     client_public_key: MycNip46ClientPublicKey,
    173     receiver_public_key: MycProviderPublicIdentity,
    174     authored_at_unix_seconds: u64,
    175     encryption_context: MycNip46EncryptionContext,
    176 }
    177 
    178 impl MycVerifiedNip46Event {
    179     /// Returns the exact signed event bytes admitted by Step 141.
    180     #[must_use]
    181     pub fn original_bytes(&self) -> &[u8] {
    182         &self.original
    183     }
    184 
    185     /// Returns the verified event identity.
    186     #[must_use]
    187     pub const fn event_id(&self) -> MycNip46EventId {
    188         self.event_id
    189     }
    190 
    191     /// Returns the verified client transport identity.
    192     #[must_use]
    193     pub const fn client_public_key(&self) -> &MycNip46ClientPublicKey {
    194         &self.client_public_key
    195     }
    196 
    197     pub(crate) const fn receiver_public_key(&self) -> &MycProviderPublicIdentity {
    198         &self.receiver_public_key
    199     }
    200 
    201     /// Returns the verified event authored time.
    202     #[must_use]
    203     pub const fn authored_at_unix_seconds(&self) -> u64 {
    204         self.authored_at_unix_seconds
    205     }
    206 
    207     /// Returns the verified encrypted-envelope context.
    208     #[must_use]
    209     pub const fn encryption_context(&self) -> MycNip46EncryptionContext {
    210         self.encryption_context
    211     }
    212 
    213     /// Returns the bounded ciphertext for the later governed decryption boundary.
    214     #[must_use]
    215     pub fn encrypted_content(&self) -> &str {
    216         &self.encrypted_content
    217     }
    218 }
    219 
    220 impl fmt::Debug for MycVerifiedNip46Event {
    221     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    222         formatter
    223             .debug_struct("MycVerifiedNip46Event")
    224             .field("wire_bytes", &self.original.len())
    225             .field("ciphertext_bytes", &self.encrypted_content.len())
    226             .field("authored_at_unix_seconds", &self.authored_at_unix_seconds)
    227             .field("encryption_context", &self.encryption_context)
    228             .field("identity", &"[redacted]")
    229             .finish()
    230     }
    231 }
    232 
    233 /// One exact typed NIP-46 request decoded from admitted plaintext.
    234 pub struct MycVerifiedNip46Request {
    235     request_id: MycNip46RequestId,
    236     method: Box<str>,
    237     canonical_request: Box<[u8]>,
    238     parameter_count: usize,
    239     parameter_bytes: usize,
    240 }
    241 
    242 impl MycVerifiedNip46Request {
    243     /// Returns the validated request identity.
    244     #[must_use]
    245     pub const fn request_id(&self) -> &MycNip46RequestId {
    246         &self.request_id
    247     }
    248 
    249     /// Returns the canonical typed method spelling.
    250     #[must_use]
    251     pub fn method(&self) -> &str {
    252         &self.method
    253     }
    254 
    255     /// Returns the typed parameter count without exposing parameters.
    256     #[must_use]
    257     pub const fn parameter_count(&self) -> usize {
    258         self.parameter_count
    259     }
    260 
    261     /// Returns the aggregate typed parameter byte count.
    262     #[must_use]
    263     pub const fn parameter_bytes(&self) -> usize {
    264         self.parameter_bytes
    265     }
    266 
    267     pub(crate) fn canonical_request(&self) -> &[u8] {
    268         &self.canonical_request
    269     }
    270 }
    271 
    272 impl fmt::Debug for MycVerifiedNip46Request {
    273     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    274         formatter
    275             .debug_struct("MycVerifiedNip46Request")
    276             .field("method", &"[redacted]")
    277             .field("canonical_bytes", &self.canonical_request.len())
    278             .field("parameter_count", &self.parameter_count)
    279             .field("parameter_bytes", &self.parameter_bytes)
    280             .field("identity", &"[redacted]")
    281             .finish()
    282     }
    283 }
    284 
    285 /// Verifies one already bounded encrypted NIP-46 event without decrypting it.
    286 pub fn verify_myc_nip46_event(
    287     bounded: MycBoundedNip46Event,
    288     expected_transport: &MycProviderBinding,
    289     observed_at: MycNip46ObservedAtUnixSeconds,
    290     authored_time_policy: MycNip46AuthoredTimePolicy,
    291 ) -> Result<MycVerifiedNip46Event, MycNip46VerificationError> {
    292     if expected_transport.role() != MycProviderRole::Transport {
    293         return Err(verification_error(
    294             MycNip46VerificationErrorKind::InvalidTransportBinding,
    295         ));
    296     }
    297     let expected_receiver = expected_transport.expected_identity();
    298     let (original, encrypted_content) = bounded.into_verification_parts();
    299     let canonical: CanonicalEventFields<'_> = serde_json::from_slice(&original)
    300         .map_err(|_| verification_error(MycNip46VerificationErrorKind::MalformedEvent))?;
    301     let event: Event = serde_json::from_slice(&original)
    302         .map_err(|_| verification_error(MycNip46VerificationErrorKind::MalformedEvent))?;
    303 
    304     if canonical.id != event.id.to_hex()
    305         || canonical.pubkey != event.pubkey.to_hex()
    306         || canonical.sig != event.sig.to_string()
    307         || canonical.created_at != event.created_at.as_secs()
    308         || canonical.kind != u64::from(event.kind.as_u16())
    309         || canonical.content != event.content
    310     {
    311         return Err(verification_error(
    312             MycNip46VerificationErrorKind::NonCanonicalEvent,
    313         ));
    314     }
    315     if event.kind != Kind::Custom(RPC_KIND) {
    316         return Err(verification_error(
    317             MycNip46VerificationErrorKind::InvalidKind,
    318         ));
    319     }
    320     let client_public_key = MycNip46ClientPublicKey::new(&event.pubkey.to_hex())
    321         .map_err(|_| verification_error(MycNip46VerificationErrorKind::InvalidSender))?;
    322     if event.pubkey.to_hex() == expected_receiver.as_hex() {
    323         return Err(verification_error(
    324             MycNip46VerificationErrorKind::InvalidSender,
    325         ));
    326     }
    327     let expected_recipient = ["p", expected_receiver.as_hex()];
    328     if event.tags.len() != 1
    329         || event.tags.as_slice()[0]
    330             .as_slice()
    331             .iter()
    332             .map(String::as_str)
    333             .ne(expected_recipient)
    334     {
    335         return Err(verification_error(
    336             MycNip46VerificationErrorKind::InvalidReceiver,
    337         ));
    338     }
    339     if !event.verify_id() {
    340         return Err(verification_error(
    341             MycNip46VerificationErrorKind::InvalidEventId,
    342         ));
    343     }
    344     if !event.verify_signature() {
    345         return Err(verification_error(
    346             MycNip46VerificationErrorKind::InvalidSignature,
    347         ));
    348     }
    349     verify_authored_time(
    350         event.created_at.as_secs(),
    351         observed_at.get(),
    352         authored_time_policy,
    353     )?;
    354     let encryption_context = verify_encryption_context(&encrypted_content)?;
    355 
    356     Ok(MycVerifiedNip46Event {
    357         original,
    358         encrypted_content,
    359         event_id: MycNip46EventId::from_bytes(event.id.to_bytes()),
    360         client_public_key,
    361         receiver_public_key: expected_receiver.clone(),
    362         authored_at_unix_seconds: event.created_at.as_secs(),
    363         encryption_context,
    364     })
    365 }
    366 
    367 /// Decodes one already bounded plaintext request through the canonical NIP-46 model.
    368 pub fn verify_myc_nip46_request(
    369     bounded: MycBoundedNip46Request,
    370 ) -> Result<MycVerifiedNip46Request, MycNip46VerificationError> {
    371     let (plaintext, parameter_count, parameter_bytes) = bounded.into_verification_parts();
    372     let message: RequestMessage = serde_json::from_slice(&plaintext)
    373         .map_err(|_| verification_error(MycNip46VerificationErrorKind::InvalidTypedRequest))?;
    374     let request_id = message
    375         .request_id()
    376         .map_err(|_| verification_error(MycNip46VerificationErrorKind::InvalidTypedRequest))?;
    377     let request_id = MycNip46RequestId::new(request_id.as_str())
    378         .map_err(|_| verification_error(MycNip46VerificationErrorKind::InvalidTypedRequest))?;
    379     let method = message.payload().method().to_string();
    380     if method.is_empty() || method.len() > METHOD_MAX_BYTES {
    381         return Err(verification_error(
    382             MycNip46VerificationErrorKind::InvalidTypedRequest,
    383         ));
    384     }
    385     let canonical_request = serde_json::to_vec(&message)
    386         .map_err(|_| verification_error(MycNip46VerificationErrorKind::InvalidTypedRequest))?;
    387 
    388     Ok(MycVerifiedNip46Request {
    389         request_id,
    390         method: method.into_boxed_str(),
    391         canonical_request: canonical_request.into_boxed_slice(),
    392         parameter_count,
    393         parameter_bytes,
    394     })
    395 }
    396 
    397 #[derive(Deserialize)]
    398 #[serde(deny_unknown_fields)]
    399 struct CanonicalEventFields<'a> {
    400     id: &'a str,
    401     pubkey: &'a str,
    402     created_at: u64,
    403     kind: u64,
    404     #[serde(rename = "tags")]
    405     _tags: serde::de::IgnoredAny,
    406     content: &'a str,
    407     sig: &'a str,
    408 }
    409 
    410 fn verify_authored_time(
    411     authored_at: u64,
    412     observed_at: u64,
    413     policy: MycNip46AuthoredTimePolicy,
    414 ) -> Result<(), MycNip46VerificationError> {
    415     let earliest = observed_at.saturating_sub(policy.maximum_past_seconds());
    416     let latest = observed_at.saturating_add(policy.maximum_future_seconds());
    417     if !(earliest..=latest).contains(&authored_at) {
    418         return Err(verification_error(
    419             MycNip46VerificationErrorKind::AuthoredTimeRejected,
    420         ));
    421     }
    422     Ok(())
    423 }
    424 
    425 fn verify_encryption_context(
    426     ciphertext: &str,
    427 ) -> Result<MycNip46EncryptionContext, MycNip46VerificationError> {
    428     if ciphertext.contains("?iv=") {
    429         verify_nip04_envelope(ciphertext)?;
    430         Ok(MycNip46EncryptionContext::Nip04)
    431     } else {
    432         verify_nip44_envelope(ciphertext)?;
    433         Ok(MycNip46EncryptionContext::Nip44V2)
    434     }
    435 }
    436 
    437 fn verify_nip04_envelope(ciphertext: &str) -> Result<(), MycNip46VerificationError> {
    438     let (encrypted, iv) = ciphertext
    439         .split_once("?iv=")
    440         .ok_or_else(|| verification_error(MycNip46VerificationErrorKind::InvalidNip04Envelope))?;
    441     if encrypted.is_empty() || iv.is_empty() || iv.contains("?iv=") {
    442         return Err(verification_error(
    443             MycNip46VerificationErrorKind::InvalidNip04Envelope,
    444         ));
    445     }
    446     let encrypted = decode_canonical_base64(
    447         encrypted,
    448         MycNip46VerificationErrorKind::InvalidNip04Envelope,
    449     )?;
    450     let iv = decode_canonical_base64(iv, MycNip46VerificationErrorKind::InvalidNip04Envelope)?;
    451     if encrypted.is_empty()
    452         || !encrypted.len().is_multiple_of(NIP04_BLOCK_BYTES)
    453         || iv.len() != NIP04_IV_BYTES
    454     {
    455         return Err(verification_error(
    456             MycNip46VerificationErrorKind::InvalidNip04Envelope,
    457         ));
    458     }
    459     Ok(())
    460 }
    461 
    462 fn verify_nip44_envelope(ciphertext: &str) -> Result<(), MycNip46VerificationError> {
    463     let decoded = decode_canonical_base64(
    464         ciphertext,
    465         MycNip46VerificationErrorKind::InvalidNip44Envelope,
    466     )?;
    467     if decoded.first() != Some(&NIP44_VERSION_V2) {
    468         return Err(verification_error(
    469             MycNip46VerificationErrorKind::InvalidNip44Envelope,
    470         ));
    471     }
    472     let overhead = 1usize
    473         .checked_add(NIP44_NONCE_BYTES)
    474         .and_then(|value| value.checked_add(NIP44_LENGTH_PREFIX_BYTES))
    475         .and_then(|value| value.checked_add(NIP44_HMAC_BYTES))
    476         .expect("fixed NIP-44 overhead fits usize");
    477     let padded = decoded
    478         .len()
    479         .checked_sub(overhead)
    480         .ok_or_else(|| verification_error(MycNip46VerificationErrorKind::InvalidNip44Envelope))?;
    481     if !is_valid_nip44_padding_length(padded) {
    482         return Err(verification_error(
    483             MycNip46VerificationErrorKind::InvalidNip44Envelope,
    484         ));
    485     }
    486     Ok(())
    487 }
    488 
    489 fn decode_canonical_base64(
    490     encoded: &str,
    491     kind: MycNip46VerificationErrorKind,
    492 ) -> Result<Vec<u8>, MycNip46VerificationError> {
    493     let decoded = BASE64_STANDARD
    494         .decode(encoded)
    495         .map_err(|_| verification_error(kind))?;
    496     if BASE64_STANDARD.encode(&decoded) != encoded {
    497         return Err(verification_error(kind));
    498     }
    499     Ok(decoded)
    500 }
    501 
    502 fn is_valid_nip44_padding_length(length: usize) -> bool {
    503     if !(NIP44_MIN_PADDED_BYTES..=NIP44_MAX_PADDED_BYTES).contains(&length) {
    504         return false;
    505     }
    506     if length <= 256 {
    507         return length.is_multiple_of(32);
    508     }
    509     let Some(next_power) = length.checked_next_power_of_two() else {
    510         return false;
    511     };
    512     let chunk = next_power / 8;
    513     length.is_multiple_of(chunk)
    514 }
    515 
    516 const fn verification_error(kind: MycNip46VerificationErrorKind) -> MycNip46VerificationError {
    517     MycNip46VerificationError { kind }
    518 }
    519 
    520 #[cfg(test)]
    521 mod tests {
    522     use super::*;
    523 
    524     #[test]
    525     fn authored_time_boundaries_are_inclusive_and_checked() {
    526         let policy = MycNip46AuthoredTimePolicy::new(10, 2).expect("policy");
    527         assert!(verify_authored_time(90, 100, policy).is_ok());
    528         assert!(verify_authored_time(102, 100, policy).is_ok());
    529         assert_eq!(
    530             verify_authored_time(89, 100, policy).unwrap_err().kind(),
    531             MycNip46VerificationErrorKind::AuthoredTimeRejected
    532         );
    533         assert_eq!(
    534             verify_authored_time(103, 100, policy).unwrap_err().kind(),
    535             MycNip46VerificationErrorKind::AuthoredTimeRejected
    536         );
    537     }
    538 
    539     #[test]
    540     fn every_protocol_padding_boundary_is_closed() {
    541         for valid in [32, 64, 96, 128, 160, 192, 224, 256, 320, 384, 65_536] {
    542             assert!(is_valid_nip44_padding_length(valid));
    543         }
    544         for invalid in [0, 31, 33, 257, 287, 288, 65_535, 65_537] {
    545             assert!(!is_valid_nip44_padding_length(invalid));
    546         }
    547     }
    548 
    549     #[test]
    550     fn every_error_is_fixed_and_source_free() {
    551         for kind in [
    552             MycNip46VerificationErrorKind::InvalidObservationTime,
    553             MycNip46VerificationErrorKind::InvalidTimePolicy,
    554             MycNip46VerificationErrorKind::InvalidTransportBinding,
    555             MycNip46VerificationErrorKind::MalformedEvent,
    556             MycNip46VerificationErrorKind::NonCanonicalEvent,
    557             MycNip46VerificationErrorKind::InvalidKind,
    558             MycNip46VerificationErrorKind::InvalidSender,
    559             MycNip46VerificationErrorKind::InvalidReceiver,
    560             MycNip46VerificationErrorKind::InvalidEventId,
    561             MycNip46VerificationErrorKind::InvalidSignature,
    562             MycNip46VerificationErrorKind::AuthoredTimeRejected,
    563             MycNip46VerificationErrorKind::InvalidNip04Envelope,
    564             MycNip46VerificationErrorKind::InvalidNip44Envelope,
    565             MycNip46VerificationErrorKind::InvalidTypedRequest,
    566         ] {
    567             let error = verification_error(kind);
    568             assert_eq!(error.kind(), kind);
    569             assert!(!error.to_string().is_empty());
    570             assert!(error.source().is_none());
    571         }
    572     }
    573 }