diff --git a/.gitignore b/.gitignore index 96ef6c0b9..c185eafdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target Cargo.lock + +.vscode diff --git a/Cargo.toml b/Cargo.toml index 5c3054ed8..de074dfd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,12 @@ description = "Core library for Verifiable Credentials and Decentralized Identif repository = "https://github.com/spruceid/ssi/" documentation = "https://docs.rs/ssi/" -exclude = [ - "json-ld-api/*", - "json-ld-normalization/*", -] +exclude = ["json-ld-api/*", "json-ld-normalization/*"] [features] default = ["ring"] http-did = ["hyper", "hyper-tls", "http", "percent-encoding", "tokio"] -libsecp256k1 = ["secp256k1"] # backward compatibility +libsecp256k1 = ["secp256k1"] # backward compatibility secp256k1 = ["k256", "rand", "k256/keccak256"] secp256r1 = ["p256", "rand"] ripemd-160 = ["ripemd160", "secp256k1"] @@ -58,7 +55,12 @@ lazy_static = "1.4" combination = "0.1" sha2 = { version = "0.9", optional = true } sha2_old = { package = "sha2", version = "0.8" } -hyper = { version = "0.14", optional = true, features = ["server", "client", "http1", "stream"] } +hyper = { version = "0.14", optional = true, features = [ + "server", + "client", + "http1", + "stream", +] } hyper-tls = { version = "0.5", optional = true } http = { version = "0.2", optional = true } hex = "0.4" @@ -77,6 +79,7 @@ p256 = { version = "0.8", optional = true, features = ["zeroize", "ecdsa"] } ssi-contexts = { version = "0.1.2", path = "contexts/" } ripemd160 = { version = "0.9", optional = true } sshkeys = "0.3" +sequoia-openpgp = "1.7" reqwest = { version = "0.11", features = ["json"] } flate2 = "1.0" bitvec = "0.20" @@ -106,7 +109,7 @@ members = [ ] [dev-dependencies] -blake2 = "0.8" # for bbs doctest +blake2 = "0.8" # for bbs doctest uuid = { version = "0.8", features = ["v4", "serde"] } difference = "2.0" did-method-key = { path = "./did-key" } diff --git a/did-webkey/Cargo.toml b/did-webkey/Cargo.toml index 782704abc..808fc19c8 100644 --- a/did-webkey/Cargo.toml +++ b/did-webkey/Cargo.toml @@ -15,10 +15,15 @@ documentation = "https://docs.rs/did-webkey/" p256 = ["ssi/p256"] [dependencies] -ssi = { version = "0.3", path = "../", default-features = false } +ssi = { version = "0.3", path = "../", features = [ + "rand", + "ring", + "p256", +], default-features = false } async-trait = "0.1" reqwest = { version = "0.11", features = ["json"] } http = "0.2" +sequoia-openpgp = "1.7" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } sshkeys = "0.3" diff --git a/did-webkey/src/lib.rs b/did-webkey/src/lib.rs index 973ccc1c3..1516f9c58 100644 --- a/did-webkey/src/lib.rs +++ b/did-webkey/src/lib.rs @@ -3,11 +3,21 @@ use core::str::FromStr; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use openpgp::Packet; +use openpgp::{ + packet::{ + key::{KeyParts, KeyRole}, + Key, + }, + parse::{PacketParser, PacketParserResult, Parse}, +}; +use sequoia_openpgp as openpgp; use sshkeys::PublicKeyKind; use ssi::did::{DIDMethod, Document, VerificationMethod, VerificationMethodMap, DIDURL}; use ssi::did_resolve::{ DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID, }; +use ssi::gpg::gpg_pkk_to_jwk; use ssi::ssh::ssh_pkk_to_jwk; // For testing, enable handling requests at localhost. @@ -39,11 +49,58 @@ impl FromStr for DIDWebKeyType { } fn parse_pubkeys_gpg( - _did: &str, - _bytes: Vec, + did: &str, + bytes: Vec, ) -> Result<(Vec, Vec), String> { - // TODO - Err(String::from("GPG Key Type Not Implemented")) + let mut ppr = PacketParser::from_bytes(&bytes) + .map_err(|e| format!("Unable to parse GPG keyring: {}", e))?; + let mut did_urls = Vec::new(); + let mut vm_maps = Vec::new(); + + while let PacketParserResult::Some(pp) = ppr { + let (packet, next_ppr) = pp + .recurse() + .map_err(|e| format!("Error occured parsing keyring: {}", e))?; + ppr = next_ppr; + + // packet is expected to be a public key + if let Packet::PublicKey(pk) = packet { + 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)) +} + +fn gpg_pk_to_vm( + did: &str, + pk: Key, +) -> Result<(VerificationMethodMap, DIDURL), String> { + let jwk = + gpg_pkk_to_jwk(&pk).map_err(|e| format!("Unable to convert GPG key to JWK: {}", e))?; + let thumbprint = jwk + .thumbprint() + .map_err(|e| format!("Unable to calculate JWK thumbprint: {}", e))?; + let vm_url = DIDURL { + did: did.to_string(), + fragment: Some(thumbprint), + ..Default::default() + }; + let vm_map = VerificationMethodMap { + id: vm_url.to_string(), + type_: "PgpVerificationKey2021".to_string(), + public_key_jwk: Some(jwk), + controller: did.to_string(), + ..Default::default() + }; + Ok((vm_map, vm_url)) } fn pk_to_vm_ed25519( @@ -348,23 +405,22 @@ mod tests { ); } - // TODO: use JWK fingerprint - const DID_URL: &str = "https://localhost/user.keys"; - const PUBKEYS: &str = include_str!("../tests/ssh_keys"); // localhost web server for serving did:web DID documents. - // TODO: pass arguments here instead of using const - fn web_server() -> Result<(String, impl FnOnce() -> Result<(), ()>), hyper::Error> { + fn web_server( + did_url: &'static str, + pubkeys: &'static str, + ) -> Result<(String, impl FnOnce() -> Result<(), ()>), hyper::Error> { use http::header::{HeaderValue, CONTENT_TYPE}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Response, Server}; let addr = ([127, 0, 0, 1], 0).into(); - let make_svc = make_service_fn(|_| async move { - Ok::<_, hyper::Error>(service_fn(|req| async move { + let make_svc = make_service_fn(move |_| async move { + Ok::<_, hyper::Error>(service_fn(move |req| async move { let uri = req.uri(); // Skip leading slash let proxied_url: String = uri.path().chars().skip(1).collect(); - if proxied_url == DID_URL { - let body = Body::from(PUBKEYS); + if proxied_url == did_url { + let body = Body::from(pubkeys); let mut response = Response::new(body); response .headers_mut() @@ -392,8 +448,12 @@ mod tests { } #[tokio::test] - async fn from_did_webkey() { - let (url, shutdown) = web_server().unwrap(); + async fn from_did_webkey_ssh() { + // TODO: use JWK fingerprint + let did_url: &str = "https://localhost/user.keys"; + let pubkeys: &str = include_str!("../tests/ssh_keys"); + + let (url, shutdown) = web_server(did_url, pubkeys).unwrap(); PROXY.with(|proxy| { proxy.replace(Some(url)); }); @@ -460,4 +520,33 @@ mod tests { }); shutdown().ok(); } + + #[tokio::test] + async fn from_did_webkey_gpg() { + // TODO: use JWK fingerprint + let did_url: &str = "https://localhost/user.gpg"; + let pubkeys: &str = include_str!("../tests/user.gpg"); + + let (url, shutdown) = web_server(did_url, pubkeys).unwrap(); + PROXY.with(|proxy| { + proxy.replace(Some(url)); + }); + let (res_meta, doc_opt, _doc_meta) = DIDWebKey + .resolve( + "did:webkey:gpg:localhost:user.gpg", + &ResolutionInputMetadata::default(), + ) + .await; + assert_eq!(res_meta.error, None); + // TODO: put correct JSON here + let value_expected = json!({}); + let doc = doc_opt.unwrap(); + let doc_value = serde_json::to_value(doc).unwrap(); + eprintln!("doc {}", serde_json::to_string_pretty(&doc_value).unwrap()); + assert_eq!(doc_value, value_expected); + PROXY.with(|proxy| { + proxy.replace(None); + }); + shutdown().ok(); + } } diff --git a/did-webkey/tests/user.gpg b/did-webkey/tests/user.gpg new file mode 100644 index 000000000..1e4345f18 --- /dev/null +++ b/did-webkey/tests/user.gpg @@ -0,0 +1,73 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +RvrMVRGyvqSEFFKDe38= +=ToEN +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/gpg.rs b/src/gpg.rs new file mode 100644 index 000000000..ecc60013b --- /dev/null +++ b/src/gpg.rs @@ -0,0 +1,67 @@ +use crate::jwk::{Base64urlUInt, Params as JWKParams, JWK}; +use openpgp::{ + crypto::mpi::PublicKey, + packet::{ + key::{KeyParts, KeyRole}, + Key, + }, + types::{Curve, PublicKeyAlgorithm}, +}; +use sequoia_openpgp as openpgp; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum GpgKeyToJWKError { + #[error("Unsupported GPG key type")] + UnsupportedGpgKeyType, + #[error("Unsupported GPG public key algorithm")] + UnsupportedGpgPkAlgorithm, + #[error("P-256 parse error: {0}")] + P256Parse(String), + #[error("Unsupported ECDSA key type: {0}")] + UnsupportedEcdsaKey(String), + #[error("Missing features: {0}")] + MissingFeatures(&'static str), +} + +/// Convert a GPG public key to a JWK. +pub fn gpg_pkk_to_jwk(pkk: &Key) -> Result { + // https://datatracker.ietf.org/doc/html/rfc4253#section-6.6 + if let Key::V4(key) = pkk { + match key.pk_algo() { + PublicKeyAlgorithm::RSAEncryptSign + | PublicKeyAlgorithm::ECDSA + | PublicKeyAlgorithm::EdDSA => match key.mpis() { + PublicKey::RSA { e, n } => Ok(JWK::from(JWKParams::RSA( + crate::jwk::RSAParams::new_public(e.value(), n.value()), + ))), + PublicKey::ECDSA { curve, q } => { + if curve == &Curve::NistP256 { + #[cfg(not(feature = "p256"))] + { + Err(GpgKeyToJWKError::MissingFeatures("p256")) + } + #[cfg(feature = "p256")] + { + crate::jwk::p256_parse(q.value()) + .map_err(|e| GpgKeyToJWKError::P256Parse(e.to_string())) + } + } else { + Err(GpgKeyToJWKError::UnsupportedEcdsaKey(curve.to_string())) + } + } + PublicKey::EdDSA { curve, q } => { + Ok(JWK::from(JWKParams::OKP(crate::jwk::OctetParams { + curve: curve.to_string(), + public_key: Base64urlUInt(q.value().to_vec()), + private_key: None, + }))) + } + _ => panic!("Something went wrong"), + }, + _ => Err(GpgKeyToJWKError::UnsupportedGpgPkAlgorithm), + } + } else { + Err(GpgKeyToJWKError::UnsupportedGpgKeyType) + } +} diff --git a/src/lib.rs b/src/lib.rs index 59aee73e3..a0187dab4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod did_resolve; #[cfg(feature = "keccak-hash")] pub mod eip712; pub mod error; +pub mod gpg; pub mod hash; pub mod jsonld; pub mod jwk;