1use anyhow::{anyhow, Context, Result};
2use async_trait::async_trait;
3use core::str::FromStr;
4use pgp::{types::KeyTrait, Deserializable, SignedPublicKey};
5use serde::{Deserialize, Serialize};
6use std::{collections::BTreeMap, io::Cursor};
7
8use sshkeys::PublicKeyKind;
9use ssi_dids::did_resolve::{
10 DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID,
11};
12use ssi_dids::{DIDMethod, Document, VerificationMethod, VerificationMethodMap, DIDURL};
13use ssi_ssh::ssh_pkk_to_jwk;
14
15#[cfg(test)]
17use std::cell::RefCell;
18#[cfg(test)]
19thread_local! {
20 static PROXY: RefCell<Option<String>> = RefCell::new(None);
21}
22
23pub struct DIDWebKey;
25
26#[derive(Debug, Serialize, Deserialize, PartialEq)]
27enum DIDWebKeyType {
28 Ssh,
29 Gpg,
30}
31
32impl FromStr for DIDWebKeyType {
33 type Err = ResolutionMetadata;
34 fn from_str(type_: &str) -> Result<Self, Self::Err> {
35 match type_ {
36 "ssh" => Ok(DIDWebKeyType::Ssh),
37 "gpg" => Ok(DIDWebKeyType::Gpg),
38 _ => Err(ResolutionMetadata::from_error(ERROR_INVALID_DID)),
39 }
40 }
41}
42
43fn parse_pubkeys_gpg(
44 did: &str,
45 bytes: Vec<u8>,
46) -> Result<(Vec<VerificationMethodMap>, Vec<DIDURL>)> {
47 let mut did_urls = Vec::new();
48 let mut vm_maps = Vec::new();
49
50 let pks = SignedPublicKey::from_armor_many(Cursor::new(bytes))?
51 .0
52 .collect::<Result<Vec<_>, _>>()?;
53 for pk in pks {
54 let (vm_map, did_url) = gpg_pk_to_vm(did, pk)?;
55 vm_maps.push(vm_map);
56 did_urls.push(did_url);
57 }
58
59 Ok((vm_maps, did_urls))
60}
61
62fn gpg_pk_to_vm(did: &str, pk: SignedPublicKey) -> Result<(VerificationMethodMap, DIDURL)> {
63 let fingerprint = pk
64 .fingerprint()
65 .iter()
66 .fold(String::new(), |acc, &x| format!("{}{:02X}", acc, x));
67 let vm_url = DIDURL {
68 did: did.to_string(),
69 fragment: Some(fingerprint.clone()),
70 ..Default::default()
71 };
72
73 let mut header = {
76 let mut res = String::new();
77 let l = fingerprint.len();
78 for (i, b) in fingerprint.chars().enumerate() {
79 if i > 0 && i % 4 == 0 {
80 res.push(' ');
81 if i * 2 == l {
82 res.push(' ');
83 }
84 }
85 res.push(b);
86 }
87 res
88 };
89 if let Some(user) = pk.details.users.get(0) {
90 header = format!("{}\nComment: {}", header, user.id.id());
92 }
93 let headers = BTreeMap::from([("Comment".to_string(), header)]);
94 let armored_pgp = pk.to_armored_string(Some(&headers))?;
95
96 let vm_map = VerificationMethodMap {
97 id: vm_url.to_string(),
98 type_: "PgpVerificationKey2021".to_string(),
99 public_key_pgp: Some(armored_pgp),
100 controller: did.to_string(),
101 ..Default::default()
102 };
103 Ok((vm_map, vm_url))
104}
105
106fn pk_to_vm_ed25519(
107 did: &str,
108 pk: sshkeys::Ed25519PublicKey,
109) -> Result<(VerificationMethodMap, DIDURL)> {
110 let jwk = ssh_pkk_to_jwk(&PublicKeyKind::Ed25519(pk))?;
111 let thumbprint = jwk
112 .thumbprint()
113 .context("Unable to calculate JWK thumbprint")?;
114 let vm_url = DIDURL {
115 did: did.to_string(),
116 fragment: Some(thumbprint),
117 ..Default::default()
118 };
119 let vm_map = VerificationMethodMap {
120 id: vm_url.to_string(),
121 type_: "Ed25519VerificationKey2018".to_string(),
122 public_key_jwk: Some(jwk),
123 controller: did.to_string(),
124 ..Default::default()
125 };
126 Ok((vm_map, vm_url))
127}
128
129fn pk_to_vm_ecdsa(
130 did: &str,
131 pk: sshkeys::EcdsaPublicKey,
132) -> Result<(VerificationMethodMap, DIDURL)> {
133 let jwk = ssh_pkk_to_jwk(&PublicKeyKind::Ecdsa(pk))?;
134 let thumbprint = jwk
135 .thumbprint()
136 .context("Unable to calculate JWK thumbprint")?;
137 let vm_url = DIDURL {
138 did: did.to_string(),
139 fragment: Some(thumbprint),
140 ..Default::default()
141 };
142 let vm_map = VerificationMethodMap {
143 id: vm_url.to_string(),
144 type_: "EcdsaSecp256r1VerificationKey2019".to_string(),
145 public_key_jwk: Some(jwk),
146 controller: did.to_string(),
147 ..Default::default()
148 };
149 Ok((vm_map, vm_url))
150}
151
152fn pk_to_vm_rsa(did: &str, pk: sshkeys::RsaPublicKey) -> Result<(VerificationMethodMap, DIDURL)> {
153 let jwk = ssh_pkk_to_jwk(&PublicKeyKind::Rsa(pk))?;
154 let thumbprint = jwk
155 .thumbprint()
156 .context("Unable to calculate JWK thumbprint")?;
157 let vm_url = DIDURL {
158 did: did.to_string(),
159 fragment: Some(thumbprint),
160 ..Default::default()
161 };
162 let vm_map = VerificationMethodMap {
163 id: vm_url.to_string(),
164 type_: "RsaVerificationKey2018".to_string(),
165 public_key_jwk: Some(jwk),
166 controller: did.to_string(),
167 ..Default::default()
168 };
169 Ok((vm_map, vm_url))
170}
171
172fn pk_to_vm_dsa(_did: &str, _pk: sshkeys::DsaPublicKey) -> Result<(VerificationMethodMap, DIDURL)> {
173 Err(anyhow!("Unsupported DSA Key"))
174}
175
176fn pk_to_vm(did: &str, pk: sshkeys::PublicKey) -> Result<(VerificationMethodMap, DIDURL)> {
177 match pk.kind {
178 PublicKeyKind::Rsa(pk) => pk_to_vm_rsa(did, pk),
179 PublicKeyKind::Dsa(pk) => pk_to_vm_dsa(did, pk),
180 PublicKeyKind::Ecdsa(pk) => pk_to_vm_ecdsa(did, pk),
181 PublicKeyKind::Ed25519(pk) => pk_to_vm_ed25519(did, pk),
182 }
183}
184
185fn parse_pubkeys_ssh(
186 did: &str,
187 bytes: Vec<u8>,
188) -> Result<(Vec<VerificationMethodMap>, Vec<DIDURL>)> {
189 let lines = String::from_utf8(bytes)?;
190 let mut did_urls = Vec::new();
191 let mut vm_maps = Vec::new();
192 let lines = lines.trim().split('\n');
193 for line in lines {
194 let pk = sshkeys::PublicKey::from_string(line)?;
195 let (vm_map, did_url) = pk_to_vm(did, pk)?;
196 vm_maps.push(vm_map);
197 did_urls.push(did_url);
198 }
199 Ok((vm_maps, did_urls))
200}
201
202fn parse_pubkeys(
203 did: &str,
204 type_: DIDWebKeyType,
205 bytes: Vec<u8>,
206) -> Result<(Vec<VerificationMethodMap>, Vec<DIDURL>)> {
207 match type_ {
208 DIDWebKeyType::Gpg => parse_pubkeys_gpg(did, bytes),
209 DIDWebKeyType::Ssh => parse_pubkeys_ssh(did, bytes),
210 }
211}
212
213fn parse_did_webkey_url(did: &str) -> Result<(DIDWebKeyType, String), ResolutionMetadata> {
214 let mut parts = did.split(':').peekable();
215 let (type_, domain_name) = match (parts.next(), parts.next(), parts.next(), parts.next()) {
216 (Some("did"), Some("webkey"), Some(type_), Some(domain_name)) => {
217 (type_.parse()?, domain_name)
218 }
219 _ => {
220 return Err(ResolutionMetadata::from_error(ERROR_INVALID_DID));
221 }
222 };
223 let path = match parts.peek() {
224 Some(_) => parts.collect::<Vec<&str>>().join("/"),
225 None => {
226 return Err(ResolutionMetadata::from_error(ERROR_INVALID_DID));
228 }
229 };
230 #[allow(unused_mut)]
231 let mut url = format!("https://{}/{}", domain_name, path);
232 #[cfg(test)]
233 PROXY.with(|proxy| {
234 if let Some(ref proxy) = *proxy.borrow() {
235 url = proxy.clone() + &url;
236 }
237 });
238 Ok((type_, url))
239}
240
241#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
242#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
243impl DIDResolver for DIDWebKey {
244 async fn resolve(
245 &self,
246 did: &str,
247 input_metadata: &ResolutionInputMetadata,
248 ) -> (
249 ResolutionMetadata,
250 Option<Document>,
251 Option<DocumentMetadata>,
252 ) {
253 let (type_, url) = match parse_did_webkey_url(did) {
254 Err(meta) => return (meta, None, None),
255 Ok(url) => url,
256 };
257 let client = match reqwest::Client::builder().build() {
259 Ok(c) => c,
260 Err(err) => {
261 return (
262 ResolutionMetadata::from_error(&format!("Error building HTTP client: {}", err)),
263 None,
264 None,
265 )
266 }
267 };
268 let accept = input_metadata
269 .accept
270 .clone()
271 .unwrap_or_else(|| "application/json".to_string());
272 let resp = match client.get(&url).header("Accept", accept).send().await {
273 Ok(req) => req,
274 Err(err) => {
275 return (
276 ResolutionMetadata::from_error(&format!(
277 "Error sending HTTP request : {}",
278 err
279 )),
280 None,
281 None,
282 )
283 }
284 };
285 match resp.error_for_status_ref() {
286 Ok(_) => (),
287 Err(err) => {
288 return (
289 ResolutionMetadata::from_error(&err.to_string()),
290 None,
291 Some(DocumentMetadata::default()),
292 )
293 }
294 };
295 let bytes = match resp.bytes().await {
296 Ok(bytes) => bytes.to_vec(),
297 Err(err) => {
298 return (
299 ResolutionMetadata::from_error(
300 &("Error reading HTTP response: ".to_string() + &err.to_string()),
301 ),
302 None,
303 None,
304 )
305 }
306 };
307 let (vm_maps, vm_urls): (Vec<VerificationMethod>, Vec<VerificationMethod>) =
308 match parse_pubkeys(did, type_, bytes) {
309 Ok((maps, urls)) => (
310 maps.into_iter().map(VerificationMethod::Map).collect(),
311 urls.into_iter().map(VerificationMethod::DIDURL).collect(),
312 ),
313 Err(err) => {
314 return (
315 ResolutionMetadata::from_error(&format!("Error parsing keys: {}", err)),
316 None,
317 None,
318 )
319 }
320 };
321 let doc = Document {
322 context: ssi_dids::Contexts::One(ssi_dids::Context::URI(
323 ssi_dids::DEFAULT_CONTEXT.into(),
324 )),
325 id: did.to_string(),
326 verification_method: Some(vm_maps),
327 authentication: Some(vm_urls.clone()),
328 assertion_method: Some(vm_urls),
329 ..Default::default()
330 };
331 (
333 ResolutionMetadata::default(),
334 Some(doc),
335 Some(DocumentMetadata::default()),
336 )
337 }
338}
339
340impl DIDMethod for DIDWebKey {
341 fn name(&self) -> &'static str {
342 "webkey"
343 }
344
345 fn to_resolver(&self) -> &dyn DIDResolver {
346 self
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use serde_json::json;
354
355 #[async_std::test]
356 async fn parse_did_webkey() {
357 assert_eq!(
358 parse_did_webkey_url("did:webkey:ssh:example.org:user.keys").unwrap(),
359 (
360 DIDWebKeyType::Ssh,
361 "https://example.org/user.keys".to_string()
362 )
363 );
364 assert_eq!(
365 parse_did_webkey_url("did:webkey:gpg:example.org:user.gpg").unwrap(),
366 (
367 DIDWebKeyType::Gpg,
368 "https://example.org/user.gpg".to_string()
369 )
370 );
371 }
372
373 fn web_server(
375 did_url: &'static str,
376 pubkeys: &'static str,
377 ) -> Result<(String, impl FnOnce() -> Result<(), ()>), hyper::Error> {
378 use http::header::{HeaderValue, CONTENT_TYPE};
379 use hyper::service::{make_service_fn, service_fn};
380 use hyper::{Body, Response, Server};
381 let addr = ([127, 0, 0, 1], 0).into();
382 let make_svc = make_service_fn(move |_| async move {
383 Ok::<_, hyper::Error>(service_fn(move |req| async move {
384 let uri = req.uri();
385 let proxied_url: String = uri.path().chars().skip(1).collect();
387 if proxied_url == did_url {
388 let body = Body::from(pubkeys);
389 let mut response = Response::new(body);
390 response
391 .headers_mut()
392 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
393 return Ok::<_, hyper::Error>(response);
394 }
395
396 let (mut parts, body) = Response::<Body>::default().into_parts();
397 parts.status = hyper::StatusCode::NOT_FOUND;
398 let response = Response::from_parts(parts, body);
399 Ok::<_, hyper::Error>(response)
400 }))
401 });
402 let server = Server::try_bind(&addr)?.serve(make_svc);
403 let url = "http://".to_string() + &server.local_addr().to_string() + "/";
404 let (shutdown_tx, shutdown_rx) = futures::channel::oneshot::channel();
405 let graceful = server.with_graceful_shutdown(async {
406 shutdown_rx.await.ok();
407 });
408 tokio::task::spawn(async move {
409 graceful.await.ok();
410 });
411 let shutdown = || shutdown_tx.send(());
412 Ok((url, shutdown))
413 }
414
415 #[tokio::test]
416 async fn from_did_webkey_ssh() {
417 let did_url: &str = "https://localhost/user.keys";
419 let pubkeys: &str = include_str!("../tests/ssh_keys");
420
421 let (url, shutdown) = web_server(did_url, pubkeys).unwrap();
422 PROXY.with(|proxy| {
423 proxy.replace(Some(url));
424 });
425 let (res_meta, doc_opt, _doc_meta) = DIDWebKey
426 .resolve(
427 "did:webkey:ssh:localhost:user.keys",
428 &ResolutionInputMetadata::default(),
429 )
430 .await;
431 assert_eq!(res_meta.error, None);
432 let value_expected = json!({
433 "@context": "https://www.w3.org/ns/did/v1",
434 "assertionMethod": [
435 "did:webkey:ssh:localhost:user.keys#UgSgEP0VYvWxHUqK_RKifG5eZB-61optu51mu-XNO-w",
436 "did:webkey:ssh:localhost:user.keys#AbXY44NRrppCuX0olBDjpfNjdEiitV-W1jVTqy2ixnE",
437 "did:webkey:ssh:localhost:user.keys#uqnr0fDZhtGue_7PgMJrRrrtf5M508uKm7yJCdISMyA"
438 ],
439 "authentication": [
440 "did:webkey:ssh:localhost:user.keys#UgSgEP0VYvWxHUqK_RKifG5eZB-61optu51mu-XNO-w",
441 "did:webkey:ssh:localhost:user.keys#AbXY44NRrppCuX0olBDjpfNjdEiitV-W1jVTqy2ixnE",
442 "did:webkey:ssh:localhost:user.keys#uqnr0fDZhtGue_7PgMJrRrrtf5M508uKm7yJCdISMyA"
443 ],
444 "id": "did:webkey:ssh:localhost:user.keys",
445 "verificationMethod": [
446 {
447 "controller": "did:webkey:ssh:localhost:user.keys",
448 "id": "did:webkey:ssh:localhost:user.keys#UgSgEP0VYvWxHUqK_RKifG5eZB-61optu51mu-XNO-w",
449 "publicKeyJwk": {
450 "crv": "Ed25519",
451 "kty": "OKP",
452 "x": "82ecCx4s9pTDh_tFeG6SlKMl6DuhSORCwgMnR7azq0k"
453 },
454 "type": "Ed25519VerificationKey2018"
455 },
456 {
457 "controller": "did:webkey:ssh:localhost:user.keys",
458 "id": "did:webkey:ssh:localhost:user.keys#AbXY44NRrppCuX0olBDjpfNjdEiitV-W1jVTqy2ixnE",
459 "publicKeyJwk": {
460 "e": "AQAB",
461 "kty": "RSA",
462 "n": "qy52x0R83O2uqWUWdcqZuWLbBhhyHeZld72Yrl_EOob1LPkPzoQPn6BWWYwpv2arBeXX90PiGN0EvCnQdoYcUNTjWdArgsE3XUWeJeeEvhvx0RHMnU4Mtd9FwTJ2iJIGrGcQ-wRcHb_BE5jEu9yF6qjnnoQcYVJZUCEnwkHMhyQdbGTBfkaKiDgV7kqfnAjc8xwW5sUz9ylZb-7_mniVBSwdeTRUIzROfDF9lXYSBGWMZIvP2bqY39y18olYd9FMnLUKJpxYvF195mw-2mWuNKJFZCoi_RSixAQZpMsRkFyD3Z1UynMXYeI9j0qGCtdxCuyfkmyXZlM7MV57PrOUCta4zvam8-zhTmO4fU9HHgqHfd-6MZ7rt5be5WJcqalPoBnJhJaYb_AuobhaYmxwDVlNySKN66nGAud25xT5i7KBFIHESn1kI3dtvs1meihYT8_oEtLfVnXdWVIob0eDTMMiMRrYsGZH3xvzHLeQY3WDEP2Xs_yZxWO3x2jcu17t"
463 },
464 "type": "RsaVerificationKey2018"
465 },
466 {
467 "controller": "did:webkey:ssh:localhost:user.keys",
468 "id": "did:webkey:ssh:localhost:user.keys#uqnr0fDZhtGue_7PgMJrRrrtf5M508uKm7yJCdISMyA",
469 "publicKeyJwk": {
470 "crv": "P-256",
471 "kty": "EC",
472 "x": "Ek29l7abGDIyzyk1lSLjXy0XWMLtXNTMgz3qDT2d7zo",
473 "y": "QTtJ7iCkbV8jT7nk48Qusi7ZQxgnJqu18F-rkOBIlzk"
474 },
475 "type": "EcdsaSecp256r1VerificationKey2019"
476 }
477 ]
478 });
479 let doc = doc_opt.unwrap();
480 let doc_value = serde_json::to_value(doc).unwrap();
481 eprintln!("doc {}", serde_json::to_string_pretty(&doc_value).unwrap());
482 assert_eq!(doc_value, value_expected);
483 PROXY.with(|proxy| {
484 proxy.replace(None);
485 });
486 shutdown().ok();
487 }
488
489 #[test_log::test(tokio::test)]
490 async fn from_did_webkey_gpg() {
491 let did_url: &str = "https://localhost/user.gpg";
492 let pubkeys: &str = include_str!("../tests/user.gpg");
493
494 let (url, shutdown) = web_server(did_url, pubkeys).unwrap();
495 PROXY.with(|proxy| {
496 proxy.replace(Some(url));
497 });
498 let (res_meta, doc_opt, _doc_meta) = DIDWebKey
499 .resolve(
500 "did:webkey:gpg:localhost:user.gpg",
501 &ResolutionInputMetadata::default(),
502 )
503 .await;
504 assert_eq!(res_meta.error, None);
505
506 let value_expected = json!({
507 "@context": "https://www.w3.org/ns/did/v1",
508 "assertionMethod": [
509 "did:webkey:gpg:localhost:user.gpg#0CEE8B84B25C0A3C554A9EC1F8FEE972E2A1D935",
510 "did:webkey:gpg:localhost:user.gpg#6BABBD68A84D5FE3CEEB986EB77927AE619B8EB6",
511 "did:webkey:gpg:localhost:user.gpg#DCB1FF1899328C0EBB5DF07BD41BBBD1FE58006E"
512 ],
513 "authentication": [
514 "did:webkey:gpg:localhost:user.gpg#0CEE8B84B25C0A3C554A9EC1F8FEE972E2A1D935",
515 "did:webkey:gpg:localhost:user.gpg#6BABBD68A84D5FE3CEEB986EB77927AE619B8EB6",
516 "did:webkey:gpg:localhost:user.gpg#DCB1FF1899328C0EBB5DF07BD41BBBD1FE58006E"
517 ],
518 "id": "did:webkey:gpg:localhost:user.gpg",
519 "verificationMethod": [
520 {
521 "controller": "did:webkey:gpg:localhost:user.gpg",
522 "id": "did:webkey:gpg:localhost:user.gpg#0CEE8B84B25C0A3C554A9EC1F8FEE972E2A1D935",
523 "publicKeyPgp": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: 0CEE 8B84 B25C 0A3C 554A 9EC1 F8FE E972 E2A1 D935\nComment: Foobar <foobar@example.org>\n\n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\n2YdBRVOAnw==\n=A/sJ\n-----END PGP PUBLIC KEY BLOCK-----\n",
524 "type": "PgpVerificationKey2021"
525 },
526 {
527 "controller": "did:webkey:gpg:localhost:user.gpg",
528 "id": "did:webkey:gpg:localhost:user.gpg#6BABBD68A84D5FE3CEEB986EB77927AE619B8EB6",
529 "publicKeyPgp": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: 6BAB BD68 A84D 5FE3 CEEB 986E B779 27AE 619B 8EB6\nComment: Foobar <foobar@example.org>\n\nmFIEYd3nnBMIKoZIzj0DAQcCAwRhnJmDiD35LzJXstn4zBMfpavUCSkYzyJKIYHe\nOwW4BFe+AF/ZdczzJnx8O1xndvYOFccVNAz7HMb7xPB7MDcEtBtGb29iYXIgPGZv\nb2JhckBleGFtcGxlLm9yZz6IkAQTEwgAOBYhBGurvWioTV/jzuuYbrd5J65hm462\nBQJh3eecAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJELd5J65hm462BNgB\nAKzxt0M3BpEGlAGjz4czrWX8zRdo6XiKeby5yeORfKDEAP4uOuIwE9ics9XICXUg\n1IZhOVNB2cUS6p7Q5ApaqwE3WbhWBGHd55wSCCqGSM49AwEHAgMEN0OVHjy6Pwyp\nfTci+EKIc486T1EGeYBs/1FErq3bB44Vqr3EsOcdscSqyj3dcxXb47d0kOkiDPKm\nKTy/6ZPWsAMBCAeIeAQYEwgAIBYhBGurvWioTV/jzuuYbrd5J65hm462BQJh3eec\nAhsMAAoJELd5J65hm462KTsA/3vbivQARQMsZfGKptW/SVaKwszMQm2SE+jOESoH\ntk3MAQCjUD7O3CzMX2rCDgLBLh6hwgB3zjn8uaHM1zO9Z48HhQ==\n=97RS\n-----END PGP PUBLIC KEY BLOCK-----\n",
530 "type": "PgpVerificationKey2021"
531 },
532 {
533 "controller": "did:webkey:gpg:localhost:user.gpg",
534 "id": "did:webkey:gpg:localhost:user.gpg#DCB1FF1899328C0EBB5DF07BD41BBBD1FE58006E",
535 "publicKeyPgp": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: DCB1 FF18 9932 8C0E BB5D F07B D41B BBD1 FE58 006E\nComment: Foobar <foobar@example.org>\n\nmDMEYd3nyxYJKwYBBAHaRw8BAQdAp756gWZbZB66yTjjn52DyUvCxUgFG7aSKqYY\n7KG2KvC0G0Zvb2JhciA8Zm9vYmFyQGV4YW1wbGUub3JnPoiQBBMWCAA4FiEE3LH/\nGJkyjA67XfB71Bu70f5YAG4FAmHd58sCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\nF4AACgkQ1Bu70f5YAG7IMQD7BEg3vAqinv1wllBpXfQov7b4+haxcADWXgmc+06D\nx1QBAMWd6Oa71iKafJKKL3Vgk5q/Sns5+xDvMJmcGbMemckMuDgEYd3nyxIKKwYB\nBAGXVQEFAQEHQECEkuj4GJuUKC0nKvyXoEA1DxJPnASFt2GPC0trMcMoAwEIB4h4\nBBgWCAAgFiEE3LH/GJkyjA67XfB71Bu70f5YAG4FAmHd58sCGwwACgkQ1Bu70f5Y\nAG6eUAEA8vwHBMR4ownA069pQ2EqGhueMoU7YQX0IQBosDf7NrMBAJCoLmuc2dGQ\nT4/C2SFSd3mgOqJXpumOyBFj6hoYkyAI\n=gMz4\n-----END PGP PUBLIC KEY BLOCK-----\n",
536 "type": "PgpVerificationKey2021"
537 }
538 ]
539 });
540
541 let doc = doc_opt.unwrap();
542 let doc_value = serde_json::to_value(doc).unwrap();
543 pretty_assertions::assert_eq!(doc_value, value_expected);
544 PROXY.with(|proxy| {
545 proxy.replace(None);
546 });
547 shutdown().ok();
548 }
549}