#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use derive_builder::Builder;
use ssi_caips::caip10::BlockchainAccountId;
use std::collections::BTreeMap as Map;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
pub mod did_resolve;
pub mod error;
pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
use crate::did_resolve::{
Content, ContentMetadata, DIDResolver, DereferencingInputMetadata, DereferencingMetadata,
DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID,
ERROR_METHOD_NOT_SUPPORTED, TYPE_DID_LD_JSON,
};
pub use crate::error::Error;
use ssi_core::one_or_many::OneOrMany;
use ssi_jwk::JWK;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(try_from = "String")]
#[serde(rename_all = "camelCase")]
pub enum VerificationRelationship {
AssertionMethod,
Authentication,
KeyAgreement,
ContractAgreement,
CapabilityInvocation,
CapabilityDelegation,
}
impl Default for VerificationRelationship {
fn default() -> Self {
Self::AssertionMethod
}
}
impl FromStr for VerificationRelationship {
type Err = Error;
fn from_str(purpose: &str) -> Result<Self, Self::Err> {
match purpose {
"authentication" => Ok(Self::Authentication),
"assertionMethod" => Ok(Self::AssertionMethod),
"keyAgreement" => Ok(Self::KeyAgreement),
"contractAgreement" => Ok(Self::ContractAgreement),
"capabilityInvocation" => Ok(Self::CapabilityInvocation),
"capabilityDelegation" => Ok(Self::CapabilityDelegation),
_ => Err(Error::UnsupportedVerificationRelationship),
}
}
}
impl TryFrom<String> for VerificationRelationship {
type Error = Error;
fn try_from(purpose: String) -> Result<Self, Self::Error> {
Self::from_str(&purpose)
}
}
impl From<VerificationRelationship> for String {
fn from(purpose: VerificationRelationship) -> String {
match purpose {
VerificationRelationship::Authentication => "authentication".to_string(),
VerificationRelationship::AssertionMethod => "assertionMethod".to_string(),
VerificationRelationship::KeyAgreement => "keyAgreement".to_string(),
VerificationRelationship::ContractAgreement => "contractAgreement".to_string(),
VerificationRelationship::CapabilityInvocation => "capabilityInvocation".to_string(),
VerificationRelationship::CapabilityDelegation => "capabilityDelegation".to_string(),
}
}
}
impl VerificationRelationship {
pub fn to_iri(&self) -> &'static str {
match self {
VerificationRelationship::Authentication => {
"https://w3id.org/security#authenticationMethod"
}
VerificationRelationship::AssertionMethod => {
"https://w3id.org/security#assertionMethod"
}
VerificationRelationship::KeyAgreement => {
"https://w3id.org/security#keyAgreementMethod"
}
VerificationRelationship::ContractAgreement => {
"https://w3id.org/security#contractAgreementMethod"
}
VerificationRelationship::CapabilityInvocation => {
"https://w3id.org/security#capabilityInvocationMethod"
}
VerificationRelationship::CapabilityDelegation => {
"https://w3id.org/security#capabilityDelegationMethod"
}
}
}
}
use async_trait::async_trait;
use chrono::prelude::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/did/v1";
pub const DEFAULT_CONTEXT_NO_WWW: &str = ssi_json_ld::DID_V1_CONTEXT_NO_WWW;
pub const ALT_DEFAULT_CONTEXT: &str = ssi_json_ld::W3ID_DID_V1_CONTEXT;
pub const V0_11_CONTEXT: &str = "https://w3id.org/did/v0.11";
#[allow(clippy::upper_case_acronyms)]
type DID = String;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub struct DIDURL {
pub did: String,
pub path_abempty: String,
pub query: Option<String>,
pub fragment: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum RelativeDIDURLPath {
Absolute(String),
NoScheme(String),
Empty,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub struct RelativeDIDURL {
pub path: RelativeDIDURLPath,
pub query: Option<String>,
pub fragment: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(try_from = "String")]
#[serde(into = "String")]
pub struct PrimaryDIDURL {
pub did: String,
pub path: Option<String>,
pub query: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Builder, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[builder(
setter(into, strip_option),
default,
build_fn(validate = "Self::validate")
)]
pub struct Document {
#[serde(rename = "@context")]
pub context: Contexts,
pub id: DID,
#[serde(skip_serializing_if = "Option::is_none")]
pub also_known_as: Option<Vec<String>>, #[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<OneOrMany<DID>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_method: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assertion_method: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_agreement: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capability_invocation: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capability_delegation: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<Vec<Service>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof: Option<OneOrMany<Proof>>,
#[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum Context {
URI(String),
Object(Map<String, Value>),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(untagged)]
#[serde(try_from = "OneOrMany<Context>")]
pub enum Contexts {
One(Context),
Many(Vec<Context>),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct VerificationMethodMap {
#[serde(rename = "@context")]
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub controller: DID,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_jwk: Option<JWK>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_pgp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_base58: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blockchain_account_id: Option<String>,
#[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum VerificationMethod {
DIDURL(DIDURL),
RelativeDIDURL(RelativeDIDURL),
Map(VerificationMethodMap),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum ServiceEndpoint {
URI(String),
Map(Value),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Service {
pub id: String,
#[serde(rename = "type")]
pub type_: OneOrMany<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub service_endpoint: Option<OneOrMany<ServiceEndpoint>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Proof {
#[serde(rename = "type")]
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
}
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
#[non_exhaustive]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum Resource {
VerificationMethod(VerificationMethodMap),
Object(Map<String, Value>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Source<'a> {
Key(&'a JWK),
KeyAndPattern(&'a JWK, &'a str),
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct DIDParameters {
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "relative-ref")]
pub relative_ref: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub version_id: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub version_time: Option<DateTime<Utc>>, #[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "hl")]
pub hashlink: Option<String>, #[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
}
pub struct DIDCreate {
pub update_key: Option<JWK>,
pub recovery_key: Option<JWK>,
pub verification_key: Option<JWK>,
pub options: Map<String, Value>,
}
pub struct DIDUpdate {
pub did: String,
pub update_key: Option<JWK>,
pub new_update_key: Option<JWK>,
pub operation: DIDDocumentOperation,
pub options: Map<String, Value>,
}
pub struct DIDRecover {
pub did: String,
pub recovery_key: Option<JWK>,
pub new_update_key: Option<JWK>,
pub new_recovery_key: Option<JWK>,
pub new_verification_key: Option<JWK>,
pub options: Map<String, Value>,
}
pub struct DIDDeactivate {
pub did: String,
pub key: Option<JWK>,
pub options: Map<String, Value>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "didDocumentOperation", content = "didDocument")]
#[serde(rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum DIDDocumentOperation {
SetDidDocument(Document),
AddToDidDocument(HashMap<String, Value>),
RemoveFromDidDocument(Vec<String>),
SetVerificationMethod {
vmm: VerificationMethodMap,
purposes: Vec<VerificationRelationship>,
},
SetService(Service),
RemoveVerificationMethod(DIDURL),
RemoveService(DIDURL),
}
#[derive(Debug, Serialize, Deserialize, Builder, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DIDMethodTransaction {
pub did_method: String,
#[serde(flatten)]
pub value: Value,
}
#[derive(Error, Debug)]
pub enum DIDMethodError {
#[error("Not implemented for DID method: {0}")]
NotImplemented(&'static str),
#[error("Option '{option}' not supported for DID operation '{operation}'")]
OptionNotSupported {
operation: &'static str,
option: String,
},
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait DIDMethod: Sync + Send {
fn name(&self) -> &'static str;
fn generate(&self, _source: &Source) -> Option<String> {
None
}
fn did_from_transaction(&self, _tx: DIDMethodTransaction) -> Result<String, DIDMethodError> {
Err(DIDMethodError::NotImplemented("DID from transaction"))
}
async fn submit_transaction(&self, _tx: DIDMethodTransaction) -> Result<Value, DIDMethodError> {
Err(DIDMethodError::NotImplemented("Transaction submission"))
}
fn create(&self, _create: DIDCreate) -> Result<DIDMethodTransaction, DIDMethodError> {
Err(DIDMethodError::NotImplemented("Create operation"))
}
fn update(&self, _update: DIDUpdate) -> Result<DIDMethodTransaction, DIDMethodError> {
Err(DIDMethodError::NotImplemented("Update operation"))
}
fn recover(&self, _recover: DIDRecover) -> Result<DIDMethodTransaction, DIDMethodError> {
Err(DIDMethodError::NotImplemented("Recover operation"))
}
fn deactivate(
&self,
_deactivate: DIDDeactivate,
) -> Result<DIDMethodTransaction, DIDMethodError> {
Err(DIDMethodError::NotImplemented("Deactivate operation"))
}
fn to_resolver(&self) -> &dyn DIDResolver;
}
#[derive(Default)]
pub struct DIDMethods<'a> {
pub methods: HashMap<&'a str, Box<dyn DIDMethod>>,
}
#[allow(clippy::borrowed_box)]
impl<'a> DIDMethods<'a> {
pub fn insert(&mut self, method: Box<dyn DIDMethod>) -> Option<Box<dyn DIDMethod>> {
let name = method.name();
self.methods.insert(name, method)
}
pub fn get(&self, method_name: &str) -> Option<&Box<dyn DIDMethod>> {
self.methods.get(method_name)
}
pub fn to_resolver(&self) -> &dyn DIDResolver {
self
}
pub fn get_method(&self, did: &str) -> Result<&Box<dyn DIDMethod>, &'static str> {
let mut parts = did.split(':');
if parts.next() != Some("did") {
return Err(ERROR_INVALID_DID);
};
let method_name = match parts.next() {
Some(method_name) => method_name,
None => {
return Err(ERROR_INVALID_DID);
}
};
let method = match self.methods.get(method_name) {
Some(method) => method,
None => {
return Err(ERROR_METHOD_NOT_SUPPORTED);
}
};
Ok(method)
}
pub fn generate(&self, source: &Source) -> Option<String> {
let (jwk, pattern) = match source {
Source::Key(_) => {
return None;
}
Source::KeyAndPattern(jwk, pattern) => (jwk, pattern),
};
let mut parts = pattern.splitn(2, ':');
let method_name = parts.next().unwrap();
let method = match self.methods.get(method_name) {
Some(method) => method,
None => return None,
};
if let Some(method_pattern) = parts.next() {
let source = Source::KeyAndPattern(jwk, method_pattern);
method.generate(&source)
} else {
let source = Source::Key(jwk);
method.generate(&source)
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<'a> DIDResolver for DIDMethods<'a> {
async fn resolve(
&self,
did: &str,
input_metadata: &ResolutionInputMetadata,
) -> (
ResolutionMetadata,
Option<Document>,
Option<DocumentMetadata>,
) {
let method = match self.get_method(did) {
Ok(method) => method,
Err(err) => return (ResolutionMetadata::from_error(err), None, None),
};
method.to_resolver().resolve(did, input_metadata).await
}
async fn resolve_representation(
&self,
did: &str,
input_metadata: &ResolutionInputMetadata,
) -> (ResolutionMetadata, Vec<u8>, Option<DocumentMetadata>) {
let method = match self.get_method(did) {
Ok(method) => method,
Err(err) => return (ResolutionMetadata::from_error(err), Vec::new(), None),
};
method
.to_resolver()
.resolve_representation(did, input_metadata)
.await
}
async fn dereference(
&self,
did_url: &PrimaryDIDURL,
input_metadata: &DereferencingInputMetadata,
) -> Option<(DereferencingMetadata, Content, ContentMetadata)> {
let method = match self.get_method(&did_url.did) {
Ok(method) => method,
Err(err) => {
return Some((
DereferencingMetadata::from_error(err),
Content::Null,
ContentMetadata::default(),
))
}
};
method
.to_resolver()
.dereference(did_url, input_metadata)
.await
}
}
impl DIDURL {
pub fn to_relative(&self, base_did: &str) -> Option<RelativeDIDURL> {
if self.did != base_did {
return None;
}
Some(RelativeDIDURL {
path: match RelativeDIDURLPath::from_str(&self.path_abempty) {
Ok(path) => path,
Err(_) => return None,
},
query: self.query.as_ref().cloned(),
fragment: self.fragment.as_ref().cloned(),
})
}
pub fn remove_fragment(self) -> (PrimaryDIDURL, Option<String>) {
(
PrimaryDIDURL {
did: self.did,
path: if !self.path_abempty.is_empty() {
Some(self.path_abempty)
} else {
None
},
query: self.query,
},
self.fragment,
)
}
}
impl RelativeDIDURL {
pub fn to_absolute(&self, base_did: &str) -> DIDURL {
DIDURL {
did: base_did.to_string(),
path_abempty: self.path.to_string(),
query: self.query.as_ref().cloned(),
fragment: self.fragment.as_ref().cloned(),
}
}
}
impl PrimaryDIDURL {
pub fn with_fragment(self, fragment: String) -> DIDURL {
DIDURL {
fragment: Some(fragment),
..DIDURL::from(self)
}
}
}
impl VerificationMethod {
pub fn get_id(&self, did: &str) -> String {
match self {
Self::DIDURL(didurl) => didurl.to_string(),
Self::RelativeDIDURL(relative_did_url) => relative_did_url.to_absolute(did).to_string(),
Self::Map(map) => map.get_id(did),
}
}
}
impl VerificationMethodMap {
pub fn get_id(&self, did: &str) -> String {
if let Ok(rel_did_url) = RelativeDIDURL::from_str(&self.id) {
rel_did_url.to_absolute(did).to_string()
} else {
self.id.to_string()
}
}
pub fn get_jwk(&self) -> Result<JWK, Error> {
let pk_hex_value = self
.property_set
.as_ref()
.and_then(|cc| cc.get("publicKeyHex"));
let pk_multibase_opt = match self.property_set {
Some(ref props) => match props.get("publicKeyMultibase") {
Some(Value::String(string)) => Some(string.clone()),
Some(Value::Null) => None,
Some(_) => return Err(Error::ExpectedStringPublicKeyMultibase),
None => None,
},
None => None,
};
let pk_bytes = match (
self.public_key_jwk.as_ref(),
self.public_key_base58.as_ref(),
pk_hex_value,
pk_multibase_opt,
) {
(Some(pk_jwk), None, None, None) => return Ok(pk_jwk.clone()),
(None, Some(pk_bs58), None, None) => bs58::decode(&pk_bs58).into_vec()?,
(None, None, Some(pk_hex), None) => {
let pk_hex = match pk_hex {
Value::String(string) => string,
_ => return Err(Error::HexString),
};
let pk_hex = pk_hex.strip_prefix("0x").unwrap_or(pk_hex);
hex::decode(pk_hex)?
}
(None, None, None, Some(pk_mb)) => multibase::decode(pk_mb)?.1,
(None, None, None, None) => return Err(Error::MissingKey),
_ => {
return Err(Error::MultipleKeyMaterial);
}
};
Ok(ssi_jwk::JWK::from_vm_type(&self.type_, pk_bytes)?)
}
pub fn match_jwk(&self, jwk: &JWK) -> Result<(), Error> {
if let Some(ref account_id) = self.blockchain_account_id {
let account_id = BlockchainAccountId::from_str(account_id)?;
account_id.verify(jwk)?;
} else {
let resolved_jwk = self.get_jwk()?;
if !resolved_jwk.equals_public(jwk) {
return Err(Error::KeyMismatch);
}
}
Ok(())
}
}
impl FromStr for DIDURL {
type Err = Error;
fn from_str(didurl: &str) -> Result<Self, Self::Err> {
let mut parts = didurl.splitn(2, '#');
let before_fragment = parts.next().unwrap().to_string();
if before_fragment.is_empty() {
return Err(Error::DIDURL);
}
let fragment = parts.next().map(|x| x.to_owned());
let primary_did_url = PrimaryDIDURL::try_from(before_fragment)?;
Ok(Self {
fragment,
..DIDURL::from(primary_did_url)
})
}
}
impl FromStr for PrimaryDIDURL {
type Err = Error;
fn from_str(didurl: &str) -> Result<Self, Self::Err> {
#[cfg(test)]
if !didurl.starts_with("did:") {
return Err(Error::DIDURL);
}
if didurl.contains('#') {
return Err(Error::UnexpectedDIDFragment);
}
let mut parts = didurl.splitn(2, '?');
let before_query = parts.next().unwrap();
let query = parts.next().map(|x| x.to_owned());
let (did, path) = match before_query.find('/') {
Some(i) => {
let (did, path) = before_query.split_at(i);
(did.to_string(), Some(path.to_string()))
}
None => (before_query.to_string(), None),
};
Ok(Self { did, path, query })
}
}
impl FromStr for RelativeDIDURL {
type Err = Error;
fn from_str(didurl: &str) -> Result<Self, Self::Err> {
let mut parts = didurl.splitn(2, '#');
let before_fragment = parts.next().unwrap().to_string();
let fragment = parts.next().map(|x| x.to_owned());
let mut parts = before_fragment.splitn(2, '?');
let before_query = parts.next().unwrap().to_string();
let query = parts.next().map(|x| x.to_owned());
let path = RelativeDIDURLPath::from_str(&before_query)?;
Ok(Self {
path,
query,
fragment,
})
}
}
impl FromStr for RelativeDIDURLPath {
type Err = Error;
fn from_str(path: &str) -> Result<Self, Self::Err> {
if path.is_empty() {
return Ok(Self::Empty);
}
if path.starts_with('/') {
if path.len() >= 2 && path.chars().nth(1) == Some('/') {
return Err(Error::DIDURL);
}
Ok(Self::Absolute(path.to_string()))
} else {
let first_segment = path.split_once('/').map_or(path, |x| x.0).to_string();
if first_segment.contains(':') {
return Err(Error::DIDURL);
}
Ok(Self::NoScheme(path.to_string()))
}
}
}
impl fmt::Display for DIDURL {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", self.did, self.path_abempty)?;
if let Some(ref query) = self.query {
write!(f, "?{}", query)?;
}
if let Some(ref fragment) = self.fragment {
write!(f, "#{}", fragment)?;
}
Ok(())
}
}
impl fmt::Display for RelativeDIDURL {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.path.fmt(f)?;
if let Some(ref query) = self.query {
write!(f, "?{}", query)?;
}
if let Some(ref fragment) = self.fragment {
write!(f, "#{}", fragment)?;
}
Ok(())
}
}
impl fmt::Display for RelativeDIDURLPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Empty => Ok(()),
Self::Absolute(string) => string.fmt(f),
Self::NoScheme(string) => string.fmt(f),
}
}
}
impl fmt::Display for PrimaryDIDURL {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.did)?;
if let Some(ref path) = self.path {
write!(f, "{}", path)?;
}
if let Some(ref query) = self.query {
write!(f, "?{}", query)?;
}
Ok(())
}
}
impl TryFrom<String> for DIDURL {
type Error = Error;
fn try_from(didurl: String) -> Result<Self, Self::Error> {
DIDURL::from_str(&didurl)
}
}
impl From<DIDURL> for String {
fn from(didurl: DIDURL) -> String {
format!("{}", didurl)
}
}
impl From<PrimaryDIDURL> for DIDURL {
fn from(primary: PrimaryDIDURL) -> DIDURL {
DIDURL {
did: primary.did,
path_abempty: primary.path.unwrap_or_default(),
query: primary.query,
fragment: None,
}
}
}
impl From<PrimaryDIDURL> for String {
fn from(didurl: PrimaryDIDURL) -> String {
format!("{}", didurl)
}
}
impl TryFrom<String> for PrimaryDIDURL {
type Error = Error;
fn try_from(didurl: String) -> Result<Self, Self::Error> {
PrimaryDIDURL::from_str(&didurl)
}
}
impl TryFrom<String> for RelativeDIDURL {
type Error = Error;
fn try_from(relative_did_url: String) -> Result<Self, Self::Error> {
RelativeDIDURL::from_str(&relative_did_url)
}
}
impl From<RelativeDIDURL> for String {
fn from(relative_did_url: RelativeDIDURL) -> String {
relative_did_url.to_string()
}
}
impl Default for Document {
fn default() -> Self {
Document::new("")
}
}
impl Default for RelativeDIDURLPath {
fn default() -> Self {
Self::Empty
}
}
impl TryFrom<OneOrMany<Context>> for Contexts {
type Error = Error;
fn try_from(context: OneOrMany<Context>) -> Result<Self, Self::Error> {
let first_uri = match context.first() {
None => return Err(Error::MissingContext),
Some(Context::URI(uri)) => uri,
Some(Context::Object(_)) => return Err(Error::InvalidContext),
};
if first_uri != DEFAULT_CONTEXT
&& first_uri != V0_11_CONTEXT
&& first_uri != ALT_DEFAULT_CONTEXT
&& first_uri != DEFAULT_CONTEXT_NO_WWW
{
return Err(Error::InvalidContext);
}
Ok(match context {
OneOrMany::One(context) => Contexts::One(context),
OneOrMany::Many(contexts) => Contexts::Many(contexts),
})
}
}
impl From<Contexts> for OneOrMany<Context> {
fn from(contexts: Contexts) -> OneOrMany<Context> {
match contexts {
Contexts::One(context) => OneOrMany::One(context),
Contexts::Many(contexts) => OneOrMany::Many(contexts),
}
}
}
impl DocumentBuilder {
fn validate(&self) -> Result<(), Error> {
if self.id.is_none() || self.id == Some("".to_string()) {
return Err(Error::MissingDocumentId);
}
if let Some(ref context) = self.context {
let first_context = match context {
Contexts::One(context) => context,
Contexts::Many(contexts) => {
if contexts.is_empty() {
return Err(Error::MissingContext);
} else {
&contexts[0]
}
}
};
let first_uri = match first_context {
Context::URI(uri) => uri,
Context::Object(_) => return Err(Error::InvalidContext),
};
if first_uri != DEFAULT_CONTEXT
&& first_uri != V0_11_CONTEXT
&& first_uri != ALT_DEFAULT_CONTEXT
&& first_uri != DEFAULT_CONTEXT_NO_WWW
{
return Err(Error::InvalidContext);
}
}
Ok(())
}
}
pub(crate) fn merge_context(dest_opt: &mut Option<Value>, source: &Contexts) {
let source = OneOrMany::<Context>::from(source.clone());
let dest = dest_opt.take().unwrap_or(Value::Null);
let mut dest_array = match dest {
Value::Array(array) => array,
Value::Object(object) => vec![Value::Object(object)],
_ => Vec::new(),
};
for context in source {
let value = match context {
Context::URI(uri) => Value::String(uri),
Context::Object(hash_map) => {
let serde_map = hash_map
.into_iter()
.collect::<serde_json::Map<String, Value>>();
Value::Object(serde_map)
}
};
dest_array.push(value);
}
if !dest_array.is_empty() {
let dest = if dest_array.len() == 1 {
dest_array.remove(0)
} else {
Value::Array(dest_array)
};
dest_opt.replace(dest);
}
}
impl Document {
pub fn new(id: &str) -> Document {
Document {
context: Contexts::One(Context::URI(DEFAULT_CONTEXT.to_string())),
id: String::from(id),
also_known_as: None,
controller: None,
verification_method: None,
authentication: None,
assertion_method: None,
key_agreement: None,
capability_invocation: None,
capability_delegation: None,
service: None,
proof: None,
property_set: None,
public_key: None,
}
}
pub fn from_json(json: &str) -> Result<Document, serde_json::Error> {
serde_json::from_str(json)
}
pub fn from_json_bytes(json: &[u8]) -> Result<Document, serde_json::Error> {
serde_json::from_slice(json)
}
pub fn select_object(&self, id: &DIDURL) -> Result<Resource, Error> {
let id_string = String::from(id.clone());
let id_relative_string_opt = id.to_relative(&self.id).map(|rel_url| rel_url.to_string());
for vm in vec![
&self.verification_method,
&self.authentication,
&self.assertion_method,
&self.key_agreement,
&self.capability_invocation,
&self.capability_delegation,
&self.public_key,
]
.iter()
.flat_map(|array| array.iter().flatten())
{
if let VerificationMethod::Map(map) = vm {
if map.id == id_string || Some(&map.id) == id_relative_string_opt.as_ref() {
let mut map = map.clone();
merge_context(&mut map.context, &self.context);
return Ok(Resource::VerificationMethod(map));
}
}
}
Err(Error::ResourceNotFound(id.to_string()))
}
pub fn select_service(&self, fragment: &str) -> Option<&Service> {
for service in self.service.iter().flatten() {
if let [service_fragment, _] =
service.id.rsplitn(2, '#').collect::<Vec<&str>>().as_slice()
{
if service_fragment == &fragment {
return Some(service);
}
}
}
None
}
pub fn get_verification_method_ids(
&self,
verification_relationship: VerificationRelationship,
) -> Result<Vec<String>, String> {
let did = &self.id;
let vms = match verification_relationship {
VerificationRelationship::AssertionMethod => &self.assertion_method,
VerificationRelationship::Authentication => &self.authentication,
VerificationRelationship::KeyAgreement => &self.key_agreement,
VerificationRelationship::CapabilityInvocation => &self.capability_invocation,
VerificationRelationship::CapabilityDelegation => &self.capability_delegation,
rel => return Err(format!("Unsupported verification relationship: {:?}", rel)),
};
let vm_ids = vms.iter().flatten().map(|vm| vm.get_id(did)).collect();
Ok(vm_ids)
}
pub fn to_representation(&self, content_type: &str) -> Result<Vec<u8>, Error> {
match content_type {
TYPE_DID_LD_JSON => Ok(serde_json::to_vec(self)?),
_ => Err(Error::RepresentationNotSupported),
}
}
}
#[cfg(feature = "example")]
pub mod example {
use crate::did_resolve::{
DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata,
ERROR_NOT_FOUND, TYPE_DID_LD_JSON,
};
use crate::{DIDMethod, Document};
use async_trait::async_trait;
const DOC_JSON_FOO: &str = include_str!("../tests/did-example-foo.json");
const DOC_JSON_BAR: &str = include_str!("../tests/did-example-bar.json");
const DOC_JSON_12345: &str = include_str!("../tests/did-example-12345.json");
const DOC_JSON_AABB: &str = include_str!("../tests/lds-eip712-issuer.json");
const DOC_JSON_TEST_ISSUER: &str = include_str!("../tests/did-example-test-issuer.json");
const DOC_JSON_TEST_HOLDER: &str = include_str!("../tests/did-example-test-holder.json");
pub struct DIDExample;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl DIDMethod for DIDExample {
fn name(&self) -> &'static str {
"example"
}
fn to_resolver(&self) -> &dyn DIDResolver {
self
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl DIDResolver for DIDExample {
async fn resolve(
&self,
did: &str,
_input_metadata: &ResolutionInputMetadata,
) -> (
ResolutionMetadata,
Option<Document>,
Option<DocumentMetadata>,
) {
let doc_str = match did {
"did:example:foo" => DOC_JSON_FOO,
"did:example:bar" => DOC_JSON_BAR,
"did:example:0xab" => DOC_JSON_TEST_ISSUER,
"did:example:12345" => DOC_JSON_12345,
"did:example:ebfeb1f712ebc6f1c276e12ec21" => DOC_JSON_TEST_HOLDER,
"did:example:aaaabbbb" => DOC_JSON_AABB,
_ => return (ResolutionMetadata::from_error(ERROR_NOT_FOUND), None, None),
};
let doc: Document = match serde_json::from_str(doc_str) {
Ok(doc) => doc,
Err(err) => {
return (ResolutionMetadata::from_error(&err.to_string()), None, None);
}
};
(
ResolutionMetadata {
error: None,
content_type: Some(TYPE_DID_LD_JSON.to_string()),
property_set: None,
},
Some(doc),
Some(DocumentMetadata::default()),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_did_url() {
let didurl_str = "did:foo:21tDAKCERh95uGgKbJNHYp?service=agent";
let didurl = DIDURL::try_from(didurl_str.to_string()).unwrap();
assert_eq!(
didurl,
DIDURL {
did: "did:foo:21tDAKCERh95uGgKbJNHYp".to_string(),
path_abempty: "".to_string(),
query: Some("service=agent".to_string()),
fragment: None,
}
);
}
#[test]
fn did_url_relative_to_absolute() {
let relative_did_url_str = "#key-1";
let did_url_ref = RelativeDIDURL::from_str(relative_did_url_str).unwrap();
let did = "did:example:123456789abcdefghi";
let did_url = did_url_ref.to_absolute(did);
assert_eq!(did_url.to_string(), "did:example:123456789abcdefghi#key-1");
}
#[test]
fn new_document() {
let id = "did:test:deadbeefcafe";
let doc = Document::new(id);
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
assert_eq!(doc.id, id);
}
#[test]
fn build_document() {
let id = "did:test:deadbeefcafe";
let doc = DocumentBuilder::default()
.id(id.to_owned())
.build()
.unwrap();
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
assert_eq!(doc.id, id);
}
#[test]
#[should_panic(expected = "Missing document ID")]
fn build_document_no_id() {
let doc = DocumentBuilder::default().build().unwrap();
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
}
#[test]
#[should_panic(expected = "Invalid context")]
fn build_document_invalid_context() {
let id = "did:test:deadbeefcafe";
let doc = DocumentBuilder::default()
.context(Contexts::One(Context::URI("example:bad".to_string())))
.id(id)
.build()
.unwrap();
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
}
#[test]
fn document_from_json() {
let doc_str = "{\
\"@context\": \"https://www.w3.org/ns/did/v1\",\
\"id\": \"did:test:deadbeefcafe\"\
}";
let id = "did:test:deadbeefcafe";
let doc = Document::from_json(doc_str).unwrap();
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
assert_eq!(doc.id, id);
}
#[test]
fn verification_method() {
let id = "did:test:deadbeefcafe";
let mut doc = Document::new(id);
doc.verification_method = Some(vec![VerificationMethod::DIDURL(
DIDURL::try_from("did:pubkey:okay".to_string()).unwrap(),
)]);
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
let pko = VerificationMethodMap {
id: String::from("did:example:123456789abcdefghi#keys-1"),
type_: String::from("Ed25519VerificationKey2018"),
controller: String::from("did:example:123456789abcdefghi"),
..Default::default()
};
doc.verification_method = Some(vec![
VerificationMethod::DIDURL(DIDURL::try_from("did:pubkey:okay".to_string()).unwrap()),
VerificationMethod::Map(pko),
]);
println!("{}", serde_json::to_string_pretty(&doc).unwrap());
assert_eq!(doc.id, id);
}
#[test]
fn vmm_to_jwk() {
const JWK: &str = include_str!("../../tests/ed25519-2020-10-18.json");
let jwk: JWK = serde_json::from_str(JWK).unwrap();
let pk_jwk = jwk.to_public();
let vmm_ed = VerificationMethodMap {
id: String::from("did:example:foo#key2"),
type_: String::from("Ed25519VerificationKey2018"),
controller: String::from("did:example:foo"),
public_key_jwk: Some(pk_jwk.clone()),
..Default::default()
};
let jwk = vmm_ed.get_jwk().unwrap();
assert_eq!(jwk, pk_jwk);
}
#[test]
fn vmm_bs58_to_jwk() {
const JWK: &str = include_str!("../../tests/ed25519-2020-10-18.json");
let jwk: JWK = serde_json::from_str(JWK).unwrap();
let pk_jwk = jwk.to_public();
let vmm_ed = VerificationMethodMap {
id: String::from("did:example:foo#key3"),
type_: String::from("Ed25519VerificationKey2018"),
controller: String::from("did:example:foo"),
public_key_base58: Some("2sXRz2VfrpySNEL6xmXJWQg6iY94qwNp1qrJJFBuPWmH".to_string()),
..Default::default()
};
let jwk = vmm_ed.get_jwk().unwrap();
assert_eq!(jwk, pk_jwk);
}
#[test]
fn vmm_hex_to_jwk() {
const JWK: &str = include_str!("../../tests/secp256k1-2021-02-17.json");
let jwk: JWK = serde_json::from_str(JWK).unwrap();
let pk_jwk = jwk.to_public();
let vmm_ed = VerificationMethodMap {
id: String::from("did:example:deprecated#lds-ecdsa-secp256k1-2019-pkhex"),
type_: String::from("EcdsaSecp256k1VerificationKey2019"),
controller: String::from("did:example:deprecated"),
public_key_jwk: Some(pk_jwk.clone()),
..Default::default()
};
let jwk = vmm_ed.get_jwk().unwrap();
assert_eq!(jwk, pk_jwk);
}
}