diff --git a/Cargo.toml b/Cargo.toml index 5e9d47c0d..1073469a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ license = "Apache-2.0" description = "Core library for Verifiable Credentials and Decentralized Identifiers." repository = "https://github.com/spruceid/ssi/" documentation = "https://docs.rs/ssi/" +resolver = "2" exclude = ["json-ld-api/*", "json-ld-normalization/*"] diff --git a/README.md b/README.md index deb9026ca..25a848b0a 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,20 @@ packages. ``` clang openssl-devel +``` + +If using feature `did-webkey/sequoia-openpgp` for PGP support, the following +dependencies are also needed: + +``` nettle-dev capnproto ``` +If using feature +[`did-webkey/crypto-cng`](https://gitlab.com/sequoia-pgp/sequoia#cryptography), +only `capnproto` is needed. + ## Install ### Crates.io diff --git a/did-webkey/Cargo.toml b/did-webkey/Cargo.toml index 4b0de587c..16c67f48b 100644 --- a/did-webkey/Cargo.toml +++ b/did-webkey/Cargo.toml @@ -25,14 +25,25 @@ ssi = { version = "0.3", path = "../", features = [ ], default-features = false } async-trait = "0.1" reqwest = { version = "0.11", features = ["json"] } +hex = "0.4" http = "0.2" -sequoia-openpgp = { version = "1.7", features = [ - "compression-deflate", -], default-features = false } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } sshkeys = "0.3" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +sequoia-openpgp = { version = "1.7", features = [ + "compression-deflate", +], default-features = false, optional = true } +# HACK: temp, point to crates once pgp publishes a version that doesn't require zeroize=1.3.0 +pgp = { git = "https://github.com/rpgp/rpgp", rev = "21081b6aaaaa5750ab937cfef30bae879a740d23", optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +# HACK: same thing as above +pgp = { git = "https://github.com/rpgp/rpgp", rev = "21081b6aaaaa5750ab937cfef30bae879a740d23", features = [ + "wasm", +] } + [target.'cfg(target_os = "android")'.dependencies.reqwest] version = "0.11" features = ["json", "native-tls-vendored"] diff --git a/did-webkey/src/lib.rs b/did-webkey/src/lib.rs index b3892b03e..cc57d1b29 100644 --- a/did-webkey/src/lib.rs +++ b/did-webkey/src/lib.rs @@ -3,11 +3,19 @@ use core::str::FromStr; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +#[cfg(all(not(target_arch = "wasm32"), feature = "sequoia-openpgp"))] use openpgp::{ cert::prelude::*, parse::{PacketParser, Parse}, serialize::SerializeInto, }; +#[cfg(any(target_arch = "wasm32", feature = "pgp"))] +use pgp::{ + composed::{PublicOrSecret, SignedPublicKey}, + errors::Error as PgpError, + types::KeyTrait, +}; +#[cfg(all(not(target_arch = "wasm32"), feature = "sequoia-openpgp"))] use sequoia_openpgp as openpgp; use sshkeys::PublicKeyKind; use ssi::did::{DIDMethod, Document, VerificationMethod, VerificationMethodMap, DIDURL}; @@ -16,6 +24,11 @@ use ssi::did_resolve::{ }; use ssi::ssh::ssh_pkk_to_jwk; +#[cfg(all(feature = "sequoia-openpgp", feature = "pgp"))] +compile_error!( + "Feature \"sequoia-openpgp\" and feature \"pgp\" cannot be enabled at the same time" +); + // For testing, enable handling requests at localhost. #[cfg(test)] use std::cell::RefCell; @@ -44,6 +57,7 @@ impl FromStr for DIDWebKeyType { } } +#[cfg(all(not(target_arch = "wasm32"), feature = "sequoia-openpgp"))] fn parse_pubkeys_gpg( did: &str, bytes: Vec, @@ -68,6 +82,7 @@ fn parse_pubkeys_gpg( Ok((vm_maps, did_urls)) } +#[cfg(all(not(target_arch = "wasm32"), feature = "sequoia-openpgp"))] fn gpg_pk_to_vm(did: &str, cert: Cert) -> Result<(VerificationMethodMap, DIDURL), String> { let vm_url = DIDURL { did: did.to_string(), @@ -92,6 +107,68 @@ fn gpg_pk_to_vm(did: &str, cert: Cert) -> Result<(VerificationMethodMap, DIDURL) Ok((vm_map, vm_url)) } +#[cfg(any(target_arch = "wasm32", feature = "pgp"))] +fn parse_pubkeys_gpg( + did: &str, + bytes: Vec, +) -> Result<(Vec, Vec), String> { + use std::io::Cursor; + + let mut did_urls = Vec::new(); + let mut vm_maps = Vec::new(); + + let c = Cursor::new(bytes); + // BUG: This seems to yield only one key. + let keys = pgp::composed::signed_key::parse::from_armor_many(c) + .map_err(|e| format!("Unable to parse GPG keyring: {}", e))? + .0 + .collect::, PgpError>>() + .map_err(|e| format!("Unable to parse GPG keyring: {}", e))?; + + for key in keys { + // ignore if secret key (which shouldn't happen) + if let PublicOrSecret::Public(pk) = key { + let (vm_map, did_url) = gpg_pk_to_vm(did, pk).map_err(|e| { + format!( + "Unable to convert GPG public key to verification method: {}", + e + ) + })?; + vm_maps.push(vm_map); + did_urls.push(did_url); + } + } + + Ok((vm_maps, did_urls)) +} + +#[cfg(any(target_arch = "wasm32", feature = "pgp"))] +fn gpg_pk_to_vm( + did: &str, + key: SignedPublicKey, +) -> Result<(VerificationMethodMap, DIDURL), String> { + let fingerprint: String = hex::encode_upper(key.fingerprint()); + + let vm_url = DIDURL { + did: did.to_string(), + fragment: Some(fingerprint), + ..Default::default() + }; + + let armored_pgp = key + .to_armored_string(None) + .map_err(|e| format!("Failed to re-serialize cert: {}", e))?; + + let vm_map = VerificationMethodMap { + id: vm_url.to_string(), + type_: "PgpVerificationKey2021".to_string(), + public_key_pgp: Some(armored_pgp), + controller: did.to_string(), + ..Default::default() + }; + Ok((vm_map, vm_url)) +} + fn pk_to_vm_ed25519( did: &str, pk: sshkeys::Ed25519PublicKey, @@ -420,7 +497,7 @@ mod tests { let (mut parts, body) = Response::::default().into_parts(); parts.status = hyper::StatusCode::NOT_FOUND; let response = Response::from_parts(parts, body); - return Ok::<_, hyper::Error>(response); + Ok::<_, hyper::Error>(response) })) }); let server = Server::try_bind(&addr)?.serve(make_svc); @@ -526,6 +603,7 @@ mod tests { ) .await; assert_eq!(res_meta.error, None); + // NOTE: sequoia-pgp and rpgp will likely produce slightly different output let value_expected = json!({ "@context": "https://www.w3.org/ns/did/v1", "assertionMethod": [