Skip to content

Commit

Permalink
Feat/custom verification method (#1334)
Browse files Browse the repository at this point in the history
* Add support for arbitrary (custom) verification method data

* wasm bindings

* custom method type + wasm

* workaround serde's issue

* Update bindings/wasm/src/verification/wasm_method_data.rs

Co-authored-by: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com>

* review comments

* fmt

* review comment

---------

Co-authored-by: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com>
  • Loading branch information
UMR1352 and abdulmth authored Mar 18, 2024
1 parent edb9150 commit 0af29fc
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 163 deletions.
297 changes: 167 additions & 130 deletions bindings/wasm/docs/api-reference.md

Large diffs are not rendered by default.

50 changes: 40 additions & 10 deletions bindings/wasm/src/verification/wasm_method_data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use identity_iota::verification::CustomMethodData;
use identity_iota::verification::MethodData;
use wasm_bindgen::prelude::*;

Expand Down Expand Up @@ -45,22 +46,23 @@ impl WasmMethodData {
Ok(Self(MethodData::PublicKeyJwk(key.0.clone())))
}

/// Creates a new {@link MethodData} variant in CAIP-10 format.
#[wasm_bindgen(js_name = newBlockchainAccountId)]
pub fn new_blockchain_account_id(data: String) -> Self {
Self(MethodData::new_blockchain_account_id(data))
/// Creates a new custom {@link MethodData}.
#[wasm_bindgen(js_name = newCustom)]
pub fn new_custom(name: String, data: JsValue) -> Result<WasmMethodData> {
let data = data.into_serde::<serde_json::Value>().wasm_result()?;
Ok(Self(MethodData::Custom(CustomMethodData { name, data })))
}

/// Returns the wrapped blockchain account id if the format is `BlockchainAccountId`.
#[wasm_bindgen(js_name = tryBlockchainAccountId)]
pub fn try_blockchain_account_id(&self) -> Result<String> {
/// Returns the wrapped custom method data format is `Custom`.
#[wasm_bindgen(js_name = tryCustom)]
pub fn try_custom(&self) -> Result<WasmCustomMethodData> {
self
.0
.blockchain_account_id()
.map(|id| id.to_string())
.custom()
.map(|custom| custom.clone().into())
.ok_or(WasmError::new(
Cow::Borrowed("MethodDataFormatError"),
Cow::Borrowed("method data format is not BlockchainAccountId"),
Cow::Borrowed("method data format is not Custom"),
))
.wasm_result()
}
Expand Down Expand Up @@ -98,3 +100,31 @@ impl From<MethodData> for WasmMethodData {
WasmMethodData(data)
}
}

/// A custom verification method data format.
#[wasm_bindgen(js_name = CustomMethodData, inspectable)]
pub struct WasmCustomMethodData(pub(crate) CustomMethodData);

#[wasm_bindgen(js_class = CustomMethodData)]
impl WasmCustomMethodData {
#[wasm_bindgen(constructor)]
pub fn new(name: String, data: JsValue) -> Result<WasmCustomMethodData> {
let data = data.into_serde::<serde_json::Value>().wasm_result()?;
Ok(Self(CustomMethodData { name, data }))
}
}

impl From<CustomMethodData> for WasmCustomMethodData {
fn from(value: CustomMethodData) -> Self {
Self(value)
}
}

impl From<WasmCustomMethodData> for CustomMethodData {
fn from(value: WasmCustomMethodData) -> Self {
value.0
}
}

impl_wasm_clone!(WasmCustomMethodData, CustomMethodData);
impl_wasm_json!(WasmCustomMethodData, CustomMethodData);
7 changes: 3 additions & 4 deletions bindings/wasm/src/verification/wasm_method_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ impl WasmMethodType {
WasmMethodType(MethodType::JSON_WEB_KEY)
}

/// The `EcdsaSecp256k1RecoverySignature2020` method type.
#[wasm_bindgen(js_name = EcdsaSecp256k1RecoverySignature2020)]
pub fn ecdsa_secp256k1_recovery_signature_2020() -> WasmMethodType {
WasmMethodType(MethodType::ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020)
/// A custom method.
pub fn custom(type_: String) -> WasmMethodType {
WasmMethodType(MethodType::custom(type_))
}

/// Returns the {@link MethodType} as a string.
Expand Down
2 changes: 1 addition & 1 deletion identity_verification/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ identity_core = { version = "=1.1.1", path = "./../identity_core", default-featu
identity_did = { version = "=1.1.1", path = "./../identity_did", default-features = false }
identity_jose = { version = "=1.1.1", path = "./../identity_jose", default-features = false }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true

[dev-dependencies]
serde_json.workspace = true
117 changes: 105 additions & 12 deletions identity_verification/src/verification_method/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ use crate::jose::jwk::Jwk;
use core::fmt::Debug;
use core::fmt::Formatter;
use identity_core::convert::BaseEncoding;
use serde::de::Visitor;
use serde::ser::SerializeMap;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use serde_json::Value;

use crate::error::Error;
use crate::error::Result;
Expand All @@ -21,9 +27,9 @@ pub enum MethodData {
PublicKeyBase58(String),
/// Verification Material in the JSON Web Key format.
PublicKeyJwk(Jwk),
/// Verification Material in CAIP-10 format.
/// [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md)
BlockchainAccountId(String),
/// Arbitrary verification material.
#[serde(untagged)]
Custom(CustomMethodData),
}

impl MethodData {
Expand All @@ -39,9 +45,9 @@ impl MethodData {
Self::PublicKeyMultibase(BaseEncoding::encode_multibase(&data, None))
}

/// Verification Material in CAIP-10 format.
pub fn new_blockchain_account_id(data: String) -> Self {
Self::BlockchainAccountId(data)
/// Creates a new `MethodData` variant from custom data.
pub fn new_custom(data: impl Into<CustomMethodData>) -> Self {
Self::Custom(data.into())
}

/// Returns a `Vec<u8>` containing the decoded bytes of the `MethodData`.
Expand All @@ -53,7 +59,7 @@ impl MethodData {
/// represented as a vector of bytes.
pub fn try_decode(&self) -> Result<Vec<u8>> {
match self {
Self::PublicKeyJwk(_) | Self::BlockchainAccountId(_) => Err(Error::InvalidMethodDataTransformation(
Self::PublicKeyJwk(_) | Self::Custom(_) => Err(Error::InvalidMethodDataTransformation(
"method data is not base encoded",
)),
Self::PublicKeyMultibase(input) => {
Expand All @@ -77,10 +83,10 @@ impl MethodData {
self.public_key_jwk().ok_or(Error::NotPublicKeyJwk)
}

/// Returns the wrapped Blockchain Account Id if the format is [`MethodData::BlockchainAccountId`].
pub fn blockchain_account_id(&self) -> Option<&str> {
if let Self::BlockchainAccountId(id) = self {
Some(id)
/// Returns the custom method data, if any.
pub fn custom(&self) -> Option<&CustomMethodData> {
if let Self::Custom(method_data) = self {
Some(method_data)
} else {
None
}
Expand All @@ -93,7 +99,94 @@ impl Debug for MethodData {
Self::PublicKeyJwk(inner) => f.write_fmt(format_args!("PublicKeyJwk({inner:#?})")),
Self::PublicKeyMultibase(inner) => f.write_fmt(format_args!("PublicKeyMultibase({inner})")),
Self::PublicKeyBase58(inner) => f.write_fmt(format_args!("PublicKeyBase58({inner})")),
Self::BlockchainAccountId(inner) => f.write_fmt(format_args!("BlockchainAccountId({inner})")),
Self::Custom(CustomMethodData { name, data }) => f.write_fmt(format_args!("{name}({data})")),
}
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// Custom verification method.
pub struct CustomMethodData {
/// Verification method's name.
pub name: String,
/// Verification method's data.
pub data: Value,
}

impl Serialize for CustomMethodData {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry(&self.name, &self.data)?;
map.end()
}
}

impl<'de> Deserialize<'de> for CustomMethodData {
fn deserialize<D>(deserializer: D) -> std::prelude::v1::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(CustomMethodDataVisitor)
}
}

struct CustomMethodDataVisitor;

impl<'de> Visitor<'de> for CustomMethodDataVisitor {
type Value = CustomMethodData;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("\"<any property name>\": <any json value>")
}
fn visit_map<A>(self, mut map: A) -> std::prelude::v1::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut custom_method_data = CustomMethodData {
name: String::default(),
data: Value::Null,
};
while let Some((name, data)) = map.next_entry::<String, Value>()? {
custom_method_data = CustomMethodData { name, data };
}

Ok(custom_method_data)
}
}

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

#[test]
fn serialize_custom_method_data() {
let custom = MethodData::Custom(CustomMethodData {
name: "anArbitraryMethod".to_owned(),
data: json!({"a": 1, "b": 2}),
});
let target_str = json!({
"anArbitraryMethod": {"a": 1, "b": 2},
})
.to_string();
assert_eq!(serde_json::to_string(&custom).unwrap(), target_str);
}
#[test]
fn deserialize_custom_method_data() {
let inner_data = json!({
"firstCustomField": "a random string",
"secondCustomField": 420,
});
let json_method_data = json!({
"myCustomVerificationMethod": &inner_data,
});
let custom = serde_json::from_value::<MethodData>(json_method_data.clone()).unwrap();
let target_method_data = MethodData::Custom(CustomMethodData {
name: "myCustomVerificationMethod".to_owned(),
data: inner_data,
});
assert_eq!(custom, target_method_data);
}
}
46 changes: 45 additions & 1 deletion identity_verification/src/verification_method/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::verification_method::MethodBuilder;
use crate::verification_method::MethodData;
use crate::verification_method::MethodRef;
use crate::verification_method::MethodType;
use crate::CustomMethodData;
use identity_did::CoreDID;
use identity_did::DIDUrl;
use identity_did::DID;
Expand All @@ -28,8 +29,8 @@ use identity_did::DID;
///
/// [Specification](https://www.w3.org/TR/did-core/#verification-method-properties)
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(from = "_VerificationMethod")]
pub struct VerificationMethod {
#[serde(deserialize_with = "deserialize_id_with_fragment")]
pub(crate) id: DIDUrl,
pub(crate) controller: CoreDID,
#[serde(rename = "type")]
Expand Down Expand Up @@ -245,3 +246,46 @@ impl KeyComparable for VerificationMethod {
self.id()
}
}

// Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume"
// the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case
// it ends up in the properties object). This workaround simply removes the duplication.
#[derive(Deserialize)]
struct _VerificationMethod {
#[serde(deserialize_with = "deserialize_id_with_fragment")]
pub(crate) id: DIDUrl,
pub(crate) controller: CoreDID,
#[serde(rename = "type")]
pub(crate) type_: MethodType,
#[serde(flatten)]
pub(crate) data: MethodData,
#[serde(flatten)]
pub(crate) properties: Object,
}

impl From<_VerificationMethod> for VerificationMethod {
fn from(value: _VerificationMethod) -> Self {
let _VerificationMethod {
id,
controller,
type_,
data,
mut properties,
} = value;
let key = match &data {
MethodData::PublicKeyBase58(_) => "publicKeyBase58",
MethodData::PublicKeyJwk(_) => "publicKeyJwk",
MethodData::PublicKeyMultibase(_) => "publicKeyMultibase",
MethodData::Custom(CustomMethodData { name, .. }) => name.as_str(),
};
properties.remove(key);

VerificationMethod {
id,
controller,
type_,
data,
properties,
}
}
}
9 changes: 4 additions & 5 deletions identity_verification/src/verification_method/method_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use crate::error::Result;
const ED25519_VERIFICATION_KEY_2018_STR: &str = "Ed25519VerificationKey2018";
const X25519_KEY_AGREEMENT_KEY_2019_STR: &str = "X25519KeyAgreementKey2019";
const JSON_WEB_KEY_METHOD_TYPE: &str = "JsonWebKey";
const ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020_STR: &str = "EcdsaSecp256k1RecoverySignature2020";

/// verification method types.
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
Expand All @@ -26,9 +25,10 @@ impl MethodType {
/// A verification method for use with JWT verification as prescribed by the [`Jwk`](::identity_jose::jwk::Jwk)
/// in the [`publicKeyJwk`](crate::MethodData::PublicKeyJwk) entry.
pub const JSON_WEB_KEY: Self = Self(Cow::Borrowed(JSON_WEB_KEY_METHOD_TYPE));
/// The `EcdsaSecp256k1RecoverySignature2020` method type.
pub const ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020: Self =
Self(Cow::Borrowed(ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020_STR));
/// Construct a custom method type.
pub fn custom(type_: impl AsRef<str>) -> Self {
Self(Cow::Owned(type_.as_ref().to_owned()))
}
}

impl MethodType {
Expand Down Expand Up @@ -58,7 +58,6 @@ impl FromStr for MethodType {
ED25519_VERIFICATION_KEY_2018_STR => Ok(Self::ED25519_VERIFICATION_KEY_2018),
X25519_KEY_AGREEMENT_KEY_2019_STR => Ok(Self::X25519_KEY_AGREEMENT_KEY_2019),
JSON_WEB_KEY_METHOD_TYPE => Ok(Self::JSON_WEB_KEY),
ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020_STR => Ok(Self::ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020),
_ => Ok(Self(Cow::Owned(string.to_owned()))),
}
}
Expand Down
1 change: 1 addition & 0 deletions identity_verification/src/verification_method/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod method_scope;
mod method_type;

pub use self::builder::MethodBuilder;
pub use self::material::CustomMethodData;
pub use self::material::MethodData;
pub use self::method::VerificationMethod;
pub use self::method_ref::MethodRef;
Expand Down

0 comments on commit 0af29fc

Please sign in to comment.