Skip to content

Commit

Permalink
(feat) Implement Resolver for did:jwk #1290 (#1299)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmulhearn authored Dec 3, 2024
1 parent a2b5ed9 commit f5f63a0
Show file tree
Hide file tree
Showing 12 changed files with 609 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .github/ci/vdrproxy.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN apk update && apk upgrade && \
USER indy
WORKDIR /home/indy

ARG RUST_VER="1.70.0"
ARG RUST_VER="1.79.0"
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUST_VER --default-host x86_64-unknown-linux-musl

ENV PATH="/home/indy/.cargo/bin:$PATH"
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ members = [
"did_core/public_key",
"misc/simple_message_relay",
"misc/display_as_json",
"did_core/did_methods/did_jwk",
]

[workspace.package]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The repository contains Rust crates to build
- [`did_sov`](did_core/did_methods/did_resolver_sov) - https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html
- [`did_web`](did_core/did_methods/did_resolver_web) - https://w3c-ccg.github.io/did-method-web/
- [`did_key`](did_core/did_methods/did_key) - https://w3c-ccg.github.io/did-method-key/
- [`did_jwk`](did_core/did_methods/did_jwk) - https://github.com/quartzjer/did-jwk/blob/main/spec.md

# Contact
Do you have a question ❓Are you considering using our components? 🚀 We'll be excited to hear from you. 👋
Expand Down
8 changes: 4 additions & 4 deletions did_core/did_doc/src/schema/types/jsonwebkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ impl Display for JsonWebKeyError {
// Unfortunately only supports curves from the original RFC
// pub struct JsonWebKey(jsonwebkey::JsonWebKey);
pub struct JsonWebKey {
kty: String,
crv: String,
x: String,
pub kty: String,
pub crv: String,
pub x: String,
#[serde(flatten)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
extra: HashMap<String, Value>,
pub extra: HashMap<String, Value>,
}

impl JsonWebKey {
Expand Down
18 changes: 18 additions & 0 deletions did_core/did_methods/did_jwk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "did_jwk"
version = "0.1.0"
edition.workspace = true

[dependencies]
did_parser_nom = { path = "../../did_parser_nom" }
did_doc = { path = "../../did_doc" }
did_resolver = { path = "../../did_resolver" }
public_key = { path = "../../public_key", features = ["jwk"] }
async-trait = "0.1.68"
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.96"
base64 = "0.22.1"
thiserror = "1.0.44"

[dev-dependencies]
tokio = { version = "1.38.0", default-features = false, features = ["macros", "rt"] }
15 changes: 15 additions & 0 deletions did_core/did_methods/did_jwk/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DidJwkError {
#[error("DID method not supported: {0}")]
MethodNotSupported(String),
#[error("Base64 encoding error: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("Serde JSON error: {0}")]
SerdeJsonError(#[from] serde_json::Error),
#[error("Public key error: {0}")]
PublicKeyError(#[from] public_key::PublicKeyError),
#[error("DID parser error: {0}")]
DidParserError(#[from] did_parser_nom::ParseError),
}
270 changes: 270 additions & 0 deletions did_core/did_methods/did_jwk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
use std::fmt::{self, Display};

use base64::{
alphabet,
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
Engine,
};
use did_doc::schema::types::jsonwebkey::JsonWebKey;
use did_parser_nom::Did;
use error::DidJwkError;
use public_key::{Key, KeyType};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;

pub mod error;
pub mod resolver;

const USE: &str = "use";
const USE_SIG: &str = "sig";
const USE_ENC: &str = "enc";

/// A default [GeneralPurposeConfig] configuration with a [decode_padding_mode] of
/// [DecodePaddingMode::Indifferent]
const LENIENT_PAD: GeneralPurposeConfig = GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(DecodePaddingMode::Indifferent);

/// A [GeneralPurpose] engine using the [alphabet::URL_SAFE] base64 alphabet and
/// [DecodePaddingMode::Indifferent] config to decode both padded and unpadded.
const URL_SAFE_LENIENT: GeneralPurpose = GeneralPurpose::new(&alphabet::URL_SAFE, LENIENT_PAD);

/// Represents did:key where the DID ID is JWK public key itself
/// See the spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md
#[derive(Clone, Debug, PartialEq)]
pub struct DidJwk {
jwk: JsonWebKey,
did: Did,
}

impl DidJwk {
pub fn parse<T>(did: T) -> Result<DidJwk, DidJwkError>
where
Did: TryFrom<T>,
<Did as TryFrom<T>>::Error: Into<DidJwkError>,
{
let did: Did = did.try_into().map_err(Into::into)?;
Self::try_from(did)
}

pub fn try_from_serialized_jwk(jwk: &str) -> Result<DidJwk, DidJwkError> {
let jwk: JsonWebKey = serde_json::from_str(jwk)?;
Self::try_from(jwk)
}

pub fn jwk(&self) -> &JsonWebKey {
&self.jwk
}

pub fn did(&self) -> &Did {
&self.did
}

pub fn key(&self) -> Result<Key, DidJwkError> {
Ok(Key::from_jwk(&serde_json::to_string(&self.jwk)?)?)
}
}

impl TryFrom<Did> for DidJwk {
type Error = DidJwkError;

fn try_from(did: Did) -> Result<Self, Self::Error> {
match did.method() {
Some("jwk") => {}
other => return Err(DidJwkError::MethodNotSupported(format!("{other:?}"))),
}

let jwk = decode_jwk(did.id())?;
Ok(Self { jwk, did })
}
}

impl TryFrom<JsonWebKey> for DidJwk {
type Error = DidJwkError;

fn try_from(jwk: JsonWebKey) -> Result<Self, Self::Error> {
let encoded_jwk = encode_jwk(&jwk)?;
let did = Did::parse(format!("did:jwk:{encoded_jwk}",))?;

Ok(Self { jwk, did })
}
}

impl TryFrom<Key> for DidJwk {
type Error = DidJwkError;

fn try_from(key: Key) -> Result<Self, Self::Error> {
let jwk = key.to_jwk()?;
let mut jwk: JsonWebKey = serde_json::from_str(&jwk)?;

match key.key_type() {
KeyType::Ed25519
| KeyType::Bls12381g1g2
| KeyType::Bls12381g1
| KeyType::Bls12381g2
| KeyType::P256
| KeyType::P384
| KeyType::P521 => {
jwk.extra.insert(String::from(USE), json!(USE_SIG));
}
KeyType::X25519 => {
jwk.extra.insert(String::from(USE), json!(USE_ENC));
}
}

Self::try_from(jwk)
}
}

impl Display for DidJwk {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.did)
}
}

impl Serialize for DidJwk {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.did.did())
}
}

impl<'de> Deserialize<'de> for DidJwk {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;

DidJwk::parse(s).map_err(de::Error::custom)
}
}

fn encode_jwk(jwk: &JsonWebKey) -> Result<String, DidJwkError> {
let jwk_bytes = serde_json::to_vec(jwk)?;
Ok(URL_SAFE_LENIENT.encode(jwk_bytes))
}

fn decode_jwk(encoded_jwk: &str) -> Result<JsonWebKey, DidJwkError> {
let jwk_bytes = URL_SAFE_LENIENT.decode(encoded_jwk)?;
Ok(serde_json::from_slice(&jwk_bytes)?)
}

#[cfg(test)]
mod tests {
use super::*;

fn valid_key_base58_fingerprint() -> String {
"z6MkeWVt6dndY6EbFwEvb3VQU6ksQXKTeorkQ9sU29DY7yRX".to_string()
}

fn valid_key() -> Key {
Key::from_fingerprint(&valid_key_base58_fingerprint()).unwrap()
}

fn valid_serialized_jwk() -> String {
r#"{
"kty": "OKP",
"crv": "Ed25519",
"x": "ANRjH_zxcKBxsjRPUtzRbp7FSVLKJXQ9APX9MP1j7k4",
"use": "sig"
}"#
.to_string()
}

fn valid_jwk() -> JsonWebKey {
serde_json::from_str(&valid_serialized_jwk()).unwrap()
}

fn valid_encoded_jwk() -> String {
URL_SAFE_LENIENT.encode(serde_json::to_vec(&valid_jwk()).unwrap())
}

fn valid_did_jwk_string() -> String {
format!("did:jwk:{}", valid_encoded_jwk())
}

fn invalid_did_jwk_string_wrong_method() -> String {
format!("did:sov:{}", valid_encoded_jwk())
}

fn invalid_did_jwk_string_invalid_id() -> String {
"did:jwk:somenonsense".to_string()
}

fn valid_did_jwk() -> DidJwk {
DidJwk {
jwk: valid_jwk(),
did: Did::parse(valid_did_jwk_string()).unwrap(),
}
}

#[test]
fn test_serialize() {
assert_eq!(
format!("\"{}\"", valid_did_jwk_string()),
serde_json::to_string(&valid_did_jwk()).unwrap(),
);
}

#[test]
fn test_deserialize() {
assert_eq!(
valid_did_jwk(),
serde_json::from_str::<DidJwk>(&format!("\"{}\"", valid_did_jwk_string())).unwrap(),
);
}

#[test]
fn test_deserialize_error_wrong_method() {
assert!(serde_json::from_str::<DidJwk>(&invalid_did_jwk_string_wrong_method()).is_err());
}

#[test]
fn test_deserialize_error_invalid_id() {
assert!(serde_json::from_str::<DidJwk>(&invalid_did_jwk_string_invalid_id()).is_err());
}

#[test]
fn test_parse() {
assert_eq!(
valid_did_jwk(),
DidJwk::parse(valid_did_jwk_string()).unwrap(),
);
}

#[test]
fn test_parse_error_wrong_method() {
assert!(DidJwk::parse(invalid_did_jwk_string_wrong_method()).is_err());
}

#[test]
fn test_parse_error_invalid_id() {
assert!(DidJwk::parse(invalid_did_jwk_string_invalid_id()).is_err());
}

#[test]
fn test_to_key() {
assert_eq!(valid_did_jwk().key().unwrap(), valid_key());
}

#[test]
fn test_try_from_serialized_jwk() {
assert_eq!(
valid_did_jwk(),
DidJwk::try_from_serialized_jwk(&valid_serialized_jwk()).unwrap(),
);
}

#[test]
fn test_try_from_jwk() {
assert_eq!(valid_did_jwk(), DidJwk::try_from(valid_jwk()).unwrap(),);
}

#[test]
fn test_try_from_key() {
assert_eq!(valid_did_jwk(), DidJwk::try_from(valid_key()).unwrap(),);
}
}
Loading

0 comments on commit f5f63a0

Please sign in to comment.