Skip to content
This repository has been archived by the owner on Apr 29, 2024. It is now read-only.

Implementing structured statement string for allowedActions #17

Merged
merged 5 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 174 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! [CACAO-ZCAP](https://demo.didkit.dev/2022/cacao-zcap/) implementation

use async_trait::async_trait;
use cacaos::siwe_cacao::SignInWithEthereum;
use cacaos::{Header, Payload, SignatureScheme, Version as CacaoVersion, CACAO};
Expand All @@ -15,6 +17,7 @@ use ssi::error::Error as SSIError;
use ssi::jsonld::SECURITY_V2_CONTEXT;
use ssi::jwk::JWK;
use ssi::ldp::{LinkedDataDocument, ProofPreparation, ProofSuite, VerificationWarnings};
use ssi::one_or_many::OneOrMany;
use ssi::vc::{LinkedDataProofOptions, Proof, ProofPurpose, URI};
use ssi::zcap::{Context, Contexts, Delegation};
use std::collections::HashMap;
Expand All @@ -23,9 +26,21 @@ use std::str::FromStr;
use thiserror::Error;
use uuid::adapter::Urn;

/// [Type](https://www.w3.org/TR/json-ld11/#specifying-the-type) term
/// for [CacaoZcap2022](https://demo.didkit.dev/2022/cacao-zcap/#CacaoZcap2022)
pub const DELEGATION_TYPE_2022: &str = "CacaoZcap2022";

/// [Type](https://www.w3.org/TR/json-ld11/#specifying-the-type) term
/// for [CacaoZcapProof2022]
pub const PROOF_TYPE_2022: &str = "CacaoZcapProof2022";

/// JSON-LD [Context](https://www.w3.org/TR/json-ld11/#the-context) URL
/// for [CACAO-ZCAP suite](https://demo.didkit.dev/2022/cacao-zcap/),
/// version 1
pub const CONTEXT_URL_V1: &str = "https://demo.didkit.dev/2022/cacao-zcap/context/v1.json";

/// [Extra properties](Delegation::property_set) for a zCap delegation
/// object
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -61,16 +76,27 @@ pub struct CacaoZcapExtraProps {
/// CACAO header "t" value
pub cacao_payload_type: String,

/// CACAO statement
/// zCap allowed actions
///
/// <https://w3id.org/security#allowedAction>
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_action: Option<OneOrMany<String>>,

/// CACAO-ZCAP substatement
///
/// [CACAO] payload "statement" value
/// Part of a [CACAO] payload "statement" value
///
/// In [EIP-4361], statement is defined as a "human-readable ASCII assertion that the user will sign".
///
/// CACAO-ZCAP requires the CACAO statement to match a format containing an optional a list of
/// [allowed actions](CacaoZcapExtraProps::allowed_action) and an optional
/// [substatement string](CacaoZcapExtraProps::cacao_zcap_substatement).
///
/// [CACAO-ZCAP]: https://demo.didkit.dev/2022/cacao-zcap/
/// [CACAO]: https://github.com/ChainAgnostic/CAIPs/blob/8fdb5bfd1bdf15c9daf8aacfbcc423533764dfe9/CAIPs/caip-draft_cacao.md#container-format
/// [EIP-4361]: https://eips.ethereum.org/EIPS/eip-4361#message-field-descriptions
#[serde(skip_serializing_if = "Option::is_none")]
pub cacao_statement: Option<String>,
pub cacao_zcap_substatement: Option<String>,

/// CACAO request ID.
///
Expand All @@ -80,7 +106,7 @@ pub struct CacaoZcapExtraProps {
pub cacao_request_id: Option<String>,
}

/// An item for [CacaoZcapProofExtraProps::capability_chain]
/// An item in a [proof capabilityChain array](CacaoZcapProofExtraProps::capability_chain)
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
Expand All @@ -89,7 +115,7 @@ pub enum CapabilityChainItem {
Object(Delegation<(), CacaoZcapExtraProps>),
}

/// Error from converting to [Zcap to a CACAO](zcap_to_cacao)
/// Error [converting ZCAP to CACAO](zcap_to_cacao)
#[derive(Error, Debug)]
pub enum CapToResourceError {
/// Unable to serialize delegation
Expand All @@ -101,6 +127,7 @@ pub enum CapToResourceError {
UriParse(#[source] iri_string::validate::Error),
}

/// Error [converting a CACAO resource URI to a delegation object](CapabilityChainItem::from_resource_uri)
#[derive(Error, Debug)]
pub enum CapFromResourceError {
/// Expected JSON base64 data URI
Expand Down Expand Up @@ -137,6 +164,7 @@ impl CapabilityChainItem {
}
}

/// Convert a [CACAO resource](Payload::resources) URI to a [delegation](Delegation) object
pub fn from_resource_uri(uri: &UriString) -> Result<Self, CapFromResourceError> {
let uri_string = uri.to_string();
let b64_json = uri_string
Expand All @@ -149,6 +177,7 @@ impl CapabilityChainItem {
}
}

/// [Extra properties](Proof::property_set) for a proof object
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
Expand All @@ -164,7 +193,7 @@ pub struct CacaoZcapProofExtraProps {
pub cacao_signature_type: String,
}

/// Error from converting between [CacaoZcapProofConvertError] and [Proof::property_set] value
/// Error [converting Proof to CacaoZcapProofExtraProps](CacaoZcapProofExtraProps::from_property_set_opt)
#[derive(Error, Debug)]
pub enum CacaoZcapProofConvertError {
/// Unable to convert HashMap to Value
Expand All @@ -185,7 +214,7 @@ pub enum CacaoZcapProofConvertError {
}

impl CacaoZcapProofExtraProps {
fn from_property_set_opt(
pub fn from_property_set_opt(
pso: Option<HashMap<String, Value>>,
) -> Result<Self, CacaoZcapProofConvertError> {
let value =
Expand All @@ -206,7 +235,113 @@ impl CacaoZcapProofExtraProps {
}
}

/// Error from converting to [CACAO to a Zcap](cacao_to_zcap)
/// A [CACAO statement](Payload::statement) for CACAO-ZCAP
#[derive(Clone, Debug)]
pub struct CacaoZcapStatement {
/// zCap [allowedAction](CacaoZcapExtraProps::allowed_action) values
pub actions: Option<OneOrMany<String>>,

/// CACAO-ZCAP [substatement](CacaoZcapExtraProps::cacao_zcap_substatement)
pub substatement: Option<String>,
}

impl CacaoZcapStatement {
/// Construct cacao-zcap statement
pub fn from_actions_and_substatement_opt(
substmt: Option<&str>,
actions: Option<&OneOrMany<String>>,
) -> Self {
Self {
actions: actions.cloned(),
substatement: substmt.map(|s| s.to_string()),
}
}

/// Serialize to a CACAO statement string, or None if there is no actions or substatement
pub fn to_string_opt(&self) -> Option<String> {
if self.actions.is_some() && self.substatement.is_some() {
Some(format!("{}", self))
} else {
None
}
}
}

impl Display for CacaoZcapStatement {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "Authorize action")?;
if let Some(actions) = self.actions.as_ref() {
write!(f, " (")?;
let mut actions_iter = actions.into_iter();
if let Some(action) = actions_iter.next() {
write!(f, "{}", action)?;
}
for action in actions_iter {
write!(f, ", {}", action)?;
}
write!(f, ")")?;
}
if let Some(substatement) = self.substatement.as_ref() {
write!(f, ": {}", substatement)?;
}
Ok(())
}
}

/// Error [parsing CacaoZcapStatement](CacaoZcapStatement::from_str)
#[derive(Error, Debug)]
pub enum CacaoZcapStatementParseError {
/// Unexpected statement prefix
#[error("Unexpected statement prefix")]
UnexpectedPrefix,

/// Expected separator
#[error("Expected separator before substatement")]
ExpectedSeparatorBeforeSubstatement,

/// Expected separator after actions
#[error("Expected separator after actions")]
ExpectedSeparatorAfterActions,
}

impl FromStr for CacaoZcapStatement {
type Err = CacaoZcapStatementParseError;
fn from_str(stmt: &str) -> Result<Self, Self::Err> {
let mut s = stmt
.strip_prefix("Authorize action")
.ok_or(CacaoZcapStatementParseError::UnexpectedPrefix)?;

let actions = if let Some(after_paren) = s.strip_prefix(" (") {
let (actions_to_split, after_actions) = after_paren
.split_once(')')
.ok_or(CacaoZcapStatementParseError::ExpectedSeparatorAfterActions)?;
s = after_actions;
Some(OneOrMany::Many(
actions_to_split
.split(", ")
.map(String::from)
.collect::<Vec<String>>(),
))
} else {
None
};
let substatement = if s.is_empty() {
None
} else {
Some(
s.strip_prefix(": ")
.ok_or(CacaoZcapStatementParseError::ExpectedSeparatorBeforeSubstatement)?
.to_string(),
)
};
Ok(Self {
actions,
substatement,
})
}
}

/// Error [converting CACAO to a Zcap](cacao_to_zcap)
#[derive(Error, Debug)]
pub enum CacaoToZcapError {
/// Unknown CACAO version. Expected v1.
Expand Down Expand Up @@ -256,6 +391,10 @@ pub enum CacaoToZcapError {
/// Unable to parse root capability id as URI
#[error("Unable to parse root capability id as URI")]
RootCapUriParse(#[source] iri_string::validate::Error),

/// Unable to parse CACAO-ZCAP statement string
#[error("Unable to parse CACAO-ZCAP statement string")]
StatementParse(#[source] CacaoZcapStatementParseError),
}

fn get_header_and_signature_type(header: &Header) -> Result<(String, String), CacaoToZcapError> {
Expand Down Expand Up @@ -323,6 +462,15 @@ where
_ => return Err(CacaoToZcapError::UnknownCacaoVersion),
}
let signature = cacao.signature();

let (substatement_opt, allowed_action_opt) = if let Some(statement) = statement_opt {
let cacao_zcap_stmt =
CacaoZcapStatement::from_str(statement).map_err(CacaoToZcapError::StatementParse)?;
(cacao_zcap_stmt.substatement, cacao_zcap_stmt.actions)
} else {
(None, None)
};

let valid_from_opt = nbf_opt.as_ref().map(|nbf| nbf.to_string());
let exp_string_opt = exp_opt.as_ref().map(|ts| ts.to_string());

Expand Down Expand Up @@ -390,12 +538,13 @@ where
..Proof::new(PROOF_TYPE_2022)
};
let delegation_extraprops = CacaoZcapExtraProps {
r#type: String::from("CacaoZcap2022"),
r#type: String::from(DELEGATION_TYPE_2022),
expires: exp_string_opt,
valid_from: valid_from_opt,
invocation_target: invocation_target.to_string(),
cacao_payload_type: header_type,
cacao_statement: statement_opt.clone(),
allowed_action: allowed_action_opt,
cacao_zcap_substatement: substatement_opt,
cacao_request_id: request_id_opt.clone(),
};
let mut delegation = Delegation {
Expand All @@ -410,7 +559,7 @@ where
Ok(delegation)
}

/// Error from converting to [Zcap to a CACAO](zcap_to_cacao)
/// Error [converting ZCAP to CACAO](zcap_to_cacao)
#[derive(Error, Debug)]
pub enum ZcapToCacaoError {
/// Delegation object is missing a proof object
Expand Down Expand Up @@ -569,7 +718,7 @@ pub struct ZcapRootURN {
pub target: UriString,
}

/// Error from attempting to parse a [ZcapRootURN]
/// Error [parsing ZcapRootURN](ZcapRootURN::from_str)
#[derive(Error, Debug)]
pub enum ZcapRootURNParseError {
/// Unable to parse [root URI](ZcapRootURN)
Expand Down Expand Up @@ -643,9 +792,16 @@ where
expires: expires_opt,
valid_from: valid_from_opt,
cacao_payload_type,
cacao_statement: cacao_statement_opt,
cacao_zcap_substatement: cacao_zcap_substatement_opt,
allowed_action: allowed_action_opt,
cacao_request_id,
} = zcap_extraprops;

let stmt = CacaoZcapStatement::from_actions_and_substatement_opt(
cacao_zcap_substatement_opt.as_ref().map(|s| s.as_str()),
allowed_action_opt.as_ref(),
);

let proof = zcap.proof.as_ref().ok_or(ZcapToCacaoError::MissingProof)?;
let proof_extraprops =
CacaoZcapProofExtraProps::from_property_set_opt(proof.property_set.clone())
Expand All @@ -664,10 +820,10 @@ where
domain: domain_opt,
..
} = proof;
if zcap_type != "CacaoZcap2022" {
if zcap_type != DELEGATION_TYPE_2022 {
return Err(ZcapToCacaoError::UnknownDelegationType);
}
if proof_type != "CacaoZcapProof2022" {
if proof_type != PROOF_TYPE_2022 {
return Err(ZcapToCacaoError::UnknownDelegationProofType);
}
let combined_type = format!("{}-{}", cacao_payload_type, cacao_signature_type);
Expand Down Expand Up @@ -785,7 +941,7 @@ where
let payload = Payload {
domain: domain.to_string().try_into().unwrap(),
iss: issuer.try_into().map_err(ZcapToCacaoError::IssuerParse)?,
statement: cacao_statement_opt.clone(),
statement: stmt.to_string_opt(),
aud: invoker
.as_str()
.try_into()
Expand All @@ -810,6 +966,8 @@ where
Ok(cacao)
}

/// [CacaoZcapProof2022](https://demo.didkit.dev/2022/cacao-zcap/#CacaoZcapProof2022) proof suite
/// implementation
pub struct CacaoZcapProof2022;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
Expand Down
8 changes: 6 additions & 2 deletions tests/delegation0-zcap.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
"https://w3id.org/security/v2",
"https://demo.didkit.dev/2022/cacao-zcap/context/v1.json"
],
"allowedAction": [
"read",
"write"
],
"cacaoPayloadType": "eip4361",
"cacaoRequestId": "https://example.org/delegations/981871674",
"cacaoStatement": "Allow access to your Kepler orbit",
"cacaoZcapSubstatement": "Allow access to your Kepler orbit",
"expires": "2022-03-14T13:32:42.763Z",
"id": "urn:uuid:f6076745-00e9-475b-bd97-53245c9be61a",
"id": "urn:uuid:0807b7fa-0ca6-445c-99f6-f9c6d015b675",
"invocationTarget": "kepler://my_orbit",
"invoker": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
"parentCapability": "urn:zcap:root:kepler%3A%2F%2Fmy_orbit",
Expand Down
2 changes: 1 addition & 1 deletion tests/delegation0.siwe
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
app.domain.com wants you to sign in with your Ethereum account:
0x98626187D3B8e1F7C5b246eE443a07579b5923Ac

Allow access to your Kepler orbit
Authorize action (read, write): Allow access to your Kepler orbit

URI: did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp
Version: 1
Expand Down
Loading