Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add did-jwk support #466

Merged
merged 3 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ ssi-caips = { path = "./ssi-caips", version = "0.1" }
[workspace]
members = [
"did-tezos",
"did-jwk",
"did-key",
"did-web",
"did-ethr",
Expand Down
38 changes: 38 additions & 0 deletions did-jwk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[package]
name = "did-method-jwk"
version = "0.1.0"
authors = ["Spruce Systems, Inc."]
edition = "2021"
license = "Apache-2.0"
keywords = ["ssi", "did"]
categories = ["cryptography::cryptocurrencies"]
description = "did:jwk DID method, using the ssi crate"
repository = "https://github.com/spruceid/ssi/"
homepage = "https://github.com/spruceid/ssi/tree/main/did-jwk/"
documentation = "https://docs.rs/did-jwk/"

[features]
default = ["secp256k1", "secp256r1"]
secp256k1 = ["ssi-dids/secp256k1"]
secp256r1 = ["ssi-dids/secp256r1"]
ssi_p384 = ["openssl"]

ring = ["ssi-dids/ring"]
openssl = ["ssi-dids/openssl"]

[dependencies]
ssi-dids = { path = "../ssi-dids", version = "0.1", default-features = false }
ssi-jwk = { path = "../ssi-jwk", version = "0.1", default-features = false, features = [
"ripemd-160",
] }
async-trait = "0.1"
multibase = "0.8"
serde_json = "1.0"
serde_jcs = "0.1"

[dev-dependencies]
ssi = { version = "0.4", path = "../", default-features = false }
serde = { version = "1.0", features = ["derive"] }
async-std = { version = "1.9", features = ["attributes"] }
serde_json = "1.0"
serde_jcs = "0.1"
10 changes: 10 additions & 0 deletions did-jwk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# did-key

Rust implementation of the [did:jwk][] DID Method, based on the [ssi][] library.
chunningham marked this conversation as resolved.
Show resolved Hide resolved

## License

[Apache License, Version 2.0](http://www.apache.org/licenses/)

[did:jwk]: https://github.com/quartzjer/did-jwk/blob/main/spec.md
[ssi]: https://github.com/spruceid/ssi/
280 changes: 280 additions & 0 deletions did-jwk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
use async_trait::async_trait;

use ssi_dids::did_resolve::{
DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID,
ERROR_NOT_FOUND,
};
use ssi_dids::{
Context, Contexts, DIDMethod, Document, Source, VerificationMethod, VerificationMethodMap,
DEFAULT_CONTEXT, DIDURL,
};
use ssi_jwk::JWK;

pub struct DIDJWK;

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl DIDResolver for DIDJWK {
async fn resolve(
&self,
did: &str,
_input_metadata: &ResolutionInputMetadata,
) -> (
ResolutionMetadata,
Option<Document>,
Option<DocumentMetadata>,
) {
if !did.starts_with("did:jwk:") {
return (
ResolutionMetadata {
error: Some(ERROR_INVALID_DID.to_string()),
content_type: None,
property_set: None,
},
None,
None,
);
}
let method_specific_id = &did[8..];
let data = match multibase::Base::decode(&multibase::Base::Base64Url, method_specific_id) {
Ok(data) => data,
Err(_err) => {
return (
ResolutionMetadata {
error: Some(ERROR_INVALID_DID.to_string()),
content_type: None,
property_set: None,
},
None,
None,
);
}
};

let jwk: JWK = if let Ok(jwk) = serde_json::from_slice(&data) {
jwk
} else {
return (
ResolutionMetadata {
error: Some(ERROR_NOT_FOUND.to_string()),
content_type: None,
property_set: None,
},
None,
None,
);
};

let public_jwk = jwk.to_public();

if public_jwk != jwk {
return (
ResolutionMetadata {
error: Some(ERROR_INVALID_DID.to_string()),
content_type: None,
property_set: None,
},
None,
None,
);
}

let vm_didurl = DIDURL {
did: did.to_string(),
fragment: Some("0".to_string()),
..Default::default()
};
let doc = Document {
context: Contexts::Many(vec![
Context::URI(DEFAULT_CONTEXT.to_string()),
Context::URI("https://w3id.org/security/suites/jws-2020/v1".to_string()),
]),
id: did.to_string(),
verification_method: Some(vec![VerificationMethod::Map(VerificationMethodMap {
id: vm_didurl.to_string(),
type_: "JsonWebKey2020".to_string(),
controller: did.to_string(),
public_key_jwk: Some(jwk),
..Default::default()
})]),
assertion_method: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]),
authentication: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]),
capability_invocation: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]),
capability_delegation: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]),
key_agreement: Some(vec![VerificationMethod::DIDURL(vm_didurl)]),
..Default::default()
};
(
ResolutionMetadata::default(),
Some(doc),
Some(DocumentMetadata::default()),
)
}
}

impl DIDMethod for DIDJWK {
fn name(&self) -> &'static str {
"jwk"
}

fn generate(&self, source: &Source) -> Option<String> {
let jwk = match source {
Source::Key(jwk) => jwk,
Source::KeyAndPattern(jwk, pattern) => {
if !pattern.is_empty() {
// pattern not supported
return None;
}
jwk
}
_ => return None,
};
let jwk = jwk.to_public();
let jwk = if let Ok(jwk) = serde_jcs::to_string(&jwk) {
jwk
} else {
return None;
};

let did =
"did:jwk:".to_string() + &multibase::encode(multibase::Base::Base64Url, &jwk)[1..];
Some(did)
}

fn to_resolver(&self) -> &dyn DIDResolver {
self
}
}

#[cfg(test)]
mod tests {
use super::*;
use ssi_dids::did_resolve::{dereference, Content, DereferencingInputMetadata};
use ssi_dids::Resource;

#[async_std::test]
async fn from_p256() {
let vm = "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0";
let (res_meta, object, _meta) =
dereference(&DIDJWK, vm, &DereferencingInputMetadata::default()).await;
assert_eq!(res_meta.error, None);
let vm = match object {
Content::Object(Resource::VerificationMethod(vm)) => vm,
_ => unreachable!(),
};

assert_eq!(vm.id, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" );
assert_eq!(vm.type_, "JsonWebKey2020");
assert_eq!(vm.controller, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9");

assert!(vm.public_key_jwk.is_some());
let jwk = serde_json::from_value(serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0",
"y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE"
}))
.unwrap();
assert_eq!(vm.public_key_jwk.unwrap(), jwk);
}

#[async_std::test]
async fn to_p256() {
let jwk: ssi_jwk::JWK = serde_json::from_value(serde_json::json!({
"crv": "P-256",
"kty": "EC",
"x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0",
"y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE"
}))
.unwrap();
let expected = "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9";
let did = DIDJWK.generate(&Source::Key(&jwk)).unwrap();
assert_eq!(expected, did);

let (res_meta, object, _meta) =
dereference(&DIDJWK, &did, &DereferencingInputMetadata::default()).await;
assert_eq!(res_meta.error, None);

let public_key_jwk = match object {
Content::DIDDocument(document) => match document.verification_method.as_deref() {
Some(
[VerificationMethod::Map(VerificationMethodMap {
ref public_key_jwk, ..
})],
) => public_key_jwk.to_owned().unwrap(),
_ => unreachable!(),
},
_ => unreachable!(),
};
assert_eq!(public_key_jwk, jwk);
}

#[async_std::test]
async fn from_x25519() {
let vm = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0";
let (res_meta, object, _meta) =
dereference(&DIDJWK, vm, &DereferencingInputMetadata::default()).await;
assert_eq!(res_meta.error, None);
let vm = match object {
Content::Object(Resource::VerificationMethod(vm)) => vm,
_ => unreachable!(),
};

assert_eq!(vm.id, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0" );
assert_eq!(vm.type_, "JsonWebKey2020");
assert_eq!(vm.controller, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9");

assert!(vm.public_key_jwk.is_some());
let jwk = serde_json::from_value(serde_json::json!({
"kty": "OKP",
"crv": "X25519",
"use": "enc",
"x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"
}))
.unwrap();
assert_eq!(vm.public_key_jwk.unwrap(), jwk);
}

#[async_std::test]
async fn to_x25519() {
let json = serde_json::json!({
"kty": "OKP",
"crv": "X25519",
"use": "enc",
"x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"
});
let jwk: ssi_jwk::JWK = serde_json::from_value(json).unwrap();
let expected = "did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9";
let did = DIDJWK.generate(&Source::Key(&jwk)).unwrap();
assert_eq!(expected, did);

let (res_meta, object, _meta) =
dereference(&DIDJWK, &did, &DereferencingInputMetadata::default()).await;
assert_eq!(res_meta.error, None);

let public_key_jwk = match object {
Content::DIDDocument(document) => match document.verification_method.as_deref() {
Some(
[VerificationMethod::Map(VerificationMethodMap {
ref public_key_jwk, ..
})],
) => public_key_jwk.to_owned().unwrap(),
_ => unreachable!(),
},
_ => unreachable!(),
};
assert_eq!(public_key_jwk, jwk);
}

#[async_std::test]
async fn deny_private_key() {
let jwk = JWK::generate_ed25519().unwrap();
let json = serde_jcs::to_string(&jwk).unwrap();
let did =
"did:jwk:".to_string() + &multibase::encode(multibase::Base::Base64Url, &json)[1..];

let (res_meta, _object, _meta) =
dereference(&DIDJWK, &did, &DereferencingInputMetadata::default()).await;
assert!(res_meta.error.is_some());
}
}