diff --git a/src/lib.rs b/src/lib.rs index 08fcc4d..fa762dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,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; @@ -61,16 +62,27 @@ pub struct CacaoZcapExtraProps { /// CACAO header "t" value pub cacao_payload_type: String, - /// CACAO statement + /// zCap allowed actions /// - /// [CACAO] payload "statement" value + /// + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_action: Option>, + + /// CACAO-ZCAP substatement + /// + /// 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, + pub cacao_zcap_substatement: Option, /// CACAO request ID. /// @@ -206,6 +218,110 @@ impl CacaoZcapProofExtraProps { } } +#[derive(Clone, Debug)] +struct CacaoZcapStatement { + /// zCap [allowedAction](CacaoZcapExtraProps::allowed_action) values + pub actions: Option>, + + /// CACAO-ZCAP [substatement](CacaoZcapExtraProps::cacao_zcap_substatement) + pub substatement: Option, +} +impl CacaoZcapStatement { + /// Construct cacao-zcap statement + pub fn from_actions_and_substatement_opt( + substmt: Option<&str>, + actions: Option<&OneOrMany>, + ) -> 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 { + 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 from attempting to parse a [CacaoZcapStatement] +#[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 { + 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::>(), + )) + } 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 from converting to [CACAO to a Zcap](cacao_to_zcap) #[derive(Error, Debug)] pub enum CacaoToZcapError { @@ -256,6 +372,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> { @@ -323,6 +443,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()); @@ -395,7 +524,8 @@ where 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 { @@ -643,9 +773,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()) @@ -785,7 +922,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() diff --git a/tests/delegation0-zcap.jsonld b/tests/delegation0-zcap.jsonld index 512f0cd..f109f38 100644 --- a/tests/delegation0-zcap.jsonld +++ b/tests/delegation0-zcap.jsonld @@ -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", diff --git a/tests/delegation0.siwe b/tests/delegation0.siwe index 452a20a..0b8eb93 100644 --- a/tests/delegation0.siwe +++ b/tests/delegation0.siwe @@ -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 diff --git a/tests/delegation1-zcap.jsonld b/tests/delegation1-zcap.jsonld index df66560..a37d028 100644 --- a/tests/delegation1-zcap.jsonld +++ b/tests/delegation1-zcap.jsonld @@ -5,12 +5,12 @@ ], "cacaoPayloadType": "eip4361", "cacaoRequestId": "https://example.org/siwe/123123213", - "cacaoStatement": "Allow access to your Kepler orbit", + "cacaoZcapSubstatement": "Allow access to your Kepler orbit", "expires": "2049-01-01T00:00:00Z", - "id": "urn:uuid:076d7929-6c3f-4596-9853-c32d4c412204", + "id": "urn:uuid:db74a464-4292-4202-b781-c43d71c26760", "invocationTarget": "kepler://my_orbit", "invoker": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", - "parentCapability": "urn:uuid:f6076745-00e9-475b-bd97-53245c9be61a", + "parentCapability": "urn:uuid:0807b7fa-0ca6-445c-99f6-f9c6d015b675", "proof": { "cacaoSignatureType": "eip191", "capabilityChain": [ @@ -20,11 +20,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", diff --git a/tests/delegation1.siwe b/tests/delegation1.siwe index c9f9437..86f0235 100644 --- a/tests/delegation1.siwe +++ b/tests/delegation1.siwe @@ -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: Allow access to your Kepler orbit URI: did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp Version: 1 @@ -13,4 +13,4 @@ Not Before: 2022-03-29T14:40:00Z Request ID: https://example.org/siwe/123123213 Resources: - kepler://my_orbit -- data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3YyIiwiaHR0cHM6Ly9kZW1vLmRpZGtpdC5kZXYvMjAyMi9jYWNhby16Y2FwL2NvbnRleHQvdjEuanNvbiJdLCJjYWNhb1BheWxvYWRUeXBlIjoiZWlwNDM2MSIsImNhY2FvUmVxdWVzdElkIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9kZWxlZ2F0aW9ucy85ODE4NzE2NzQiLCJjYWNhb1N0YXRlbWVudCI6IkFsbG93IGFjY2VzcyB0byB5b3VyIEtlcGxlciBvcmJpdCIsImV4cGlyZXMiOiIyMDIyLTAzLTE0VDEzOjMyOjQyLjc2M1oiLCJpZCI6InVybjp1dWlkOmY2MDc2NzQ1LTAwZTktNDc1Yi1iZDk3LTUzMjQ1YzliZTYxYSIsImludm9jYXRpb25UYXJnZXQiOiJrZXBsZXI6Ly9teV9vcmJpdCIsImludm9rZXIiOiJkaWQ6a2V5Ono2TWtpVEJ6MXltdWVwQVE0SEVIWVNGMUg4cXVHNUdMVlZRUjNkamRYM21Eb29XcCN6Nk1raVRCejF5bXVlcEFRNEhFSFlTRjFIOHF1RzVHTFZWUVIzZGpkWDNtRG9vV3AiLCJwYXJlbnRDYXBhYmlsaXR5IjoidXJuOnpjYXA6cm9vdDprZXBsZXIlM0ElMkYlMkZteV9vcmJpdCIsInByb29mIjp7ImNhY2FvU2lnbmF0dXJlVHlwZSI6ImVpcDE5MSIsImNhcGFiaWxpdHlDaGFpbiI6WyJ1cm46emNhcDpyb290OmtlcGxlciUzQSUyRiUyRm15X29yYml0Il0sImNyZWF0ZWQiOiIyMDIyLTAzLTE0VDEzOjMwOjQyLjc2M1oiLCJkb21haW4iOiJhcHAuZG9tYWluLmNvbSIsIm5vbmNlIjoiaDh5UVRkU2N3dDlwVHlhUWEiLCJwcm9vZlB1cnBvc2UiOiJjYXBhYmlsaXR5RGVsZWdhdGlvbiIsInByb29mVmFsdWUiOiJmMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsInR5cGUiOiJDYWNhb1pjYXBQcm9vZjIwMjIiLCJ2ZXJpZmljYXRpb25NZXRob2QiOiJkaWQ6cGtoOmVpcDE1NToxOjB4OTg2MjYxODdEM0I4ZTFGN0M1YjI0NmVFNDQzYTA3NTc5YjU5MjNBYyNibG9ja2NoYWluQWNjb3VudElkIn0sInR5cGUiOiJDYWNhb1pjYXAyMDIyIiwidmFsaWRGcm9tIjoiMjAyMi0wMy0xNFQxMzozMTo0Mi43NjNaIn0= \ No newline at end of file +- data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3YyIiwiaHR0cHM6Ly9kZW1vLmRpZGtpdC5kZXYvMjAyMi9jYWNhby16Y2FwL2NvbnRleHQvdjEuanNvbiJdLCJhbGxvd2VkQWN0aW9uIjpbInJlYWQiLCJ3cml0ZSJdLCJjYWNhb1BheWxvYWRUeXBlIjoiZWlwNDM2MSIsImNhY2FvUmVxdWVzdElkIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9kZWxlZ2F0aW9ucy85ODE4NzE2NzQiLCJjYWNhb1pjYXBTdWJzdGF0ZW1lbnQiOiJBbGxvdyBhY2Nlc3MgdG8geW91ciBLZXBsZXIgb3JiaXQiLCJleHBpcmVzIjoiMjAyMi0wMy0xNFQxMzozMjo0Mi43NjNaIiwiaWQiOiJ1cm46dXVpZDowODA3YjdmYS0wY2E2LTQ0NWMtOTlmNi1mOWM2ZDAxNWI2NzUiLCJpbnZvY2F0aW9uVGFyZ2V0Ijoia2VwbGVyOi8vbXlfb3JiaXQiLCJpbnZva2VyIjoiZGlkOmtleTp6Nk1raVRCejF5bXVlcEFRNEhFSFlTRjFIOHF1RzVHTFZWUVIzZGpkWDNtRG9vV3AjejZNa2lUQnoxeW11ZXBBUTRIRUhZU0YxSDhxdUc1R0xWVlFSM2RqZFgzbURvb1dwIiwicGFyZW50Q2FwYWJpbGl0eSI6InVybjp6Y2FwOnJvb3Q6a2VwbGVyJTNBJTJGJTJGbXlfb3JiaXQiLCJwcm9vZiI6eyJjYWNhb1NpZ25hdHVyZVR5cGUiOiJlaXAxOTEiLCJjYXBhYmlsaXR5Q2hhaW4iOlsidXJuOnpjYXA6cm9vdDprZXBsZXIlM0ElMkYlMkZteV9vcmJpdCJdLCJjcmVhdGVkIjoiMjAyMi0wMy0xNFQxMzozMDo0Mi43NjNaIiwiZG9tYWluIjoiYXBwLmRvbWFpbi5jb20iLCJub25jZSI6Img4eVFUZFNjd3Q5cFR5YVFhIiwicHJvb2ZQdXJwb3NlIjoiY2FwYWJpbGl0eURlbGVnYXRpb24iLCJwcm9vZlZhbHVlIjoiZjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJ0eXBlIjoiQ2FjYW9aY2FwUHJvb2YyMDIyIiwidmVyaWZpY2F0aW9uTWV0aG9kIjoiZGlkOnBraDplaXAxNTU6MToweDk4NjI2MTg3RDNCOGUxRjdDNWIyNDZlRTQ0M2EwNzU3OWI1OTIzQWMjYmxvY2tjaGFpbkFjY291bnRJZCJ9LCJ0eXBlIjoiQ2FjYW9aY2FwMjAyMiIsInZhbGlkRnJvbSI6IjIwMjItMDMtMTRUMTM6MzE6NDIuNzYzWiJ9 \ No newline at end of file