status.rs (11892B)
1 //! Signer progress and status models. 2 3 use core::fmt; 4 5 #[cfg(not(feature = "std"))] 6 use alloc::{string::String, vec::Vec}; 7 #[cfg(feature = "std")] 8 use std::{string::String, vec::Vec}; 9 10 use crate::{Error, capability::SignerCapability, error::Kind}; 11 12 const MAX_AUTH_URI_BYTES: usize = 2_048; 13 14 /// A remote authentication interaction required to continue signing. 15 #[non_exhaustive] 16 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 17 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] 18 #[derive(Clone, PartialEq, Eq)] 19 pub struct AuthChallenge { 20 uri: String, 21 required_at_unix: u64, 22 expires_at_unix: Option<u64>, 23 } 24 25 impl AuthChallenge { 26 /// Creates a bounded HTTPS authentication challenge. 27 pub fn new( 28 uri: impl Into<String>, 29 required_at_unix: u64, 30 expires_at_unix: Option<u64>, 31 ) -> Result<Self, Error> { 32 let uri = uri.into(); 33 if uri.len() > MAX_AUTH_URI_BYTES 34 || uri.trim() != uri 35 || !uri.starts_with("https://") 36 || uri.chars().any(char::is_control) 37 { 38 return Err(Error::new(Kind::InvalidArgument)); 39 } 40 if let Some(expires_at_unix) = expires_at_unix 41 && expires_at_unix < required_at_unix 42 { 43 return Err(Error::new(Kind::InvalidArgument)); 44 } 45 Ok(Self { 46 uri, 47 required_at_unix, 48 expires_at_unix, 49 }) 50 } 51 52 /// Borrows the host-displayable authentication URI. 53 #[must_use] 54 pub fn uri(&self) -> &str { 55 self.uri.as_str() 56 } 57 58 /// Returns when the challenge became required. 59 #[must_use] 60 pub const fn required_at_unix(&self) -> u64 { 61 self.required_at_unix 62 } 63 64 /// Returns the optional absolute challenge expiry. 65 #[must_use] 66 pub const fn expires_at_unix(&self) -> Option<u64> { 67 self.expires_at_unix 68 } 69 } 70 71 impl fmt::Debug for AuthChallenge { 72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 73 formatter 74 .debug_struct("AuthChallenge") 75 .field("uri", &"[redacted]") 76 .field("required_at_unix", &self.required_at_unix) 77 .field("expires_at_unix", &self.expires_at_unix) 78 .finish() 79 } 80 } 81 82 #[cfg(feature = "serde")] 83 impl<'de> serde::Deserialize<'de> for AuthChallenge { 84 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 85 where 86 D: serde::Deserializer<'de>, 87 { 88 #[derive(serde::Deserialize)] 89 #[serde(deny_unknown_fields)] 90 struct Repr { 91 uri: String, 92 required_at_unix: u64, 93 expires_at_unix: Option<u64>, 94 } 95 96 let value = Repr::deserialize(deserializer)?; 97 Self::new(value.uri, value.required_at_unix, value.expires_at_unix) 98 .map_err(serde::de::Error::custom) 99 } 100 } 101 102 /// Stable signing progress stages. 103 #[non_exhaustive] 104 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 105 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] 106 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 107 pub enum SignProgressStage { 108 Queued, 109 Validating, 110 AwaitingAuthentication, 111 RequestPublished, 112 AwaitingSignature, 113 VerifyingOutput, 114 Complete, 115 } 116 117 /// One immutable signer progress update. 118 #[non_exhaustive] 119 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 120 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] 121 #[derive(Clone, Debug, PartialEq, Eq)] 122 pub struct SignProgress { 123 stage: SignProgressStage, 124 challenge: Option<AuthChallenge>, 125 } 126 127 impl SignProgress { 128 /// Creates a progress update without an authentication challenge. 129 pub const fn stage(stage: SignProgressStage) -> Result<Self, Error> { 130 if matches!(stage, SignProgressStage::AwaitingAuthentication) { 131 return Err(Error::new(Kind::InvalidArgument)); 132 } 133 Ok(Self { 134 stage, 135 challenge: None, 136 }) 137 } 138 139 /// Creates an explicit authentication-challenge update. 140 #[must_use] 141 pub const fn authentication(challenge: AuthChallenge) -> Self { 142 Self { 143 stage: SignProgressStage::AwaitingAuthentication, 144 challenge: Some(challenge), 145 } 146 } 147 148 /// Returns the stable progress stage. 149 #[must_use] 150 pub const fn stage_value(&self) -> SignProgressStage { 151 self.stage 152 } 153 154 /// Borrows the authentication challenge, when present. 155 #[must_use] 156 pub const fn challenge(&self) -> Option<&AuthChallenge> { 157 self.challenge.as_ref() 158 } 159 } 160 161 #[cfg(feature = "serde")] 162 impl<'de> serde::Deserialize<'de> for SignProgress { 163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 164 where 165 D: serde::Deserializer<'de>, 166 { 167 #[derive(serde::Deserialize)] 168 #[serde(deny_unknown_fields)] 169 struct Repr { 170 stage: SignProgressStage, 171 challenge: Option<AuthChallenge>, 172 } 173 174 let value = Repr::deserialize(deserializer)?; 175 match (value.stage, value.challenge) { 176 (SignProgressStage::AwaitingAuthentication, Some(challenge)) => { 177 Ok(Self::authentication(challenge)) 178 } 179 (SignProgressStage::AwaitingAuthentication, None) => { 180 Err(serde::de::Error::custom(Error::new(Kind::InvalidArgument))) 181 } 182 (_, Some(_)) => Err(serde::de::Error::custom(Error::new(Kind::InvalidArgument))), 183 (stage, None) => Self::stage(stage).map_err(serde::de::Error::custom), 184 } 185 } 186 } 187 188 /// Current signer availability. 189 #[non_exhaustive] 190 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 191 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] 192 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 193 pub enum SignerAvailability { 194 Ready, 195 Busy, 196 AwaitingAuthentication, 197 Unavailable, 198 } 199 200 /// Current signer availability, capabilities, and optional progress. 201 #[non_exhaustive] 202 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 203 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] 204 #[derive(Clone, Debug, PartialEq, Eq)] 205 pub struct SignerStatus { 206 availability: SignerAvailability, 207 capabilities: Vec<SignerCapability>, 208 progress: Option<SignProgress>, 209 } 210 211 impl SignerStatus { 212 /// Creates an explicit status snapshot. 213 #[must_use] 214 pub fn new( 215 availability: SignerAvailability, 216 capabilities: Vec<SignerCapability>, 217 progress: Option<SignProgress>, 218 ) -> Self { 219 Self { 220 availability, 221 capabilities, 222 progress, 223 } 224 } 225 226 /// Creates an unavailable status without claiming capabilities. 227 #[must_use] 228 pub const fn unavailable() -> Self { 229 Self { 230 availability: SignerAvailability::Unavailable, 231 capabilities: Vec::new(), 232 progress: None, 233 } 234 } 235 236 #[must_use] 237 pub const fn availability(&self) -> SignerAvailability { 238 self.availability 239 } 240 241 #[must_use] 242 pub fn capabilities(&self) -> &[SignerCapability] { 243 &self.capabilities 244 } 245 246 #[must_use] 247 pub const fn progress(&self) -> Option<&SignProgress> { 248 self.progress.as_ref() 249 } 250 } 251 252 #[cfg(test)] 253 mod tests { 254 use super::*; 255 #[cfg(feature = "serde")] 256 use crate::capability::{CancellationSupport, SignerKind}; 257 #[cfg(feature = "serde")] 258 use crate::recovery::ReplayCapability; 259 260 #[cfg(not(feature = "std"))] 261 use alloc::format; 262 #[cfg(all(not(feature = "std"), feature = "serde"))] 263 use alloc::vec; 264 265 #[test] 266 fn challenge_validation_and_debug_redaction_are_explicit() { 267 let challenge = 268 AuthChallenge::new("https://auth.example/approve?token=sensitive", 10, Some(20)) 269 .expect("challenge"); 270 271 assert_eq!(challenge.required_at_unix(), 10); 272 assert_eq!( 273 challenge.uri(), 274 "https://auth.example/approve?token=sensitive" 275 ); 276 assert_eq!(challenge.expires_at_unix(), Some(20)); 277 assert!(!format!("{challenge:?}").contains("sensitive")); 278 assert_eq!( 279 AuthChallenge::new("http://auth.example", 10, None) 280 .expect_err("HTTP challenge must fail") 281 .kind(), 282 Kind::InvalidArgument 283 ); 284 assert_eq!( 285 AuthChallenge::new("https://auth.example", 20, Some(10)) 286 .expect_err("invalid expiry must fail") 287 .kind(), 288 Kind::InvalidArgument 289 ); 290 for invalid in [ 291 " https://auth.example", 292 "https://auth.example ", 293 "https://auth.example/line\nbreak", 294 ] { 295 assert_eq!( 296 AuthChallenge::new(invalid, 10, None).unwrap_err().kind(), 297 Kind::InvalidArgument 298 ); 299 } 300 assert_eq!( 301 AuthChallenge::new( 302 format!("https://auth.example/{}", "x".repeat(MAX_AUTH_URI_BYTES)), 303 10, 304 None 305 ) 306 .unwrap_err() 307 .kind(), 308 Kind::InvalidArgument 309 ); 310 } 311 312 #[test] 313 fn progress_requires_challenges_only_at_the_authentication_stage() { 314 assert_eq!( 315 SignProgress::stage(SignProgressStage::AwaitingAuthentication) 316 .expect_err("missing challenge must fail") 317 .kind(), 318 Kind::InvalidArgument 319 ); 320 let challenge = 321 AuthChallenge::new("https://auth.example/approve", 10, None).expect("challenge"); 322 let progress = SignProgress::authentication(challenge); 323 assert_eq!( 324 progress.stage_value(), 325 SignProgressStage::AwaitingAuthentication 326 ); 327 assert!(progress.challenge().is_some()); 328 let queued = SignProgress::stage(SignProgressStage::Queued).unwrap(); 329 assert_eq!(queued.stage_value(), SignProgressStage::Queued); 330 assert_eq!(queued.challenge(), None); 331 let unavailable = SignerStatus::unavailable(); 332 assert_eq!(unavailable.availability(), SignerAvailability::Unavailable); 333 assert!(unavailable.capabilities().is_empty()); 334 assert_eq!(unavailable.progress(), None); 335 } 336 337 #[cfg(feature = "serde")] 338 #[test] 339 fn status_round_trips_and_invalid_progress_fails_closed() { 340 let capability = SignerCapability::new( 341 SignerKind::Remote, 342 ReplayCapability::ExactReplayByRequestId, 343 CancellationSupport::BeforePublication, 344 true, 345 true, 346 ); 347 let challenge = 348 AuthChallenge::new("https://auth.example/approve", 10, Some(20)).expect("challenge"); 349 let status = SignerStatus::new( 350 SignerAvailability::AwaitingAuthentication, 351 vec![capability], 352 Some(SignProgress::authentication(challenge)), 353 ); 354 let encoded = serde_json::to_string(&status).expect("serialize status"); 355 let decoded: SignerStatus = serde_json::from_str(&encoded).expect("deserialize status"); 356 357 assert_eq!(decoded, status); 358 assert!( 359 serde_json::from_str::<SignProgress>( 360 r#"{"stage":"awaiting_authentication","challenge":null}"# 361 ) 362 .is_err() 363 ); 364 assert!(serde_json::from_str::<SignProgress>( 365 r#"{"stage":"queued","challenge":{"uri":"https://auth.example","required_at_unix":1,"expires_at_unix":null}}"# 366 ) 367 .is_err()); 368 assert!( 369 serde_json::from_str::<AuthChallenge>( 370 r#"{"uri":"http://auth.example","required_at_unix":1,"expires_at_unix":null}"# 371 ) 372 .is_err() 373 ); 374 assert!( 375 serde_json::from_str::<SignProgress>(r#"{"stage":"queued","challenge":null}"#).is_ok() 376 ); 377 } 378 }