-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Tiago Nascimento <[email protected]>
- Loading branch information
1 parent
d19575d
commit b4b7f54
Showing
4 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |