identity_kit/status_list/
check.rs1use chrono::{DateTime, Utc};
4use url::Url;
5
6use crate::jose::{DefaultVerifier, JwsVerifier};
7use crate::methods::web::{HttpClient, HttpResponse};
8use crate::resolver::{KeyReference, KeyResolver, KeyRole};
9use crate::vc::Jwt;
10use crate::vc::validation::{CredentialStatus, StatusCheck, StatusError};
11
12use super::{StatusListError, StatusListToken, StatusMechanism};
13
14type BoxError = Box<dyn std::error::Error + Send + Sync>;
15
16pub struct StatusListCheck<C, R, V = DefaultVerifier> {
48 client: C,
49 resolver: R,
50 verifier: V,
51}
52
53impl<C, R, V> StatusListCheck<C, R, V> {
54 pub const MAX_REDIRECTS: usize = 3;
56}
57
58impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusListCheck<C, R, V> {
59 pub fn new(client: C, resolver: R, verifier: V) -> Self {
62 Self {
63 client,
64 resolver,
65 verifier,
66 }
67 }
68
69 async fn get(&self, uri: &str) -> Result<HttpResponse, StatusError> {
71 let unavailable = |reason: String, source: Option<BoxError>| StatusError::Unavailable {
72 uri: uri.to_owned(),
73 reason,
74 source,
75 };
76 let mut url = Url::parse(uri).map_err(|e| StatusError::InvalidList {
77 uri: uri.to_owned(),
78 reason: "the URI is not a URL".to_owned(),
79 source: Some(Box::new(e)),
80 })?;
81 let mut visited = vec![url.clone()];
82 loop {
83 let response = self
84 .client
85 .get_accepting(&url, StatusListToken::MEDIA_TYPE)
86 .await
87 .map_err(|e| unavailable(format!("fetching {url}"), Some(Box::new(e))))?;
88 if !matches!(response.status, 301 | 302 | 303 | 307 | 308) {
89 return Ok(response);
90 }
91 if visited.len() > Self::MAX_REDIRECTS {
92 return Err(unavailable(
93 format!("more than {} redirects", Self::MAX_REDIRECTS),
94 None,
95 ));
96 }
97 let location = response.location.ok_or_else(|| {
98 unavailable(
99 format!(
100 "{url} answered HTTP {} without a `Location`",
101 response.status
102 ),
103 None,
104 )
105 })?;
106 let next = url.join(&location).map_err(|e| {
107 unavailable(
108 format!("redirect from {url} to {location:?}"),
109 Some(Box::new(e)),
110 )
111 })?;
112 if next.scheme() != "https" {
113 return Err(unavailable(
114 format!("redirect from {url} to {next}, which is not HTTPS"),
115 None,
116 ));
117 }
118 if visited.contains(&next) {
119 return Err(unavailable(format!("redirect loop at {next}"), None));
120 }
121 visited.push(next.clone());
122 url = next;
123 }
124 }
125
126 async fn fetch(&self, uri: &str) -> Result<StatusListToken, StatusError> {
128 let invalid = |reason: &str, source: Option<BoxError>| StatusError::InvalidList {
129 uri: uri.to_owned(),
130 reason: reason.to_owned(),
131 source,
132 };
133 let response = self.get(uri).await?;
134 if !(200..300).contains(&response.status) {
135 return Err(StatusError::Unavailable {
136 uri: uri.to_owned(),
137 reason: format!("HTTP {}", response.status),
138 source: None,
139 });
140 }
141 let body = String::from_utf8(response.body)
142 .map_err(|e| invalid("the body is not UTF-8", Some(Box::new(e))))?;
143 let not_a_token = |e: BoxError| invalid("not a status list token", Some(e));
144 let jwt = Jwt::new(body.trim()).map_err(|e| not_a_token(Box::new(e)))?;
145 let (_, token) = StatusListToken::decode(&jwt).map_err(|e| not_a_token(Box::new(e)))?;
146 let jws = jwt.decode().map_err(|e| not_a_token(Box::new(e)))?;
147 let iss = token.additional.get("iss").and_then(|iss| iss.as_str());
148 let reference = KeyReference::from_jws(&jws, iss)
149 .map_err(|e| invalid("no key to verify it with", Some(Box::new(e))))?;
150 let key = self
151 .resolver
152 .resolve_key(&reference, KeyRole::Issuer)
153 .await
154 .map_err(|e| StatusError::UnresolvedKey {
155 uri: uri.to_owned(),
156 key: reference.to_string(),
157 source: e,
158 })?;
159 jws.verify(&self.verifier, &key)
160 .map_err(|e| invalid("the signature does not verify", Some(Box::new(e))))?;
161 let bound = match iss {
162 Some(iss) => reference.binds_to(Some(iss), &key),
163 None => matches!(reference, KeyReference::DidUrl(_)),
164 };
165 if !bound {
166 return Err(invalid(
167 &format!("key {reference} does not belong to the issuer {iss:?}"),
168 None,
169 ));
170 }
171 Ok(token)
172 }
173}
174
175#[async_trait::async_trait]
176impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusCheck for StatusListCheck<C, R, V> {
177 async fn check(
180 &self,
181 status: CredentialStatus<'_>,
182 now: DateTime<Utc>,
183 ) -> Result<(), StatusError> {
184 let CredentialStatus::StatusClaim(status) = status else {
185 return Err(StatusError::Unsupported(status.mechanism()));
186 };
187 let Some(reference) = &status.status_list else {
188 let names: Vec<&str> = status
189 .mechanisms()
190 .filter_map(|m| match m {
191 StatusMechanism::Other { name, .. } => Some(name),
192 _ => None,
193 })
194 .collect();
195 return Err(StatusError::Unsupported(names.join(", ")));
196 };
197 let uri = &reference.uri;
198 let token = self.fetch(uri).await?;
199 let status_type = token.status_of(reference, now).map_err(|e| match e {
200 StatusListError::Expired { expired_at, now } => StatusError::Expired {
201 uri: uri.clone(),
202 expired_at,
203 now,
204 },
205 StatusListError::IndexOutOfRange { idx, len } => StatusError::IndexOutOfRange {
206 uri: uri.clone(),
207 idx,
208 len,
209 },
210 other => StatusError::InvalidList {
211 uri: uri.clone(),
212 reason: "it does not cover the reference".to_owned(),
213 source: Some(Box::new(other)),
214 },
215 })?;
216 if status_type.is_valid() {
217 Ok(())
218 } else {
219 Err(StatusError::Invalid(status_type.to_string()))
220 }
221 }
222}
223
224impl<C: core::fmt::Debug, R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug
225 for StatusListCheck<C, R, V>
226{
227 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
228 f.debug_struct("StatusListCheck")
229 .field("client", &self.client)
230 .field("resolver", &self.resolver)
231 .field("verifier", &self.verifier)
232 .finish()
233 }
234}