Skip to content

Commit

Permalink
implement webkey gpg parsing to did
Browse files Browse the repository at this point in the history
  • Loading branch information
fairingrey committed Jan 11, 2022
1 parent 130cbee commit 7e3289d
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/target
Cargo.lock

.vscode
17 changes: 10 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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" }
Expand Down
7 changes: 6 additions & 1 deletion did-webkey/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
119 changes: 104 additions & 15 deletions did-webkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -39,11 +49,58 @@ impl FromStr for DIDWebKeyType {
}

fn parse_pubkeys_gpg(
_did: &str,
_bytes: Vec<u8>,
did: &str,
bytes: Vec<u8>,
) -> Result<(Vec<VerificationMethodMap>, Vec<DIDURL>), 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<P: KeyParts, R: KeyRole>(
did: &str,
pk: Key<P, R>,
) -> 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(
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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));
});
Expand Down Expand Up @@ -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();
}
}
73 changes: 73 additions & 0 deletions did-webkey/tests/user.gpg
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
RvrMVRGyvqSEFFKDe38=
=ToEN
-----END PGP PUBLIC KEY BLOCK-----
67 changes: 67 additions & 0 deletions src/gpg.rs
Original file line number Diff line number Diff line change
@@ -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<P: KeyParts, R: KeyRole>(pkk: &Key<P, R>) -> Result<JWK, GpgKeyToJWKError> {
// 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)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 7e3289d

Please sign in to comment.