did_webkey/
lib.rs

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// For testing, enable handling requests at localhost.
16#[cfg(test)]
17use std::cell::RefCell;
18#[cfg(test)]
19thread_local! {
20    static PROXY: RefCell<Option<String>> = RefCell::new(None);
21}
22
23/// did:webkey Method
24pub 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    // For compatibility with sequoia-openpgp
74    // Note that the output still won't be identical because sequoia uses the new style of CTB whilst rpgp doesn't
75    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        // Workaround to have the same key multiple times (`Comment`)
91        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            // TODO: use .well-known?
227            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        // TODO: https://w3c-ccg.github.io/did-method-web/#in-transit-security
258        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        // TODO: set document created/updated metadata from HTTP headers?
332        (
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    // localhost web server for serving did:web DID documents.
374    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                // Skip leading slash
386                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        // TODO: use JWK fingerprint
418        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}