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