Skip to content

Commit

Permalink
Add did-jwk support
Browse files Browse the repository at this point in the history
Signed-off-by: Tiago Nascimento <[email protected]>
  • Loading branch information
theosirian committed Oct 6, 2022
1 parent 5f635fc commit 5b9fc5e
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 0 deletions.
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.

## 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/
248 changes: 248 additions & 0 deletions did-jwk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
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,
};

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) => {
// TODO: pass through these errors somehow
return (
ResolutionMetadata {
error: Some(ERROR_INVALID_DID.to_string()),
content_type: None,
property_set: None,
},
None,
None,
);
}
};

let 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 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 = serde_jcs::to_string(&jwk).unwrap();
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);
}
}

0 comments on commit 5b9fc5e

Please sign in to comment.