nip46_replay.rs (7892B)
1 //! Stable replay identities derived from verified NIP-46 input. 2 3 use core::fmt; 4 5 use sha2::{Digest, Sha256}; 6 7 use crate::{ 8 MycNip46ClientPublicKey, MycNip46EventId, MycNip46RequestId, MycProviderPublicIdentity, 9 MycSignerRequestDigest, MycSignerRequestError, MycVerifiedNip46Event, MycVerifiedNip46Request, 10 state_request::derive_request_identity, 11 }; 12 13 const CONNECTION_IDENTITY_DOMAIN: &[u8] = b"radroots.myc.nip46.connection_identity.v1\0"; 14 15 macro_rules! redacted_identity { 16 ($name:ident) => { 17 #[derive(Clone, Copy, PartialEq, Eq, Hash)] 18 pub struct $name([u8; 32]); 19 20 impl $name { 21 /// Returns the exact stable identity bytes. 22 #[must_use] 23 pub const fn as_bytes(&self) -> &[u8; 32] { 24 &self.0 25 } 26 } 27 28 impl fmt::Debug for $name { 29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 30 formatter.write_str(concat!(stringify!($name), "([redacted])")) 31 } 32 } 33 }; 34 } 35 36 redacted_identity!(MycNip46ConnectionIdentity); 37 redacted_identity!(MycNip46LogicalRequestIdentity); 38 39 /// Pure relation between one retained replay binding and a new candidate. 40 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 41 pub enum MycNip46ReplayDisposition { 42 /// The event and logical request identities are both new. 43 DistinctRequest, 44 /// The exact signed event and exact canonical request were already observed. 45 DuplicateEvent, 46 /// A distinct signed event carries the exact same logical request. 47 ExactRequestReplay, 48 /// One logical request identity was reused with different canonical bytes. 49 ConflictingRequestReuse, 50 /// One signed event identity was paired with a different logical request. 51 ConflictingEventReuse, 52 } 53 54 /// Compact replay identity that retains no event, ciphertext, or plaintext. 55 #[derive(Clone, Copy, PartialEq, Eq, Hash)] 56 pub struct MycNip46ReplayKey { 57 event_id: MycNip46EventId, 58 connection_identity: MycNip46ConnectionIdentity, 59 request_identity: MycNip46LogicalRequestIdentity, 60 request_digest: MycSignerRequestDigest, 61 } 62 63 impl fmt::Debug for MycNip46ReplayKey { 64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 65 formatter.write_str("MycNip46ReplayKey([redacted])") 66 } 67 } 68 69 /// Sealed replay evidence derived from one verified event/request candidate. 70 /// 71 /// This type does not prove that the plaintext was decrypted from the retained 72 /// event. The Step 145 decryption boundary owns that cryptographic association 73 /// before this evidence may be admitted to durable request state. 74 pub struct MycReplayBoundNip46Request { 75 event: MycVerifiedNip46Event, 76 request: MycVerifiedNip46Request, 77 connection_identity: MycNip46ConnectionIdentity, 78 request_identity: MycNip46LogicalRequestIdentity, 79 request_digest: MycSignerRequestDigest, 80 } 81 82 impl MycReplayBoundNip46Request { 83 /// Returns the stable client connection identity. 84 #[must_use] 85 pub const fn connection_identity(&self) -> MycNip46ConnectionIdentity { 86 self.connection_identity 87 } 88 89 /// Returns the stable connection-scoped request identity. 90 #[must_use] 91 pub const fn request_identity(&self) -> MycNip46LogicalRequestIdentity { 92 self.request_identity 93 } 94 95 /// Returns the digest of the exact canonical typed request bytes. 96 #[must_use] 97 pub const fn request_digest(&self) -> MycSignerRequestDigest { 98 self.request_digest 99 } 100 101 /// Returns the cryptographically verified signed-event identity. 102 #[must_use] 103 pub const fn event_id(&self) -> MycNip46EventId { 104 self.event.event_id() 105 } 106 107 /// Returns the verified client transport identity. 108 #[must_use] 109 pub const fn client_public_key(&self) -> &MycNip46ClientPublicKey { 110 self.event.client_public_key() 111 } 112 113 #[must_use] 114 pub(crate) const fn encryption_context(&self) -> crate::MycNip46EncryptionContext { 115 self.event.encryption_context() 116 } 117 118 /// Returns the validated request identifier. 119 #[must_use] 120 pub const fn request_id(&self) -> &MycNip46RequestId { 121 self.request.request_id() 122 } 123 124 pub(crate) fn method(&self) -> &str { 125 self.request.method() 126 } 127 128 pub(crate) fn canonical_request(&self) -> &[u8] { 129 self.request.canonical_request() 130 } 131 132 /// Projects the digest-only replay key without retaining protected content. 133 #[must_use] 134 pub const fn replay_key(&self) -> MycNip46ReplayKey { 135 MycNip46ReplayKey { 136 event_id: self.event.event_id(), 137 connection_identity: self.connection_identity, 138 request_identity: self.request_identity, 139 request_digest: self.request_digest, 140 } 141 } 142 143 /// Classifies this candidate against one retained digest-only key. 144 #[must_use] 145 pub fn classify_against(&self, retained: MycNip46ReplayKey) -> MycNip46ReplayDisposition { 146 let same_event = self.event_id() == retained.event_id; 147 let same_connection = self.connection_identity == retained.connection_identity; 148 let same_request_identity = self.request_identity == retained.request_identity; 149 let same_request_bytes = self.request_digest == retained.request_digest; 150 151 if same_event { 152 if same_connection && same_request_identity && same_request_bytes { 153 MycNip46ReplayDisposition::DuplicateEvent 154 } else { 155 MycNip46ReplayDisposition::ConflictingEventReuse 156 } 157 } else if same_connection && same_request_identity { 158 if same_request_bytes { 159 MycNip46ReplayDisposition::ExactRequestReplay 160 } else { 161 MycNip46ReplayDisposition::ConflictingRequestReuse 162 } 163 } else { 164 MycNip46ReplayDisposition::DistinctRequest 165 } 166 } 167 } 168 169 impl fmt::Debug for MycReplayBoundNip46Request { 170 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 171 formatter 172 .debug_struct("MycReplayBoundNip46Request") 173 .field("identity", &"[redacted]") 174 .finish() 175 } 176 } 177 178 /// Derives stable replay identities from one verified event/request candidate. 179 /// 180 /// The caller must not treat this pure identity projection as proof that the 181 /// request plaintext came from the encrypted event. Step 145 owns decryption, 182 /// association, supported-method admission, and conversion into durable work. 183 pub fn bind_myc_nip46_replay( 184 event: MycVerifiedNip46Event, 185 request: MycVerifiedNip46Request, 186 ) -> Result<MycReplayBoundNip46Request, MycSignerRequestError> { 187 let connection_identity = 188 derive_connection_identity(event.receiver_public_key(), event.client_public_key()); 189 let request_identity = MycNip46LogicalRequestIdentity(derive_request_identity( 190 event.client_public_key(), 191 request.request_id(), 192 )); 193 let request_digest = 194 MycSignerRequestDigest::for_canonical_request(request.canonical_request())?; 195 196 Ok(MycReplayBoundNip46Request { 197 event, 198 request, 199 connection_identity, 200 request_identity, 201 request_digest, 202 }) 203 } 204 205 fn derive_connection_identity( 206 receiver_public_key: &MycProviderPublicIdentity, 207 client_public_key: &MycNip46ClientPublicKey, 208 ) -> MycNip46ConnectionIdentity { 209 let mut hasher = Sha256::new(); 210 hasher.update(CONNECTION_IDENTITY_DOMAIN); 211 update_length_prefixed(&mut hasher, receiver_public_key.as_hex().as_bytes()); 212 update_length_prefixed(&mut hasher, client_public_key.as_hex().as_bytes()); 213 MycNip46ConnectionIdentity(hasher.finalize().into()) 214 } 215 216 fn update_length_prefixed(hasher: &mut Sha256, value: &[u8]) { 217 hasher.update( 218 u64::try_from(value.len()) 219 .expect("canonical identity length fits u64") 220 .to_be_bytes(), 221 ); 222 hasher.update(value); 223 }