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/src/lib.rs b/did-webkey/src/lib.rs index da06d0c41..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( @@ -476,12 +533,13 @@ mod tests { }); let (res_meta, doc_opt, _doc_meta) = DIDWebKey .resolve( - "did:webkey:gpg:localhost:user.keys", + "did:webkey:gpg:localhost:user.gpg", &ResolutionInputMetadata::default(), ) .await; assert_eq!(res_meta.error, None); - // let value_expected = unimplemented!(); + // 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()); 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;