1use http::header;
2use iref::{uri::AuthorityBuf, UriBuf};
3use ssi_dids_core::{
4 document::representation::MediaType,
5 resolution::{self, DIDMethodResolver, Error, Output},
6 DIDMethod,
7};
8
9pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
10
11#[cfg(test)]
13use std::cell::RefCell;
14use std::{net::Ipv4Addr, str::FromStr};
15
16#[cfg(test)]
17thread_local! {
18 static PROXY: RefCell<Option<String>> = const { RefCell::new(None) };
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum InternalError {
23 #[error("Error building HTTP client: {0}")]
24 Client(reqwest::Error),
25
26 #[error("Error sending HTTP request ({0}): {1}")]
27 Request(String, reqwest::Error),
28
29 #[error("Server error: {0}")]
30 Server(String),
31
32 #[error("Error reading HTTP response: {0}")]
33 Response(reqwest::Error),
34}
35
36pub struct DIDWeb;
40
41fn did_web_url(id: &str) -> Result<UriBuf, Error> {
42 let mut parts = id.split(':').peekable();
43
44 let encoded_authority = parts
46 .next()
47 .ok_or_else(|| Error::InvalidMethodSpecificId(id.to_owned()))?;
48
49 let authority: AuthorityBuf = match encoded_authority.rsplit_once("%3A") {
51 Some((host, port)) => AuthorityBuf::new(format!("{host}:{port}").into_bytes())
52 .map_err(|_| Error::InvalidMethodSpecificId(id.to_owned()))?,
53 None => encoded_authority
54 .parse()
55 .map_err(|_| Error::InvalidMethodSpecificId(id.to_owned()))?,
56 };
57
58 let host = authority.host().as_str();
60 let scheme = if host == "localhost" {
61 "http"
62 } else {
63 match Ipv4Addr::from_str(host) {
64 Ok(ip) if ip.is_private() || ip.is_loopback() => "http",
65 Ok(_) => return Err(Error::InvalidMethodSpecificId(id.to_owned())),
66 _ => "https",
67 }
68 };
69
70 let path = match parts.peek() {
76 Some(_) => parts.collect::<Vec<&str>>().join("/"),
77 None => ".well-known".to_string(),
78 };
79
80 #[allow(unused_mut)]
81 let mut url = format!("{scheme}://{}/{path}/did.json", authority);
82
83 #[cfg(test)]
84 PROXY.with(|proxy| {
85 if let Some(ref proxy) = *proxy.borrow() {
86 url = proxy.clone() + &url;
87 }
88 });
89
90 UriBuf::new(url.into_bytes()).map_err(|_| Error::InvalidMethodSpecificId(id.to_owned()))
91}
92
93impl DIDMethod for DIDWeb {
94 const DID_METHOD_NAME: &'static str = "web";
95}
96
97impl DIDMethodResolver for DIDWeb {
99 async fn resolve_method_representation<'a>(
100 &'a self,
101 method_specific_id: &'a str,
102 options: resolution::Options,
103 ) -> Result<Output<Vec<u8>>, Error> {
104 let url = did_web_url(method_specific_id)?;
107 let mut headers = reqwest::header::HeaderMap::new();
110
111 headers.insert(
112 "User-Agent",
113 reqwest::header::HeaderValue::from_static(USER_AGENT),
114 );
115
116 #[cfg(target_os = "android")]
117 let client = reqwest::Client::builder()
118 .use_rustls_tls()
119 .default_headers(headers)
120 .build()
121 .map_err(|e| Error::internal(InternalError::Client(e)))?;
122
123 #[cfg(not(target_os = "android"))]
124 let client = reqwest::Client::builder()
125 .default_headers(headers)
126 .build()
127 .map_err(|e| Error::internal(InternalError::Client(e)))?;
128
129 let accept = options.accept.unwrap_or(MediaType::Json);
130
131 let resp = client
132 .get(url.as_str())
133 .header(header::ACCEPT, accept.to_string())
134 .send()
135 .await
136 .map_err(|e| Error::internal(InternalError::Request(url.to_string(), e)))?;
137
138 resp.error_for_status_ref().map_err(|err| {
139 if err.status() == Some(reqwest::StatusCode::NOT_FOUND) {
140 Error::NotFound
141 } else {
142 Error::internal(InternalError::Server(err.to_string()))
143 }
144 })?;
145
146 let media_type = resp
147 .headers()
148 .get(header::CONTENT_TYPE)
149 .map(|value| match value.as_bytes() {
150 b"application/json" => Ok(MediaType::Json),
151 b"application/json; charset=utf-8" => Ok(MediaType::Json),
152 other => MediaType::from_bytes(other),
153 })
154 .transpose()?
155 .unwrap_or(MediaType::Json);
156
157 let document = resp
158 .bytes()
159 .await
160 .map_err(|e| Error::internal(InternalError::Response(e)))?;
161
162 Ok(Output {
164 document: document.into(),
165 document_metadata: ssi_dids_core::document::Metadata::default(),
166 metadata: resolution::Metadata::from_content_type(Some(media_type.to_string())),
167 })
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use iref::Uri;
174 use ssi_claims::{
175 data_integrity::{AnySuite, CryptographicSuite, ProofOptions},
176 vc::{syntax::NonEmptyVec, v1::JsonCredential},
177 VerificationParameters,
178 };
179 use ssi_dids_core::{did, DIDResolver, Document, VerificationMethodDIDResolver, DID};
180 use ssi_jwk::JWK;
181 use ssi_verification_methods_core::{ProofPurpose, SingleSecretSigner};
182 use static_iref::{iri, uri};
183
184 use super::*;
185
186 #[tokio::test]
187 async fn parse_did_web() {
188 let test_vectors: [(&DID, &Uri); 7] = [
189 (
190 did!("did:web:w3c-ccg.github.io"),
192 uri!("https://w3c-ccg.github.io/.well-known/did.json"),
193 ),
194 (
195 did!("did:web:w3c-ccg.github.io:user:alice"),
197 uri!("https://w3c-ccg.github.io/user/alice/did.json"),
198 ),
199 (
200 did!("did:web:example.com:u:bob"),
202 uri!("https://example.com/u/bob/did.json"),
203 ),
204 (
205 did!("did:web:example.com%3A443:u:bob"),
207 uri!("https://example.com:443/u/bob/did.json"),
208 ),
209 (
210 did!("did:web:localhost:u:alice"),
212 uri!("http://localhost/u/alice/did.json"),
213 ),
214 (
215 did!("did:web:192.168.0.1:u:alice"),
217 uri!("http://192.168.0.1/u/alice/did.json"),
218 ),
219 (
220 did!("did:web:192.168.0.1%3A3003:u:alice"),
222 uri!("http://192.168.0.1:3003/u/alice/did.json"),
223 ),
224 ];
225
226 for (did, url) in test_vectors {
227 assert_eq!(did_web_url(did.method_specific_id()).unwrap(), url);
228 }
229 }
230
231 const DID_URL: &str = "http://localhost/.well-known/did.json";
232 const DID_JSON: &str = r#"{
233 "@context": "https://www.w3.org/ns/did/v1",
234 "id": "did:web:localhost",
235 "verificationMethod": [{
236 "id": "did:web:localhost#key1",
237 "type": "Ed25519VerificationKey2018",
238 "controller": "did:web:localhost",
239 "publicKeyBase58": "2sXRz2VfrpySNEL6xmXJWQg6iY94qwNp1qrJJFBuPWmH"
240 }],
241 "assertionMethod": ["did:web:localhost#key1"]
242 }"#;
243
244 fn web_server() -> Result<(String, impl FnOnce() -> Result<(), ()>), hyper::Error> {
247 use http::header::{HeaderValue, CONTENT_TYPE};
248 use hyper::service::{make_service_fn, service_fn};
249 use hyper::{Body, Response, Server};
250 let addr = ([127, 0, 0, 1], 0).into();
251 let make_svc = make_service_fn(|_| async move {
252 Ok::<_, hyper::Error>(service_fn(|req| async move {
253 let uri = req.uri();
254 let proxied_url: String = uri.path().chars().skip(1).collect();
256 if proxied_url == DID_URL {
257 let body = Body::from(DID_JSON);
258 let mut response = Response::new(body);
259 response
260 .headers_mut()
261 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
262 return Ok::<_, hyper::Error>(response);
263 }
264
265 let (mut parts, body) = Response::<Body>::default().into_parts();
266 parts.status = hyper::StatusCode::NOT_FOUND;
267 let response = Response::from_parts(parts, body);
268 Ok::<_, hyper::Error>(response)
269 }))
270 });
271 let server = Server::try_bind(&addr)?.serve(make_svc);
272 let url = "http://".to_string() + &server.local_addr().to_string() + "/";
273 let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel();
274 let graceful = server.with_graceful_shutdown(async {
275 shutdown_rx.await.ok();
276 });
277 tokio::task::spawn(async move {
278 graceful.await.ok();
279 });
280 let shutdown = || shutdown_tx.send(());
281 Ok((url, shutdown))
282 }
283
284 #[tokio::test]
285 async fn from_did_key() {
286 let (url, shutdown) = web_server().unwrap();
287 PROXY.with(|proxy| {
288 proxy.replace(Some(url));
289 });
290 let doc = DIDWeb.resolve(did!("did:web:localhost")).await.unwrap();
291 let doc_expected = Document::from_bytes(MediaType::Json, DID_JSON.as_bytes()).unwrap();
292 assert_eq!(doc.document.document(), doc_expected.document());
293 PROXY.with(|proxy| {
294 proxy.replace(None);
295 });
296 shutdown().ok();
297 }
298
299 #[tokio::test]
300 async fn credential_prove_verify_did_web() {
301 let didweb = VerificationMethodDIDResolver::new(DIDWeb);
302 let params = VerificationParameters::from_resolver(&didweb);
303
304 let (url, shutdown) = web_server().unwrap();
305 PROXY.with(|proxy| {
306 proxy.replace(Some(url));
307 });
308
309 let cred = JsonCredential::new(
310 None,
311 did!("did:web:localhost").to_owned().into_uri().into(),
312 "2021-01-26T16:57:27Z".parse().unwrap(),
313 NonEmptyVec::new(json_syntax::json!({
314 "id": "did:web:localhost"
315 })),
316 );
317
318 let key: JWK = include_str!("../../../../../tests/ed25519-2020-10-18.json")
319 .parse()
320 .unwrap();
321 let verification_method = iri!("did:web:localhost#key1").to_owned().into();
322 let suite = AnySuite::pick(&key, Some(&verification_method)).unwrap();
323 let issue_options = ProofOptions::new(
324 "2021-01-26T16:57:27Z".parse().unwrap(),
325 verification_method,
326 ProofPurpose::Assertion,
327 Default::default(),
328 );
329 let signer = SingleSecretSigner::new(key).into_local();
330 let vc = suite
331 .sign(cred, &didweb, &signer, issue_options)
332 .await
333 .unwrap();
334
335 println!(
336 "proof: {}",
337 serde_json::to_string_pretty(&vc.proofs).unwrap()
338 );
339 assert_eq!(vc.proofs.first().unwrap().signature.as_ref(), "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..BCvVb4jz-yVaTeoP24Wz0cOtiHKXCdPcmFQD_pxgsMU6aCAj1AIu3cqHyoViU93nPmzqMLswOAqZUlMyVnmzDw");
340 assert!(vc.verify(¶ms).await.unwrap().is_ok());
341
342 let mut vc_bad_issuer = vc.clone();
344 vc_bad_issuer.issuer = uri!("did:pkh:example:bad").to_owned().into();
345 assert!(vc_bad_issuer.verify(params).await.unwrap().is_err());
347
348 PROXY.with(|proxy| {
349 proxy.replace(None);
350 });
351 shutdown().ok();
352 }
353}