myc

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

state_request.rs (37892B)


      1 //! Durable NIP-46 request identity and idempotent admission.
      2 
      3 use core::fmt;
      4 use std::error::Error;
      5 
      6 use nostr::PublicKey;
      7 use radroots_service_sqlite::{
      8     ServiceSqliteTransaction, ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind,
      9 };
     10 use sha2::{Digest, Sha256};
     11 use sqlx::Row;
     12 
     13 use crate::{
     14     MycStateRepository, MycStateRepositoryError, MycStateRepositoryErrorKind,
     15     state_repository::{PersistedMetadata, RepositoryOperationError, require_expected_metadata},
     16 };
     17 
     18 /// Maximum UTF-8 byte length of a canonical NIP-46 request identifier.
     19 pub const MYC_NIP46_REQUEST_ID_MAX_UTF8_BYTES: usize = 128;
     20 
     21 /// Maximum canonical decrypted request bytes accepted by the state boundary.
     22 pub const MYC_NIP46_CANONICAL_REQUEST_MAX_BYTES: usize = 262_144;
     23 
     24 const REQUEST_DIGEST_DOMAIN: &[u8] = b"radroots.myc.nip46.request.v1\0";
     25 const REQUEST_IDENTITY_DOMAIN: &[u8] = b"radroots.myc.nip46.request_identity.v1\0";
     26 const OPERATION_ID_DOMAIN: &[u8] = b"radroots.myc.nip46.operation.v1\0";
     27 const CORRELATION_ID_DOMAIN: &[u8] = b"radroots.myc.nip46.correlation.v1\0";
     28 
     29 const READ_BY_DEDUP_SQL: &str = r#"SELECT
     30     CASE WHEN typeof(r.operation_id) = 'blob' AND length(r.operation_id) = 32
     31         THEN r.operation_id ELSE NULL END AS operation_id,
     32     CASE WHEN typeof(r.correlation_id) = 'blob' AND length(r.correlation_id) = 32
     33         THEN r.correlation_id ELSE NULL END AS correlation_id,
     34     CASE WHEN typeof(r.operation_nonce) = 'blob' AND length(r.operation_nonce) = 32
     35         THEN r.operation_nonce ELSE NULL END AS operation_nonce,
     36     CASE
     37         WHEN typeof(r.request_identity_sha256) = 'blob'
     38             AND length(r.request_identity_sha256) = 32
     39         THEN r.request_identity_sha256
     40         ELSE NULL
     41     END AS request_identity_sha256,
     42     CASE
     43         WHEN typeof(selected.request_sha256) = 'blob'
     44             AND length(selected.request_sha256) = 32
     45         THEN selected.request_sha256
     46         ELSE NULL
     47     END AS selected_request_sha256,
     48     CASE
     49         WHEN typeof(request_dedup.request_sha256) = 'blob'
     50             AND length(request_dedup.request_sha256) = 32
     51         THEN request_dedup.request_sha256
     52         ELSE NULL
     53     END AS logical_request_sha256,
     54     CASE
     55         WHEN typeof(request_dedup.operation_id) = 'blob'
     56             AND length(request_dedup.operation_id) = 32
     57         THEN request_dedup.operation_id
     58         ELSE NULL
     59     END AS logical_operation_id,
     60     CASE
     61         WHEN typeof(r.client_public_key) = 'text'
     62             AND length(CAST(r.client_public_key AS BLOB)) = 64
     63         THEN r.client_public_key
     64         ELSE NULL
     65     END AS client_public_key,
     66     CASE
     67         WHEN typeof(r.request_id) = 'text'
     68             AND length(CAST(r.request_id AS BLOB)) BETWEEN 1 AND 128
     69         THEN r.request_id
     70         ELSE NULL
     71     END AS request_id,
     72     CASE WHEN typeof(r.first_event_id) = 'blob' AND length(r.first_event_id) = 32
     73         THEN r.first_event_id ELSE NULL END AS first_event_id,
     74     CASE
     75         WHEN typeof(r.method) = 'text'
     76             AND length(CAST(r.method AS BLOB)) BETWEEN 1 AND 64
     77         THEN r.method
     78         ELSE NULL
     79     END AS method,
     80     CASE WHEN typeof(r.request_sha256) = 'blob' AND length(r.request_sha256) = 32
     81         THEN r.request_sha256 ELSE NULL END AS request_sha256,
     82     CASE
     83         WHEN typeof(r.received_at_unix_ms) = 'integer'
     84             AND r.received_at_unix_ms BETWEEN 1 AND 9223372036854775807
     85         THEN r.received_at_unix_ms
     86         ELSE NULL
     87     END AS received_at_unix_ms,
     88     CASE
     89         WHEN typeof(request_dedup.replay_count) = 'integer'
     90             AND request_dedup.replay_count BETWEEN 0 AND 9223372036854775807
     91         THEN request_dedup.replay_count
     92         ELSE NULL
     93     END AS replay_count,
     94     CASE
     95         WHEN typeof(request_dedup.conflict_count) = 'integer'
     96             AND request_dedup.conflict_count BETWEEN 0 AND 9223372036854775807
     97         THEN request_dedup.conflict_count
     98         ELSE NULL
     99     END AS conflict_count,
    100     CASE
    101         WHEN typeof(request_dedup.last_seen_at_unix_ms) = 'integer'
    102             AND request_dedup.last_seen_at_unix_ms BETWEEN 1 AND 9223372036854775807
    103         THEN request_dedup.last_seen_at_unix_ms
    104         ELSE NULL
    105     END AS last_seen_at_unix_ms
    106 FROM nip46_request_dedup AS selected
    107 JOIN nip46_requests AS r ON r.operation_id = selected.operation_id
    108 JOIN nip46_request_dedup AS request_dedup
    109   ON request_dedup.dedup_kind = 'request'
    110  AND request_dedup.identity_sha256 = r.request_identity_sha256
    111 WHERE selected.dedup_kind = ? AND selected.identity_sha256 = ?
    112 LIMIT 2"#;
    113 
    114 const INSERT_REQUEST_SQL: &str = r#"INSERT INTO nip46_requests (
    115     operation_id,
    116     correlation_id,
    117     operation_nonce,
    118     request_identity_sha256,
    119     client_public_key,
    120     request_id,
    121     first_event_id,
    122     method,
    123     request_sha256,
    124     received_at_unix_ms
    125 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#;
    126 
    127 const INSERT_DEDUP_SQL: &str = r#"INSERT INTO nip46_request_dedup (
    128     dedup_kind,
    129     identity_sha256,
    130     request_sha256,
    131     operation_id,
    132     replay_count,
    133     conflict_count,
    134     first_seen_at_unix_ms,
    135     last_seen_at_unix_ms
    136 ) VALUES (?, ?, ?, ?, ?, 0, ?, ?)"#;
    137 
    138 const RECORD_REPLAY_SQL: &str = r#"UPDATE nip46_request_dedup
    139 SET replay_count = replay_count + 1,
    140     last_seen_at_unix_ms = MAX(last_seen_at_unix_ms, ?)
    141 WHERE dedup_kind = ?
    142   AND identity_sha256 = ?
    143   AND replay_count < 9223372036854775807"#;
    144 
    145 const RECORD_CONFLICT_SQL: &str = r#"UPDATE nip46_request_dedup
    146 SET conflict_count = conflict_count + 1,
    147     last_seen_at_unix_ms = MAX(last_seen_at_unix_ms, ?)
    148 WHERE dedup_kind = ?
    149   AND identity_sha256 = ?
    150   AND conflict_count < 9223372036854775807"#;
    151 
    152 /// A bounded canonical NIP-46 request ID.
    153 #[derive(Clone, PartialEq, Eq, Hash)]
    154 pub struct MycNip46RequestId(Box<str>);
    155 
    156 impl MycNip46RequestId {
    157     /// Validates borrowed input before allocating an owned identifier.
    158     pub fn new(value: &str) -> Result<Self, MycSignerRequestError> {
    159         if value.is_empty()
    160             || value.len() > MYC_NIP46_REQUEST_ID_MAX_UTF8_BYTES
    161             || value.trim() != value
    162             || value.chars().any(char::is_control)
    163         {
    164             return Err(MycSignerRequestError::new(
    165                 MycSignerRequestErrorKind::InvalidRequestId,
    166             ));
    167         }
    168         Ok(Self(value.into()))
    169     }
    170 
    171     /// Returns the exact canonical request ID.
    172     #[must_use]
    173     pub fn as_str(&self) -> &str {
    174         &self.0
    175     }
    176 }
    177 
    178 impl fmt::Debug for MycNip46RequestId {
    179     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    180         formatter.write_str("MycNip46RequestId([redacted])")
    181     }
    182 }
    183 
    184 /// One validated NIP-46 client public key.
    185 #[derive(Clone, PartialEq, Eq, Hash)]
    186 pub struct MycNip46ClientPublicKey(Box<str>);
    187 
    188 impl MycNip46ClientPublicKey {
    189     /// Parses one canonical lowercase 32-byte x-only public key.
    190     pub fn new(value: &str) -> Result<Self, MycSignerRequestError> {
    191         if value.len() != 64
    192             || value
    193                 .bytes()
    194                 .any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
    195         {
    196             return Err(MycSignerRequestError::new(
    197                 MycSignerRequestErrorKind::InvalidClientIdentity,
    198             ));
    199         }
    200         let public_key = PublicKey::from_hex(value).map_err(|_| {
    201             MycSignerRequestError::new(MycSignerRequestErrorKind::InvalidClientIdentity)
    202         })?;
    203         if public_key.to_hex() != value {
    204             return Err(MycSignerRequestError::new(
    205                 MycSignerRequestErrorKind::InvalidClientIdentity,
    206             ));
    207         }
    208         Ok(Self(value.into()))
    209     }
    210 
    211     /// Returns the canonical public identity.
    212     #[must_use]
    213     pub fn as_hex(&self) -> &str {
    214         &self.0
    215     }
    216 }
    217 
    218 impl fmt::Debug for MycNip46ClientPublicKey {
    219     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    220         formatter.write_str("MycNip46ClientPublicKey([redacted])")
    221     }
    222 }
    223 
    224 macro_rules! redacted_digest {
    225     ($name:ident) => {
    226         #[derive(Clone, Copy, PartialEq, Eq, Hash)]
    227         pub struct $name([u8; 32]);
    228 
    229         impl $name {
    230             /// Returns the exact identity bytes.
    231             #[must_use]
    232             pub const fn as_bytes(&self) -> &[u8; 32] {
    233                 &self.0
    234             }
    235         }
    236 
    237         impl fmt::Debug for $name {
    238             fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    239                 formatter.write_str(concat!(stringify!($name), "([redacted])"))
    240             }
    241         }
    242     };
    243 }
    244 
    245 redacted_digest!(MycNip46EventId);
    246 redacted_digest!(MycSignerRequestDigest);
    247 redacted_digest!(MycSignerOperationId);
    248 redacted_digest!(MycSignerCorrelationId);
    249 
    250 impl MycSignerOperationId {
    251     pub(crate) const fn from_persisted(bytes: [u8; 32]) -> Self {
    252         Self(bytes)
    253     }
    254 }
    255 
    256 /// One-use entropy evidence for a new logical signer operation.
    257 ///
    258 /// The runtime obtains these bytes from its injected entropy source. Admission
    259 /// consumes the value, binds it to the logical request identity, and persists
    260 /// it so the resulting operation identity can be revalidated after restart.
    261 #[derive(PartialEq, Eq)]
    262 pub struct MycSignerOperationNonce([u8; 32]);
    263 
    264 impl MycSignerOperationNonce {
    265     /// Wraps exact bytes supplied by the injected entropy boundary.
    266     #[must_use]
    267     pub const fn from_injected_entropy(bytes: [u8; 32]) -> Self {
    268         Self(bytes)
    269     }
    270 }
    271 
    272 impl fmt::Debug for MycSignerOperationNonce {
    273     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    274         formatter.write_str("MycSignerOperationNonce([redacted])")
    275     }
    276 }
    277 
    278 impl MycNip46EventId {
    279     /// Constructs an event identity from an already verified NIP-01 event ID.
    280     #[must_use]
    281     pub const fn from_bytes(bytes: [u8; 32]) -> Self {
    282         Self(bytes)
    283     }
    284 }
    285 
    286 impl MycSignerRequestDigest {
    287     /// Hashes exact canonical decrypted request bytes under the Myc domain.
    288     pub fn for_canonical_request(bytes: &[u8]) -> Result<Self, MycSignerRequestError> {
    289         if bytes.is_empty() || bytes.len() > MYC_NIP46_CANONICAL_REQUEST_MAX_BYTES {
    290             return Err(MycSignerRequestError::new(
    291                 MycSignerRequestErrorKind::InvalidCanonicalRequest,
    292             ));
    293         }
    294         let mut hasher = Sha256::new();
    295         hasher.update(REQUEST_DIGEST_DOMAIN);
    296         hasher.update(
    297             u64::try_from(bytes.len())
    298                 .expect("bounded request length fits u64")
    299                 .to_be_bytes(),
    300         );
    301         hasher.update(bytes);
    302         Ok(Self(hasher.finalize().into()))
    303     }
    304 }
    305 
    306 /// Exact supported NIP-46 method identity.
    307 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
    308 pub enum MycSignerRequestMethod {
    309     Connect,
    310     GetPublicKey,
    311     GetSessionCapability,
    312     SignEvent,
    313     Nip04Encrypt,
    314     Nip04Decrypt,
    315     Nip44Encrypt,
    316     Nip44Decrypt,
    317     Ping,
    318     SwitchRelays,
    319     Logout,
    320 }
    321 
    322 impl MycSignerRequestMethod {
    323     /// Returns the canonical NIP-46 wire spelling.
    324     #[must_use]
    325     pub const fn as_str(self) -> &'static str {
    326         match self {
    327             Self::Connect => "connect",
    328             Self::GetPublicKey => "get_public_key",
    329             Self::GetSessionCapability => "get_session_capability",
    330             Self::SignEvent => "sign_event",
    331             Self::Nip04Encrypt => "nip04_encrypt",
    332             Self::Nip04Decrypt => "nip04_decrypt",
    333             Self::Nip44Encrypt => "nip44_encrypt",
    334             Self::Nip44Decrypt => "nip44_decrypt",
    335             Self::Ping => "ping",
    336             Self::SwitchRelays => "switch_relays",
    337             Self::Logout => "logout",
    338         }
    339     }
    340 
    341     pub(crate) fn parse(value: &str) -> Option<Self> {
    342         match value {
    343             "connect" => Some(Self::Connect),
    344             "get_public_key" => Some(Self::GetPublicKey),
    345             "get_session_capability" => Some(Self::GetSessionCapability),
    346             "sign_event" => Some(Self::SignEvent),
    347             "nip04_encrypt" => Some(Self::Nip04Encrypt),
    348             "nip04_decrypt" => Some(Self::Nip04Decrypt),
    349             "nip44_encrypt" => Some(Self::Nip44Encrypt),
    350             "nip44_decrypt" => Some(Self::Nip44Decrypt),
    351             "ping" => Some(Self::Ping),
    352             "switch_relays" => Some(Self::SwitchRelays),
    353             "logout" => Some(Self::Logout),
    354             _ => None,
    355         }
    356     }
    357 }
    358 
    359 /// Positive UTC millisecond instant at which a request reached admission.
    360 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    361 pub struct MycRequestReceivedAtUnixMs(u64);
    362 
    363 impl MycRequestReceivedAtUnixMs {
    364     /// Validates a positive instant representable by SQLite's signed integer.
    365     pub fn new(value: u64) -> Result<Self, MycSignerRequestError> {
    366         if value == 0 || i64::try_from(value).is_err() {
    367             return Err(MycSignerRequestError::new(
    368                 MycSignerRequestErrorKind::InvalidReceivedAt,
    369             ));
    370         }
    371         Ok(Self(value))
    372     }
    373 
    374     #[must_use]
    375     /// Returns the validated UTC millisecond instant.
    376     pub const fn get(self) -> u64 {
    377         self.0
    378     }
    379 
    380     fn sqlite_value(self) -> i64 {
    381         i64::try_from(self.0).expect("validated request time fits SQLite integer")
    382     }
    383 }
    384 
    385 /// Fully validated request-admission input.
    386 #[derive(PartialEq, Eq)]
    387 pub struct MycSignerRequest {
    388     client_public_key: MycNip46ClientPublicKey,
    389     request_id: MycNip46RequestId,
    390     event_id: MycNip46EventId,
    391     method: MycSignerRequestMethod,
    392     request_digest: MycSignerRequestDigest,
    393     request_identity: [u8; 32],
    394     operation_nonce: MycSignerOperationNonce,
    395     received_at: MycRequestReceivedAtUnixMs,
    396 }
    397 
    398 impl MycSignerRequest {
    399     /// Binds validated input to one caller-supplied injected-entropy value.
    400     #[must_use]
    401     pub fn new(
    402         client_public_key: MycNip46ClientPublicKey,
    403         request_id: MycNip46RequestId,
    404         event_id: MycNip46EventId,
    405         method: MycSignerRequestMethod,
    406         request_digest: MycSignerRequestDigest,
    407         operation_nonce: MycSignerOperationNonce,
    408         received_at: MycRequestReceivedAtUnixMs,
    409     ) -> Self {
    410         let request_identity = derive_request_identity(&client_public_key, &request_id);
    411         Self {
    412             client_public_key,
    413             request_id,
    414             event_id,
    415             method,
    416             request_digest,
    417             request_identity,
    418             operation_nonce,
    419             received_at,
    420         }
    421     }
    422 
    423     fn owned(&self) -> Self {
    424         Self {
    425             client_public_key: self.client_public_key.clone(),
    426             request_id: self.request_id.clone(),
    427             event_id: self.event_id,
    428             method: self.method,
    429             request_digest: self.request_digest,
    430             request_identity: self.request_identity,
    431             operation_nonce: MycSignerOperationNonce(self.operation_nonce.0),
    432             received_at: self.received_at,
    433         }
    434     }
    435 
    436     pub(crate) const fn client_public_key(&self) -> &MycNip46ClientPublicKey {
    437         &self.client_public_key
    438     }
    439 
    440     pub(crate) const fn request_id(&self) -> &MycNip46RequestId {
    441         &self.request_id
    442     }
    443 
    444     pub(crate) const fn event_id(&self) -> MycNip46EventId {
    445         self.event_id
    446     }
    447 
    448     pub(crate) const fn method(&self) -> MycSignerRequestMethod {
    449         self.method
    450     }
    451 
    452     pub(crate) const fn request_digest(&self) -> MycSignerRequestDigest {
    453         self.request_digest
    454     }
    455 
    456     fn derived_operation_id(&self) -> MycSignerOperationId {
    457         MycSignerOperationId(derive_operation_id(
    458             &self.request_identity,
    459             &self.operation_nonce,
    460         ))
    461     }
    462 
    463     fn derived_correlation_id(&self) -> MycSignerCorrelationId {
    464         MycSignerCorrelationId(derive_digest(
    465             CORRELATION_ID_DOMAIN,
    466             self.derived_operation_id().as_bytes(),
    467         ))
    468     }
    469 
    470     #[cfg(test)]
    471     pub(crate) fn admitted_record_for_test(&self) -> MycSignerRequestRecord {
    472         MycSignerRequestRecord {
    473             operation_id: self.derived_operation_id(),
    474             correlation_id: self.derived_correlation_id(),
    475             client_public_key: self.client_public_key.clone(),
    476             request_id: self.request_id.clone(),
    477             first_event_id: self.event_id,
    478             method: self.method,
    479             request_digest: self.request_digest,
    480             received_at: self.received_at,
    481             replay_count: 0,
    482             conflict_count: 0,
    483             last_seen_at: self.received_at,
    484         }
    485     }
    486 }
    487 
    488 impl fmt::Debug for MycSignerRequest {
    489     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    490         formatter.write_str("MycSignerRequest([redacted])")
    491     }
    492 }
    493 
    494 /// Durable state of one accepted logical NIP-46 request.
    495 #[derive(Clone, PartialEq, Eq)]
    496 pub struct MycSignerRequestRecord {
    497     operation_id: MycSignerOperationId,
    498     correlation_id: MycSignerCorrelationId,
    499     client_public_key: MycNip46ClientPublicKey,
    500     request_id: MycNip46RequestId,
    501     first_event_id: MycNip46EventId,
    502     method: MycSignerRequestMethod,
    503     request_digest: MycSignerRequestDigest,
    504     received_at: MycRequestReceivedAtUnixMs,
    505     replay_count: u64,
    506     conflict_count: u64,
    507     last_seen_at: MycRequestReceivedAtUnixMs,
    508 }
    509 
    510 impl MycSignerRequestRecord {
    511     pub(crate) const fn client_public_key(&self) -> &MycNip46ClientPublicKey {
    512         &self.client_public_key
    513     }
    514 
    515     pub(crate) const fn request_id(&self) -> &MycNip46RequestId {
    516         &self.request_id
    517     }
    518 
    519     #[must_use]
    520     /// Returns the stable logical operation identity.
    521     pub const fn operation_id(&self) -> MycSignerOperationId {
    522         self.operation_id
    523     }
    524 
    525     #[must_use]
    526     /// Returns the stable correlation identity.
    527     pub const fn correlation_id(&self) -> MycSignerCorrelationId {
    528         self.correlation_id
    529     }
    530 
    531     #[must_use]
    532     /// Returns the closed request method recorded for the operation.
    533     pub const fn method(&self) -> MycSignerRequestMethod {
    534         self.method
    535     }
    536 
    537     #[must_use]
    538     /// Returns the number of accepted exact replays.
    539     pub const fn replay_count(&self) -> u64 {
    540         self.replay_count
    541     }
    542 
    543     #[must_use]
    544     /// Returns the number of rejected conflicting reuses.
    545     pub const fn conflict_count(&self) -> u64 {
    546         self.conflict_count
    547     }
    548 
    549     pub(crate) fn matches_request(&self, request: &MycSignerRequest) -> bool {
    550         self.operation_id == request.derived_operation_id()
    551             && self.correlation_id == request.derived_correlation_id()
    552             && &self.client_public_key == request.client_public_key()
    553             && &self.request_id == request.request_id()
    554             && self.first_event_id == request.event_id()
    555             && self.method == request.method()
    556             && self.request_digest == request.request_digest()
    557             && self.received_at == request.received_at
    558     }
    559 
    560     pub(crate) const fn received_at(&self) -> MycRequestReceivedAtUnixMs {
    561         self.received_at
    562     }
    563 }
    564 
    565 impl fmt::Debug for MycSignerRequestRecord {
    566     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    567         formatter
    568             .debug_struct("MycSignerRequestRecord")
    569             .field("method", &self.method)
    570             .field("replay_count", &self.replay_count)
    571             .field("conflict_count", &self.conflict_count)
    572             .field("identity", &"[redacted]")
    573             .finish()
    574     }
    575 }
    576 
    577 /// Idempotent result of durable request admission.
    578 #[derive(Clone, PartialEq, Eq)]
    579 pub enum MycSignerRequestAdmission {
    580     Admitted(MycSignerRequestRecord),
    581     ExactReplay(MycSignerRequestRecord),
    582     ConflictingReuse(MycSignerRequestRecord),
    583 }
    584 
    585 impl MycSignerRequestAdmission {
    586     #[must_use]
    587     /// Returns the durable record associated with the admission outcome.
    588     pub const fn record(&self) -> &MycSignerRequestRecord {
    589         match self {
    590             Self::Admitted(record) | Self::ExactReplay(record) | Self::ConflictingReuse(record) => {
    591                 record
    592             }
    593         }
    594     }
    595 }
    596 
    597 impl fmt::Debug for MycSignerRequestAdmission {
    598     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    599         formatter.write_str(match self {
    600             Self::Admitted(_) => "MycSignerRequestAdmission::Admitted([redacted])",
    601             Self::ExactReplay(_) => "MycSignerRequestAdmission::ExactReplay([redacted])",
    602             Self::ConflictingReuse(_) => "MycSignerRequestAdmission::ConflictingReuse([redacted])",
    603         })
    604     }
    605 }
    606 
    607 /// Stable source-free input failure for request construction.
    608 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    609 pub enum MycSignerRequestErrorKind {
    610     InvalidRequestId,
    611     InvalidClientIdentity,
    612     InvalidCanonicalRequest,
    613     InvalidReceivedAt,
    614 }
    615 
    616 impl MycSignerRequestErrorKind {
    617     #[must_use]
    618     /// Returns the stable machine-readable classification.
    619     pub const fn code(self) -> &'static str {
    620         match self {
    621             Self::InvalidRequestId => "signer_request_id_invalid",
    622             Self::InvalidClientIdentity => "signer_request_client_identity_invalid",
    623             Self::InvalidCanonicalRequest => "signer_request_payload_invalid",
    624             Self::InvalidReceivedAt => "signer_request_time_invalid",
    625         }
    626     }
    627 }
    628 
    629 #[derive(Clone, Copy, PartialEq, Eq)]
    630 pub struct MycSignerRequestError {
    631     kind: MycSignerRequestErrorKind,
    632 }
    633 
    634 impl MycSignerRequestError {
    635     const fn new(kind: MycSignerRequestErrorKind) -> Self {
    636         Self { kind }
    637     }
    638 
    639     #[must_use]
    640     /// Returns the stable failure class.
    641     pub const fn kind(self) -> MycSignerRequestErrorKind {
    642         self.kind
    643     }
    644 
    645     #[must_use]
    646     /// Returns the stable machine-readable failure code.
    647     pub const fn code(self) -> &'static str {
    648         self.kind.code()
    649     }
    650 }
    651 
    652 impl fmt::Display for MycSignerRequestError {
    653     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    654         formatter.write_str(match self.kind {
    655             MycSignerRequestErrorKind::InvalidRequestId => "NIP-46 request ID is invalid",
    656             MycSignerRequestErrorKind::InvalidClientIdentity => "NIP-46 client identity is invalid",
    657             MycSignerRequestErrorKind::InvalidCanonicalRequest => {
    658                 "canonical NIP-46 request is invalid"
    659             }
    660             MycSignerRequestErrorKind::InvalidReceivedAt => "NIP-46 request time is invalid",
    661         })
    662     }
    663 }
    664 
    665 impl fmt::Debug for MycSignerRequestError {
    666     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    667         formatter
    668             .debug_struct("MycSignerRequestError")
    669             .field("kind", &self.kind)
    670             .finish()
    671     }
    672 }
    673 
    674 impl Error for MycSignerRequestError {}
    675 
    676 impl MycStateRepository<'_> {
    677     /// Atomically admits a new request, replays an exact request, or records conflict.
    678     pub async fn admit_signer_request(
    679         &self,
    680         request: &MycSignerRequest,
    681     ) -> Result<MycSignerRequestAdmission, MycStateRepositoryError> {
    682         let request = request.owned();
    683         let expected = PersistedMetadata::from(self.expected());
    684         self.host()
    685             .transaction(move |transaction| {
    686                 Box::pin(async move {
    687                     require_expected_metadata(transaction, &expected)
    688                         .await
    689                         .map_err(|error| match error {
    690                             RepositoryOperationError::Binding => RequestOperationError::Binding,
    691                             RepositoryOperationError::Storage => RequestOperationError::Storage,
    692                         })?;
    693                     admit_request(transaction, &request).await
    694                 })
    695             })
    696             .await
    697             .map_err(map_transaction_error)
    698     }
    699 }
    700 
    701 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    702 enum RequestOperationError {
    703     Binding,
    704     Storage,
    705 }
    706 
    707 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    708 enum DedupKind {
    709     Request,
    710     Event,
    711 }
    712 
    713 impl DedupKind {
    714     const fn as_str(self) -> &'static str {
    715         match self {
    716             Self::Request => "request",
    717             Self::Event => "event",
    718         }
    719     }
    720 }
    721 
    722 async fn admit_request(
    723     transaction: &mut ServiceSqliteTransaction<'_>,
    724     request: &MycSignerRequest,
    725 ) -> Result<MycSignerRequestAdmission, RequestOperationError> {
    726     let request_match =
    727         read_by_dedup(transaction, DedupKind::Request, &request.request_identity).await?;
    728     let event_match =
    729         read_by_dedup(transaction, DedupKind::Event, request.event_id.as_bytes()).await?;
    730 
    731     match (request_match, event_match) {
    732         (None, None) => insert_new_request(transaction, request).await,
    733         (Some(existing), event) if exact_request(&existing, request) => {
    734             if let Some(event) = event {
    735                 if event.operation_id != existing.operation_id {
    736                     record_conflict(
    737                         transaction,
    738                         DedupKind::Request,
    739                         &request.request_identity,
    740                         request.received_at,
    741                     )
    742                     .await?;
    743                     return read_outcome(
    744                         transaction,
    745                         DedupKind::Request,
    746                         &request.request_identity,
    747                         MycSignerRequestAdmission::ConflictingReuse,
    748                     )
    749                     .await;
    750                 }
    751                 record_replay(
    752                     transaction,
    753                     DedupKind::Event,
    754                     request.event_id.as_bytes(),
    755                     request.received_at,
    756                 )
    757                 .await?;
    758             } else {
    759                 insert_dedup(
    760                     transaction,
    761                     DedupKind::Event,
    762                     request.event_id.as_bytes(),
    763                     request.request_digest.as_bytes(),
    764                     existing.operation_id.as_bytes(),
    765                     0,
    766                     request.received_at,
    767                 )
    768                 .await?;
    769             }
    770             record_replay(
    771                 transaction,
    772                 DedupKind::Request,
    773                 &request.request_identity,
    774                 request.received_at,
    775             )
    776             .await?;
    777             read_outcome(
    778                 transaction,
    779                 DedupKind::Request,
    780                 &request.request_identity,
    781                 MycSignerRequestAdmission::ExactReplay,
    782             )
    783             .await
    784         }
    785         (Some(_), _) => {
    786             record_conflict(
    787                 transaction,
    788                 DedupKind::Request,
    789                 &request.request_identity,
    790                 request.received_at,
    791             )
    792             .await?;
    793             read_outcome(
    794                 transaction,
    795                 DedupKind::Request,
    796                 &request.request_identity,
    797                 MycSignerRequestAdmission::ConflictingReuse,
    798             )
    799             .await
    800         }
    801         (None, Some(_)) => {
    802             record_conflict(
    803                 transaction,
    804                 DedupKind::Event,
    805                 request.event_id.as_bytes(),
    806                 request.received_at,
    807             )
    808             .await?;
    809             let record = read_by_dedup(transaction, DedupKind::Event, request.event_id.as_bytes())
    810                 .await?
    811                 .ok_or(RequestOperationError::Binding)?;
    812             Ok(MycSignerRequestAdmission::ConflictingReuse(record))
    813         }
    814     }
    815 }
    816 
    817 async fn insert_new_request(
    818     transaction: &mut ServiceSqliteTransaction<'_>,
    819     request: &MycSignerRequest,
    820 ) -> Result<MycSignerRequestAdmission, RequestOperationError> {
    821     let operation_id = MycSignerOperationId(derive_operation_id(
    822         &request.request_identity,
    823         &request.operation_nonce,
    824     ));
    825     let correlation_id = MycSignerCorrelationId(derive_digest(
    826         CORRELATION_ID_DOMAIN,
    827         operation_id.as_bytes(),
    828     ));
    829     let result = sqlx::query(INSERT_REQUEST_SQL)
    830         .bind(operation_id.as_bytes().as_slice())
    831         .bind(correlation_id.as_bytes().as_slice())
    832         .bind(request.operation_nonce.0.as_slice())
    833         .bind(request.request_identity.as_slice())
    834         .bind(request.client_public_key.as_hex())
    835         .bind(request.request_id.as_str())
    836         .bind(request.event_id.as_bytes().as_slice())
    837         .bind(request.method.as_str())
    838         .bind(request.request_digest.as_bytes().as_slice())
    839         .bind(request.received_at.sqlite_value())
    840         .execute(&mut *transaction)
    841         .await
    842         .map_err(|_| RequestOperationError::Storage)?;
    843     if result.rows_affected() != 1 {
    844         return Err(RequestOperationError::Storage);
    845     }
    846     insert_dedup(
    847         transaction,
    848         DedupKind::Request,
    849         &request.request_identity,
    850         request.request_digest.as_bytes(),
    851         operation_id.as_bytes(),
    852         0,
    853         request.received_at,
    854     )
    855     .await?;
    856     insert_dedup(
    857         transaction,
    858         DedupKind::Event,
    859         request.event_id.as_bytes(),
    860         request.request_digest.as_bytes(),
    861         operation_id.as_bytes(),
    862         0,
    863         request.received_at,
    864     )
    865     .await?;
    866     read_outcome(
    867         transaction,
    868         DedupKind::Request,
    869         &request.request_identity,
    870         MycSignerRequestAdmission::Admitted,
    871     )
    872     .await
    873 }
    874 
    875 async fn insert_dedup(
    876     transaction: &mut ServiceSqliteTransaction<'_>,
    877     kind: DedupKind,
    878     identity: &[u8; 32],
    879     request_digest: &[u8; 32],
    880     operation_id: &[u8; 32],
    881     replay_count: i64,
    882     received_at: MycRequestReceivedAtUnixMs,
    883 ) -> Result<(), RequestOperationError> {
    884     let result = sqlx::query(INSERT_DEDUP_SQL)
    885         .bind(kind.as_str())
    886         .bind(identity.as_slice())
    887         .bind(request_digest.as_slice())
    888         .bind(operation_id.as_slice())
    889         .bind(replay_count)
    890         .bind(received_at.sqlite_value())
    891         .bind(received_at.sqlite_value())
    892         .execute(&mut *transaction)
    893         .await
    894         .map_err(|_| RequestOperationError::Storage)?;
    895     (result.rows_affected() == 1)
    896         .then_some(())
    897         .ok_or(RequestOperationError::Storage)
    898 }
    899 
    900 async fn record_replay(
    901     transaction: &mut ServiceSqliteTransaction<'_>,
    902     kind: DedupKind,
    903     identity: &[u8; 32],
    904     received_at: MycRequestReceivedAtUnixMs,
    905 ) -> Result<(), RequestOperationError> {
    906     update_counter(transaction, RECORD_REPLAY_SQL, kind, identity, received_at).await
    907 }
    908 
    909 async fn record_conflict(
    910     transaction: &mut ServiceSqliteTransaction<'_>,
    911     kind: DedupKind,
    912     identity: &[u8; 32],
    913     received_at: MycRequestReceivedAtUnixMs,
    914 ) -> Result<(), RequestOperationError> {
    915     update_counter(
    916         transaction,
    917         RECORD_CONFLICT_SQL,
    918         kind,
    919         identity,
    920         received_at,
    921     )
    922     .await
    923 }
    924 
    925 async fn update_counter(
    926     transaction: &mut ServiceSqliteTransaction<'_>,
    927     sql: &'static str,
    928     kind: DedupKind,
    929     identity: &[u8; 32],
    930     received_at: MycRequestReceivedAtUnixMs,
    931 ) -> Result<(), RequestOperationError> {
    932     let result = sqlx::query(sql)
    933         .bind(received_at.sqlite_value())
    934         .bind(kind.as_str())
    935         .bind(identity.as_slice())
    936         .execute(&mut *transaction)
    937         .await
    938         .map_err(|_| RequestOperationError::Storage)?;
    939     (result.rows_affected() == 1)
    940         .then_some(())
    941         .ok_or(RequestOperationError::Storage)
    942 }
    943 
    944 async fn read_outcome(
    945     transaction: &mut ServiceSqliteTransaction<'_>,
    946     kind: DedupKind,
    947     identity: &[u8; 32],
    948     outcome: fn(MycSignerRequestRecord) -> MycSignerRequestAdmission,
    949 ) -> Result<MycSignerRequestAdmission, RequestOperationError> {
    950     read_by_dedup(transaction, kind, identity)
    951         .await?
    952         .map(outcome)
    953         .ok_or(RequestOperationError::Binding)
    954 }
    955 
    956 async fn read_by_dedup(
    957     transaction: &mut ServiceSqliteTransaction<'_>,
    958     kind: DedupKind,
    959     identity: &[u8; 32],
    960 ) -> Result<Option<MycSignerRequestRecord>, RequestOperationError> {
    961     let rows = sqlx::query(READ_BY_DEDUP_SQL)
    962         .bind(kind.as_str())
    963         .bind(identity.as_slice())
    964         .fetch_all(&mut *transaction)
    965         .await
    966         .map_err(|_| RequestOperationError::Storage)?;
    967     if rows.len() > 1 {
    968         return Err(RequestOperationError::Binding);
    969     }
    970     rows.first()
    971         .map(|row| parse_record(row, kind, identity))
    972         .transpose()
    973 }
    974 
    975 fn parse_record(
    976     row: &sqlx::sqlite::SqliteRow,
    977     selected_kind: DedupKind,
    978     selected_identity: &[u8; 32],
    979 ) -> Result<MycSignerRequestRecord, RequestOperationError> {
    980     let operation_id = MycSignerOperationId(exact_digest(row, "operation_id")?);
    981     let correlation_id = MycSignerCorrelationId(exact_digest(row, "correlation_id")?);
    982     let operation_nonce = exact_digest(row, "operation_nonce")?;
    983     let request_identity = exact_digest(row, "request_identity_sha256")?;
    984     let selected_request_digest = exact_digest(row, "selected_request_sha256")?;
    985     let logical_request_digest = exact_digest(row, "logical_request_sha256")?;
    986     let logical_operation_id = exact_digest(row, "logical_operation_id")?;
    987     let request_digest = exact_digest(row, "request_sha256")?;
    988     let client_public_key = MycNip46ClientPublicKey::new(bounded_text(row, "client_public_key")?)
    989         .map_err(|_| RequestOperationError::Binding)?;
    990     let request_id = MycNip46RequestId::new(bounded_text(row, "request_id")?)
    991         .map_err(|_| RequestOperationError::Binding)?;
    992     if (selected_kind == DedupKind::Request && selected_identity != &request_identity)
    993         || selected_request_digest != request_digest
    994         || logical_request_digest != request_digest
    995         || logical_operation_id != *operation_id.as_bytes()
    996         || derive_request_identity(&client_public_key, &request_id) != request_identity
    997         || derive_operation_id(&request_identity, &MycSignerOperationNonce(operation_nonce))
    998             != *operation_id.as_bytes()
    999         || derive_digest(CORRELATION_ID_DOMAIN, operation_id.as_bytes())
   1000             != *correlation_id.as_bytes()
   1001     {
   1002         return Err(RequestOperationError::Binding);
   1003     }
   1004     let method = MycSignerRequestMethod::parse(bounded_text(row, "method")?)
   1005         .ok_or(RequestOperationError::Binding)?;
   1006     let received_at = bounded_time(row, "received_at_unix_ms")?;
   1007     let last_seen_at = bounded_time(row, "last_seen_at_unix_ms")?;
   1008     if last_seen_at < received_at {
   1009         return Err(RequestOperationError::Binding);
   1010     }
   1011     Ok(MycSignerRequestRecord {
   1012         operation_id,
   1013         correlation_id,
   1014         client_public_key,
   1015         request_id,
   1016         first_event_id: MycNip46EventId(exact_digest(row, "first_event_id")?),
   1017         method,
   1018         request_digest: MycSignerRequestDigest(request_digest),
   1019         received_at,
   1020         replay_count: bounded_count(row, "replay_count")?,
   1021         conflict_count: bounded_count(row, "conflict_count")?,
   1022         last_seen_at,
   1023     })
   1024 }
   1025 
   1026 fn exact_request(existing: &MycSignerRequestRecord, request: &MycSignerRequest) -> bool {
   1027     existing.client_public_key == request.client_public_key
   1028         && existing.request_id == request.request_id
   1029         && existing.method == request.method
   1030         && existing.request_digest == request.request_digest
   1031 }
   1032 
   1033 fn exact_digest(
   1034     row: &sqlx::sqlite::SqliteRow,
   1035     column: &str,
   1036 ) -> Result<[u8; 32], RequestOperationError> {
   1037     row.try_get::<Option<Vec<u8>>, _>(column)
   1038         .map_err(|_| RequestOperationError::Binding)?
   1039         .ok_or(RequestOperationError::Binding)?
   1040         .try_into()
   1041         .map_err(|_| RequestOperationError::Binding)
   1042 }
   1043 
   1044 fn bounded_text<'row>(
   1045     row: &'row sqlx::sqlite::SqliteRow,
   1046     column: &str,
   1047 ) -> Result<&'row str, RequestOperationError> {
   1048     row.try_get::<Option<&str>, _>(column)
   1049         .map_err(|_| RequestOperationError::Binding)?
   1050         .ok_or(RequestOperationError::Binding)
   1051 }
   1052 
   1053 fn bounded_time(
   1054     row: &sqlx::sqlite::SqliteRow,
   1055     column: &str,
   1056 ) -> Result<MycRequestReceivedAtUnixMs, RequestOperationError> {
   1057     let value = row
   1058         .try_get::<i64, _>(column)
   1059         .map_err(|_| RequestOperationError::Binding)?;
   1060     let value = u64::try_from(value).map_err(|_| RequestOperationError::Binding)?;
   1061     MycRequestReceivedAtUnixMs::new(value).map_err(|_| RequestOperationError::Binding)
   1062 }
   1063 
   1064 fn bounded_count(
   1065     row: &sqlx::sqlite::SqliteRow,
   1066     column: &str,
   1067 ) -> Result<u64, RequestOperationError> {
   1068     let value = row
   1069         .try_get::<i64, _>(column)
   1070         .map_err(|_| RequestOperationError::Binding)?;
   1071     u64::try_from(value).map_err(|_| RequestOperationError::Binding)
   1072 }
   1073 
   1074 pub(crate) fn derive_request_identity(
   1075     client_public_key: &MycNip46ClientPublicKey,
   1076     request_id: &MycNip46RequestId,
   1077 ) -> [u8; 32] {
   1078     let mut hasher = Sha256::new();
   1079     hasher.update(REQUEST_IDENTITY_DOMAIN);
   1080     update_length_prefixed(&mut hasher, client_public_key.as_hex().as_bytes());
   1081     update_length_prefixed(&mut hasher, request_id.as_str().as_bytes());
   1082     hasher.finalize().into()
   1083 }
   1084 
   1085 fn derive_digest(domain: &[u8], value: &[u8; 32]) -> [u8; 32] {
   1086     let mut hasher = Sha256::new();
   1087     hasher.update(domain);
   1088     hasher.update(value);
   1089     hasher.finalize().into()
   1090 }
   1091 
   1092 fn derive_operation_id(request_identity: &[u8; 32], nonce: &MycSignerOperationNonce) -> [u8; 32] {
   1093     let mut hasher = Sha256::new();
   1094     hasher.update(OPERATION_ID_DOMAIN);
   1095     hasher.update(request_identity);
   1096     hasher.update(nonce.0);
   1097     hasher.finalize().into()
   1098 }
   1099 
   1100 fn update_length_prefixed(hasher: &mut Sha256, value: &[u8]) {
   1101     hasher.update(
   1102         u64::try_from(value.len())
   1103             .expect("bounded identity length fits u64")
   1104             .to_be_bytes(),
   1105     );
   1106     hasher.update(value);
   1107 }
   1108 
   1109 fn map_transaction_error(
   1110     error: ServiceSqliteTransactionError<RequestOperationError>,
   1111 ) -> MycStateRepositoryError {
   1112     if error.kind() == ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown {
   1113         return MycStateRepositoryError::new(MycStateRepositoryErrorKind::CommitOutcomeUnknown);
   1114     }
   1115     let kind = match error.operation_error() {
   1116         Some(RequestOperationError::Binding) => MycStateRepositoryErrorKind::Binding,
   1117         Some(RequestOperationError::Storage) | None => MycStateRepositoryErrorKind::Transaction,
   1118     };
   1119     MycStateRepositoryError::new(kind)
   1120 }