diff --git a/Cargo.toml b/Cargo.toml index 65f7e4b0f..10e9c0aed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ ssi-jwt = { path = "./ssi-jwt", version = "0.1", default-features = false } ssi-tzkey = { path = "./ssi-tzkey", version = "0.1", default-features = false } ssi-ldp = { path = "./ssi-ldp", version = "0.3.0", default-features = false } ssi-ssh = { path = "./ssi-ssh", version = "0.1", default-features = false } -ssi-ucan = { path = "./ssi-ucan", version = "0.1" } +ssi-ucan = { path = "./ssi-ucan", version = "0.2" } ssi-vc = { path = "./ssi-vc", version = "0.2.0" } ssi-zcap-ld = { path = "./ssi-zcap-ld", version = "0.1.2" } ssi-caips = { path = "./ssi-caips", version = "0.1", default-features = false } diff --git a/ssi-ucan/Cargo.toml b/ssi-ucan/Cargo.toml index 4ba414339..a67e24704 100644 --- a/ssi-ucan/Cargo.toml +++ b/ssi-ucan/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ssi-ucan" -version = "0.1.1" +version = "0.2.0" edition = "2021" authors = ["Spruce Systems, Inc."] license = "Apache-2.0" @@ -12,6 +12,7 @@ documentation = "https://docs.rs/ssi-ucan/" thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_jcs = "0.1" serde_with = { version = "1.14", features = ["base64"]} base64 = "0.12" ssi-jwk = { path = "../ssi-jwk", version = "0.1", default-features = false } @@ -20,8 +21,8 @@ ssi-jwt = { path = "../ssi-jwt", version = "0.1", default-features = false } ssi-dids = { path = "../ssi-dids", version = "0.1.1" } ssi-core = { path = "../ssi-core", version = "0.1" } ssi-caips = { path = "../ssi-caips", version = "0.1", default-features = false } -libipld = { version = "0.14", default-features = false, features = ["dag-cbor", "dag-json", "derive", "serde-codec"]} - +libipld = { version = "0.16", default-features = false, features = ["dag-json", "derive", "serde-codec"]} +ucan-capabilities-object = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] chrono = { version = "0.4", features = ["serde"] } diff --git a/ssi-ucan/src/error.rs b/ssi-ucan/src/error.rs deleted file mode 100644 index d36cede58..000000000 --- a/ssi-ucan/src/error.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - JWS(#[from] ssi_jws::Error), - #[error(transparent)] - DID(#[from] ssi_dids::Error), - #[error(transparent)] - Ipld(#[from] libipld::error::Error), - #[error("Verification method mismatch")] - VerificationMethodMismatch, - #[error("Missing UCAN field, expected: '{0}'")] - MissingUCANHeaderField(&'static str), - #[error("Invalid DID URL")] - DIDURL, - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error(transparent)] - Caip10Parse(#[from] ssi_caips::caip10::BlockchainAccountIdParseError), - #[error(transparent)] - Caip10Verify(#[from] ssi_caips::caip10::BlockchainAccountIdVerifyError), -} diff --git a/ssi-ucan/src/jose.rs b/ssi-ucan/src/jose.rs new file mode 100644 index 000000000..e2f6e070d --- /dev/null +++ b/ssi-ucan/src/jose.rs @@ -0,0 +1,159 @@ +use crate::{ + jwt::{DecodeError, EncodeError, JwtSignatureDe, JwtSignatureSer}, + Ucan, +}; +use serde::Deserialize; +use ssi_dids::did_resolve::DIDResolver; +use ssi_jwk::Algorithm; +use ssi_jws::verify_bytes; + +#[derive(Clone, PartialEq, Debug)] +pub enum Signature { + ES256([u8; 64]), + ES512([u8; 128]), + EdDSA([u8; 64]), + RS256(Vec), + RS512(Vec), + ES256K([u8; 64]), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Incorrect Signature Length")] + IncorrectSignatureLength, + #[error("Unsupported algorithm")] + UnsupportedAlgorithm, +} + +impl Signature { + pub fn new(alg: Algorithm, signature: Vec) -> Result { + Ok(match alg { + Algorithm::ES256 => Self::ES256( + signature + .try_into() + .map_err(|_| Error::IncorrectSignatureLength)?, + ), + Algorithm::EdDSA => Self::EdDSA( + signature + .try_into() + .map_err(|_| Error::IncorrectSignatureLength)?, + ), + Algorithm::RS256 => Self::RS256(signature), + Algorithm::RS512 => Self::RS512(signature), + Algorithm::ES256K => Self::ES256K( + signature + .try_into() + .map_err(|_| Error::IncorrectSignatureLength)?, + ), + _ => return Err(Error::UnsupportedAlgorithm), + }) + } + + pub fn bytes(&self) -> &[u8] { + match self { + Self::ES256(sig) => sig, + Self::ES512(sig) => sig, + Self::EdDSA(sig) => sig, + Self::RS256(sig) => sig, + Self::RS512(sig) => sig, + Self::ES256K(sig) => sig, + } + } + + pub fn alg(&self) -> Algorithm { + match self { + Signature::ES256(_) => Algorithm::ES256, + Signature::ES512(_) => Algorithm::ES256, + Signature::EdDSA(_) => Algorithm::EdDSA, + Signature::RS256(_) => Algorithm::RS256, + Signature::RS512(_) => Algorithm::RS512, + Signature::ES256K(_) => Algorithm::ES256K, + } + } +} + +impl Ucan { + /// Get the JOSE Algorithm of the UCAN + pub fn algorithm(&self) -> Algorithm { + self.signature.alg() + } + + /// Get the Signature bytes of the UCAN + pub fn sig_bytes(&self) -> &[u8] { + self.signature.bytes() + } +} + +impl JwtSignatureSer for Signature { + type Alg<'a> = Algorithm; + type Signature<'s> = &'s [u8]; + fn alg(&self) -> Self::Alg<'_> { + self.alg() + } + fn sig(&self) -> Self::Signature<'_> { + self.bytes() + } +} + +impl JwtSignatureDe for Signature { + type Alg = Algorithm; + type Signature = Vec; + type Error = Error; + fn from_header(a: Self::Alg, s: Self::Signature) -> Result { + Self::new(a, s) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum VerificationError { + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + DID(#[from] ssi_dids::Error), + #[error(transparent)] + Decode(#[from] DecodeError), +} + +impl From for VerificationError +where + E: std::error::Error, +{ + fn from(e: EncodeError) -> Self { + Self::Decode(e.into()) + } +} + +impl Ucan { + /// Decode the UCAN and verify it's signature + /// + /// This method will resolve the DID of the issuer and verify the signature + /// using their public key. + pub async fn decode_and_verify_jwt<'a>( + jwt: &'a str, + resolver: &dyn DIDResolver, + algorithm: Option, + ) -> Result> + where + F: for<'d> Deserialize<'d>, + A: for<'d> Deserialize<'d>, + { + // decode the ucan + let ucan = Self::decode(jwt).map_err(VerificationError::Decode)?; + + // get verification key + let jwk = ucan.get_verification_key(resolver).await?; + + // get signed bytes + let signed = jwt.rsplit_once('.').ok_or(ssi_jws::Error::InvalidJWS)?.0; + + verify_bytes( + algorithm + .or(jwk.algorithm) + .unwrap_or(ucan.signature().alg()), + signed.as_ref(), + &jwk, + ucan.signature().bytes(), + )?; + Ok(ucan) + } +} diff --git a/ssi-ucan/src/jwt.rs b/ssi-ucan/src/jwt.rs new file mode 100644 index 000000000..a104457db --- /dev/null +++ b/ssi-ucan/src/jwt.rs @@ -0,0 +1,188 @@ +use crate::{Payload, Ucan, UcanDecode, UcanEncode}; +use serde::{Deserialize, Serialize}; +pub use ssi_jwk::{Algorithm, JWK}; +use ssi_jws::split_jws; + +pub struct Jwt; + +pub trait JwtSignatureSer { + type Alg<'a> + where + Self: 'a; + type Signature<'s> + where + Self: 's; + fn alg(&self) -> Self::Alg<'_>; + fn sig(&self) -> Self::Signature<'_>; +} + +pub trait JwtSignatureDe { + type Alg; + type Signature; + type Error; + fn from_header(a: Self::Alg, s: Self::Signature) -> Result + where + Self: Sized; +} + +#[derive(thiserror::Error, Debug)] +pub enum DecodeError { + #[error("Invalid DID URL")] + DIDURL, + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + InvalidJws(S), + #[error("Invalid Header Type, expected 'JWT', found {0}")] + InvalidHeaderType(String), + #[error("Invalid Jwt String")] + InvalidJwtString, +} + +impl From for DecodeError +where + S: std::error::Error, +{ + fn from(e: EncodeError) -> Self { + match e { + EncodeError::Base64(e) => DecodeError::Base64(e), + EncodeError::Json(e) => DecodeError::Json(e), + } + } +} + +impl UcanDecode for Ucan +where + F: for<'d> Deserialize<'d>, + A: for<'d> Deserialize<'d>, + S: JwtSignatureDe, + S::Alg: for<'d> Deserialize<'d>, + S::Signature: From>, + S::Error: std::error::Error, +{ + type Error = DecodeError; + type Encoded<'e> = &'e str; + fn decode(jwt: Self::Encoded<'_>) -> Result { + let (alg, payload, sig) = decode_ucan_jwt::(jwt)?; + Ok(Self { + payload, + signature: S::from_header(alg, sig.into()).map_err(DecodeError::InvalidJws)?, + }) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum EncodeError { + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +impl UcanEncode for Ucan +where + F: Serialize, + A: Serialize, + S: JwtSignatureSer, + for<'a> S::Alg<'a>: Serialize, + for<'a> S::Signature<'a>: AsRef<[u8]>, +{ + type Error = EncodeError; + type Encoded<'a> = String where Self: 'a; + /// Encode the UCAN in canonicalized form, by encoding the JWS segments + /// as JCS/DAG-JSON + fn encode(&self) -> Result { + Ok([ + base64::encode_config( + serde_jcs::to_string(&DummyHeader::new(self.signature.alg()))?, + base64::URL_SAFE_NO_PAD, + ), + base64::encode_config( + serde_jcs::to_string(&self.payload)?, + base64::URL_SAFE_NO_PAD, + ), + base64::encode_config(self.signature.sig().as_ref(), base64::URL_SAFE_NO_PAD), + ] + .join(".")) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub(crate) struct DummyHeader { + pub alg: A, + typ: String, +} + +impl DummyHeader { + pub fn new(alg: A) -> Self { + Self { + alg, + typ: "JWT".to_string(), + } + } + + pub(crate) fn check_type(&self) -> Result<(), &str> { + if self.typ != "JWT" { + Err(&self.typ) + } else { + Ok(()) + } + } +} + +impl DummyHeader { + pub fn from_str(h: &str) -> Result> + where + Self: for<'a> Deserialize<'a>, + E: std::error::Error, + { + Ok(serde_json::from_slice(&base64::decode_config( + h, + base64::URL_SAFE_NO_PAD, + )?)?) + } +} + +impl From for DummyHeader { + fn from(alg: A) -> Self { + Self::new(alg) + } +} + +type JwtParts = (A, Payload, Vec); + +fn decode_ucan_jwt(jwt: &str) -> Result, DecodeError> +where + A: for<'d> Deserialize<'d>, + F: for<'d> Deserialize<'d>, + NB: for<'d> Deserialize<'d>, + E: std::error::Error, +{ + let parts = split_jws(jwt).map_err(|_| DecodeError::InvalidJwtString)?; + + let header = DummyHeader::::from_str(parts.0)?; + + // header can only contain 'typ' and 'alg' fields + header + .check_type() + .map_err(|t| DecodeError::InvalidHeaderType(t.to_string()))?; + + let payload: Payload = + serde_json::from_slice(&base64::decode_config(parts.1, base64::URL_SAFE_NO_PAD)?)?; + + // aud must be a DID + if !payload.audience.starts_with("did:") { + return Err(DecodeError::DIDURL); + } + + // iss must be a DID + if !payload.issuer.starts_with("did:") { + return Err(DecodeError::DIDURL); + } + + let sig = base64::decode_config(parts.2, base64::URL_SAFE_NO_PAD)?; + Ok((header.alg, payload, sig)) +} diff --git a/ssi-ucan/src/lib.rs b/ssi-ucan/src/lib.rs index 7ed2c2742..4a86cdb0f 100644 --- a/ssi-ucan/src/lib.rs +++ b/ssi-ucan/src/lib.rs @@ -1,668 +1,109 @@ -pub mod error; -pub use error::Error; -use libipld::{ - codec::{Codec, Decode, Encode}, - error::Error as IpldError, - json::DagJsonCodec, - serde::{from_ipld, to_ipld}, - Block, Cid, Ipld, -}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::Value as JsonValue; -use serde_with::{ - base64::{Base64, UrlSafe}, - serde_as, DisplayFromStr, -}; -use ssi_core::uri::URI; -use ssi_dids::{ - did_resolve::{dereference, Content, DIDResolver}, - Document, Resource, VerificationMethod, VerificationMethodMap, -}; -use ssi_jwk::{Algorithm, JWK}; -use ssi_jws::{decode_jws_parts, sign_bytes, split_jws, verify_bytes, Header}; -use ssi_jwt::NumericDate; -use std::{ - fmt::Display, - io::{Read, Seek, Write}, -}; - -#[derive(Clone, PartialEq, Debug)] -pub struct Ucan { - pub header: Header, - pub payload: Payload, - pub signature: Vec, - // unfortunately this matters for sig verification - // we have to keep track of how this ucan was created - // alternatively we could have 2 different types? - // e.g. DagJsonUcan and RawJwtUcan - codec: UcanCodec, -} - -#[derive(Clone, PartialEq, Debug)] -enum UcanCodec { - // maintain serialization - Raw(String), - DagJson, -} - -impl Default for UcanCodec { - fn default() -> Self { - Self::DagJson - } -} - -impl Ucan { - pub async fn verify_signature(&self, resolver: &dyn DIDResolver) -> Result<(), Error> - where - F: Serialize + Send, - A: Serialize + Send, - { - // extract or deduce signing key - let key: JWK = match ( - self.payload.issuer.get(..4), - self.payload.issuer.get(4..8), - &self.header.jwk, - dereference(resolver, &self.payload.issuer, &Default::default()) - .await - .1, - ) { - // did:pkh without fragment - (Some("did:"), Some("pkh:"), Some(jwk), Content::DIDDocument(d)) => { - match_key_with_did_pkh(jwk, &d)?; - jwk.clone() - } - // did:pkh with fragment - ( - Some("did:"), - Some("pkh:"), - Some(jwk), - Content::Object(Resource::VerificationMethod(vm)), - ) => { - match_key_with_vm(jwk, &vm)?; - jwk.clone() - } - // did:key without fragment - (Some("did:"), Some("key:"), _, Content::DIDDocument(d)) => d - .verification_method - .iter() - .flatten() - .next() - .and_then(|v| match v { - VerificationMethod::Map(vm) => Some(vm), - _ => None, - }) - .ok_or(Error::VerificationMethodMismatch)? - .get_jwk()?, - // general case, did with fragment - (Some("did:"), Some(_), _, Content::Object(Resource::VerificationMethod(vm))) => { - vm.get_jwk()? - } - _ => return Err(Error::VerificationMethodMismatch), - }; - - Ok(verify_bytes( - self.header.algorithm, - self.encode()? - .rsplit_once('.') - .ok_or(ssi_jws::Error::InvalidJWS)? - .0 - .as_bytes(), - &key, - &self.signature, - )?) - } - - pub fn decode(jwt: &str) -> Result - where - F: DeserializeOwned, - A: DeserializeOwned, - { - let parts = split_jws(jwt).and_then(|(h, p, s)| decode_jws_parts(h, p.as_bytes(), s))?; - let (payload, codec): (Payload, UcanCodec) = - match serde_json::from_slice(&parts.payload) { - Ok(p) => Ok((p, UcanCodec::Raw(jwt.to_string()))), - Err(e) => match DagJsonCodec.decode(&parts.payload) { - Ok(p) => Ok((p, UcanCodec::DagJson)), - Err(_) => Err(e), - }, - }?; - - if parts.header.type_.as_deref() != Some("JWT") { - return Err(Error::MissingUCANHeaderField("type: JWT")); - } - - match parts.header.additional_parameters.get("ucv") { - Some(JsonValue::String(v)) if v == "0.9.0" => (), - _ => return Err(Error::MissingUCANHeaderField("ucv: 0.9.0")), - } - - if !payload.audience.starts_with("did:") { - return Err(Error::DIDURL); - } - - Ok(Self { - header: parts.header, - payload, - signature: parts.signature, - codec, - }) - } - - pub fn encode(&self) -> Result - where - F: Serialize, - A: Serialize, - { - Ok(match &self.codec { - UcanCodec::Raw(r) => r.clone(), - UcanCodec::DagJson => [ - base64::encode_config( - DagJsonCodec.encode(&to_ipld(&self.header).map_err(IpldError::new)?)?, - base64::URL_SAFE_NO_PAD, - ), - base64::encode_config(DagJsonCodec.encode(&self.payload)?, base64::URL_SAFE_NO_PAD), - base64::encode_config(&self.signature, base64::URL_SAFE_NO_PAD), - ] - .join("."), - }) - } - - pub fn to_block(&self, hash: H) -> Result, IpldError> - where - F: Serialize, - A: Serialize, - S: libipld::store::StoreParams, - H: Into, - S::Codecs: From + From, - { - match &self.codec { - UcanCodec::Raw(r) => Block::encode(libipld::raw::RawCodec, hash.into(), r.as_bytes()), - UcanCodec::DagJson => Block::encode( - DagJsonCodec, - hash.into(), - &to_ipld(ipld_encoding::DagJsonUcanRef::from(self))?, - ), - } - } - - pub fn from_block(block: &Block) -> Result - where - F: DeserializeOwned, - A: DeserializeOwned, - S: libipld::store::StoreParams, - S::Codecs: From + From, - Ipld: Decode, - { - if block.cid().codec() == S::Codecs::from(DagJsonCodec).into() { - let ipld: Ipld = S::Codecs::from(DagJsonCodec).decode(block.data())?; - let du: ipld_encoding::DagJsonUcan = from_ipld(ipld)?; - Ok(du.into()) - } else if block.cid().codec() == S::Codecs::from(libipld::raw::RawCodec).into() { - Ok(Self::decode(std::str::from_utf8(block.data())?)?) - } else { - Err(IpldError::msg("Invalid Codec, expected 'raw' or 'dagJson'")) - } - } -} - -fn match_key_with_did_pkh(key: &JWK, doc: &Document) -> Result<(), Error> { - doc.verification_method - .iter() - .flatten() - .find_map(|vm| match vm { - VerificationMethod::Map(vm) if vm.blockchain_account_id.is_some() => { - Some(match_key_with_vm(key, vm)) - } - _ => None, - }) - .unwrap_or(Err(Error::VerificationMethodMismatch)) -} - -fn match_key_with_vm(key: &JWK, vm: &VerificationMethodMap) -> Result<(), Error> { - use std::str::FromStr; - Ok(ssi_caips::caip10::BlockchainAccountId::from_str( - vm.blockchain_account_id - .as_ref() - .ok_or(Error::VerificationMethodMismatch)?, - )? - .verify(key)?) -} - -#[serde_as] -#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] -pub struct Payload { - #[serde(rename = "iss")] - pub issuer: String, - #[serde(rename = "aud")] - pub audience: String, - #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")] - pub not_before: Option, - #[serde(rename = "exp")] - pub expiration: NumericDate, - #[serde(rename = "nnc", skip_serializing_if = "Option::is_none")] - pub nonce: Option, - #[serde(rename = "fct", skip_serializing_if = "Option::is_none")] - pub facts: Option>, - #[serde_as(as = "Vec")] - #[serde(rename = "prf")] - pub proof: Vec, - #[serde(rename = "att")] - pub attenuation: Vec>, -} - -#[derive(thiserror::Error, Debug)] -pub enum TimeInvalid { - #[error("UCAN not yet valid")] - TooEarly, - #[error("UCAN has expired")] - TooLate, -} - -impl Payload { - pub fn validate_time(&self, time: Option) -> Result<(), TimeInvalid> { - let t = time.unwrap_or_else(now); - match (self.not_before, t > self.expiration.as_seconds()) { - (_, true) => Err(TimeInvalid::TooLate), - (Some(nbf), _) if t < nbf.as_seconds() => Err(TimeInvalid::TooEarly), - _ => Ok(()), - } - } - - // NOTE IntoIter::new is deprecated, but into_iter() returns references until we move to 2021 edition - #[allow(deprecated)] - pub fn sign(self, algorithm: Algorithm, key: &JWK) -> Result, Error> - where - F: Serialize, - A: Serialize, - { - let header = Header { - algorithm, - type_: Some("JWT".to_string()), - additional_parameters: std::array::IntoIter::new([( - "ucv".to_string(), - serde_json::Value::String("0.9.0".to_string()), - )]) - .collect(), - ..Default::default() - }; - - let signature = sign_bytes( - algorithm, - [ - base64::encode_config( - DagJsonCodec.encode(&to_ipld(&header).map_err(IpldError::new)?)?, - base64::URL_SAFE_NO_PAD, - ), - base64::encode_config(DagJsonCodec.encode(&self)?, base64::URL_SAFE_NO_PAD), - ] - .join(".") - .as_bytes(), - key, - )?; - - Ok(Ucan { - header, - payload: self, - signature, - codec: UcanCodec::DagJson, - }) - } -} - -#[serde_as] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] -#[serde(untagged)] -pub enum UcanResource { - Proof(#[serde_as(as = "DisplayFromStr")] UcanProofRef), - URI(URI), -} - -impl Display for UcanResource { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match &self { - Self::Proof(p) => write!(f, "{p}"), - Self::URI(u) => write!(f, "{u}"), - } - } -} +pub mod jose; +pub mod jwt; +pub mod payload; +pub mod revocation; +pub mod util; +pub mod version; -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct UcanProofRef(pub Cid); +pub use payload::{capabilities, Payload, TimeInvalid}; +pub use revocation::Revocation; +pub use util::{UcanDecode, UcanEncode}; -impl Display for UcanProofRef { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "ucan:{}", self.0) - } -} +use serde_json::Value as JsonValue; +use ssi_dids::did_resolve::DIDResolver; +pub use ssi_jwk::{Algorithm, JWK}; -#[derive(thiserror::Error, Debug)] -pub enum ProofRefParseErr { - #[error("Missing ucan prefix")] - Format, - #[error("Invalid Cid reference")] - ParseCid(#[from] libipld::cid::Error), +/// A deserialized UCAN +#[derive(Clone, PartialEq, Debug)] +pub struct Ucan { + payload: Payload, + signature: S, } -impl std::str::FromStr for UcanProofRef { - type Err = ProofRefParseErr; - - fn from_str(s: &str) -> Result { - Ok(UcanProofRef( - s.strip_prefix("ucan:") - .map(Cid::from_str) - .ok_or(ProofRefParseErr::Format)??, - )) +impl Ucan { + /// Get the Payload of the UCAN + pub fn payload(&self) -> &Payload { + &self.payload } -} -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct UcanScope { - pub namespace: String, - pub capability: String, -} - -impl std::fmt::Display for UcanScope { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}/{}", self.namespace, self.capability) + pub fn signature(&self) -> &S { + &self.signature } -} - -#[derive(thiserror::Error, Debug)] -pub enum UcanScopeParseErr { - #[error("Missing namespace")] - Namespace, -} - -impl std::str::FromStr for UcanScope { - type Err = UcanScopeParseErr; - fn from_str(s: &str) -> Result { - let (ns, cap) = s.split_once('/').ok_or(UcanScopeParseErr::Namespace)?; - Ok(UcanScope { - namespace: ns.to_string(), - capability: cap.to_string(), - }) + pub fn into_inner(self) -> (Payload, S) { + (self.payload, self.signature) } -} - -/// 3.2.5 A JSON capability MUST include the with and can fields and -/// MAY have additional fields needed to describe the capability -#[serde_as] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] -pub struct Capability { - pub with: UcanResource, - #[serde_as(as = "DisplayFromStr")] - pub can: UcanScope, - #[serde(rename = "nb", skip_serializing_if = "Option::is_none")] - pub additional_fields: Option, -} - -fn now() -> f64 { - (chrono::prelude::Utc::now() - .timestamp_nanos_opt() - .expect("value can not be represented in a timestamp with nanosecond precision.") - as f64) - / 1e+9_f64 -} - -#[serde_as] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] -pub struct UcanRevocation { - #[serde(rename = "iss")] - pub issuer: String, - #[serde_as(as = "DisplayFromStr")] - pub revoke: Cid, - #[serde_as(as = "Base64")] - pub challenge: Vec, -} -impl UcanRevocation { - pub fn sign( - issuer: String, - revoke: Cid, - jwk: &JWK, - algorithm: Algorithm, - ) -> Result { - Ok(Self { - issuer, - revoke, - challenge: sign_bytes(algorithm, format!("REVOKE:{revoke}").as_bytes(), jwk)?, - }) - } - pub async fn verify_signature( + /// Extract or resolve the JWK used to issue this UCAN, if possible + pub async fn get_verification_key( &self, resolver: &dyn DIDResolver, - algorithm: Algorithm, - jwk: Option<&JWK>, - ) -> Result<(), Error> { - let key: JWK = match ( - self.issuer.get(..4), - self.issuer.get(4..8), - jwk, - dereference(resolver, &self.issuer, &Default::default()) - .await - .1, - ) { - // did:pkh without fragment - (Some("did:"), Some("pkh:"), Some(jwk), Content::DIDDocument(d)) => { - match_key_with_did_pkh(jwk, &d)?; - jwk.clone() - } - // did:pkh with fragment - ( - Some("did:"), - Some("pkh:"), - Some(jwk), - Content::Object(Resource::VerificationMethod(vm)), - ) => { - match_key_with_vm(jwk, &vm)?; - jwk.clone() - } - // did:key without fragment - (Some("did:"), Some("key:"), _, Content::DIDDocument(d)) => d - .verification_method - .iter() - .flatten() - .next() - .and_then(|v| match v { - VerificationMethod::Map(vm) => Some(vm), - _ => None, - }) - .ok_or(Error::VerificationMethodMismatch)? - .get_jwk()?, - // general case, did with fragment - (Some("did:"), Some(_), _, Content::Object(Resource::VerificationMethod(vm))) => { - vm.get_jwk()? - } - _ => return Err(Error::VerificationMethodMismatch), - }; - - Ok(verify_bytes( - algorithm, - format!("REVOKE:{}", self.revoke).as_bytes(), - &key, - &self.challenge, - )?) - } -} - -mod ipld_encoding { - use super::*; - - #[derive(Serialize, Clone, PartialEq, Debug)] - pub struct DagJsonUcanRef<'a, F = JsonValue, A = JsonValue> { - header: &'a Header, - payload: DagJsonPayloadRef<'a, F, A>, - signature: &'a [u8], - } - - #[derive(Deserialize, Clone, PartialEq, Debug)] - pub struct DagJsonUcan { - header: Header, - payload: DagJsonPayload, - signature: Vec, - } - - #[derive(Serialize, Clone, PartialEq, Debug)] - pub struct DagJsonPayloadRef<'a, F = JsonValue, A = JsonValue> { - pub iss: &'a str, - pub aud: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - pub nbf: &'a Option, - pub exp: &'a NumericDate, - #[serde(skip_serializing_if = "Option::is_none")] - pub nnc: &'a Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub fct: &'a Option>, - pub prf: &'a Vec, - pub att: &'a Vec>, - } - - #[derive(Deserialize, Clone, PartialEq, Debug)] - pub struct DagJsonPayload { - pub iss: String, - pub aud: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub nbf: Option, - pub exp: NumericDate, - #[serde(skip_serializing_if = "Option::is_none")] - pub nnc: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub fct: Option>, - pub prf: Vec, - pub att: Vec>, - } - - impl Encode for Ucan - where - F: Serialize, - A: Serialize, - { - fn encode(&self, c: DagJsonCodec, w: &mut W) -> Result<(), IpldError> { - to_ipld(ipld_encoding::DagJsonUcanRef::from(self))?.encode(c, w) - } + ) -> Result { + util::get_verification_key(&self.payload.issuer, resolver).await } - impl Decode for Ucan + /// Decode the UCAN + pub fn decode( + encoded: >::Encoded<'_>, + ) -> Result>::Error> where - F: DeserializeOwned, - A: DeserializeOwned, + Self: UcanDecode, { - fn decode(c: DagJsonCodec, r: &mut R) -> Result { - let u: ipld_encoding::DagJsonUcan = from_ipld(Ipld::decode(c, r)?)?; - Ok(u.into()) - } - } - - impl Encode for Payload - where - F: Serialize, - A: Serialize, - { - fn encode(&self, c: DagJsonCodec, w: &mut W) -> Result<(), IpldError> { - to_ipld(ipld_encoding::DagJsonPayloadRef::from(self))?.encode(c, w) - } + UcanDecode::::decode(encoded) } - impl Decode for Payload + /// Encode the UCAN + pub fn encode( + &self, + ) -> Result<>::Encoded<'_>, >::Error> where - F: DeserializeOwned, - A: DeserializeOwned, + Self: UcanEncode, { - fn decode(c: DagJsonCodec, r: &mut R) -> Result { - let p: ipld_encoding::DagJsonPayload = from_ipld(Ipld::decode(c, r)?)?; - Ok(p.into()) - } - } - - impl<'a, F, A> From<&'a Ucan> for DagJsonUcanRef<'a, F, A> { - fn from(u: &'a Ucan) -> Self { - Self { - header: &u.header, - payload: DagJsonPayloadRef::from(&u.payload), - signature: &u.signature, - } - } - } - - impl From> for Ucan { - fn from(u: DagJsonUcan) -> Self { - Self { - header: u.header, - payload: u.payload.into(), - signature: u.signature, - codec: UcanCodec::DagJson, - } - } - } - - impl<'a, F, A> From<&'a Payload> for DagJsonPayloadRef<'a, F, A> { - fn from(p: &'a Payload) -> Self { - Self { - iss: &p.issuer, - aud: &p.audience, - nbf: &p.not_before, - exp: &p.expiration, - nnc: &p.nonce, - fct: &p.facts, - prf: &p.proof, - att: &p.attenuation, - } - } - } - - impl From> for Payload { - fn from(p: DagJsonPayload) -> Self { - Self { - issuer: p.iss, - audience: p.aud, - not_before: p.nbf, - expiration: p.exp, - nonce: p.nnc, - facts: p.fct, - proof: p.prf, - attenuation: p.att, - } - } + UcanEncode::::encode(self) } } #[cfg(test)] mod tests { + use super::payload::now; + use super::util::canonical_cid; use super::*; use did_method_key::DIDKey; - use ssi_dids::DIDMethod; + use serde::Deserialize; + use ssi_dids::{DIDMethod, Source}; + use ssi_jwk::Algorithm; + use ssi_jws::Header; #[async_std::test] async fn valid() { let cases: Vec = - serde_json::from_str(include_str!("../../tests/ucan-v0.9.0-valid.json")).unwrap(); + serde_json::from_str(include_str!("../../tests/ucan-v0.10.0-valid.json")).unwrap(); for case in cases { - let ucan = match Ucan::decode(&case.token) { - Ok(u) => u, - Err(e) => Err(e).unwrap(), - }; - - match ucan.verify_signature(DIDKey.to_resolver()).await { - Err(e) => Err(e).unwrap(), - _ => {} - }; + let ucan = Ucan::decode_and_verify_jwt(&case.token, DIDKey.to_resolver(), None) + .await + .unwrap(); assert_eq!(ucan.payload, case.assertions.payload); - assert_eq!(ucan.header, case.assertions.header); + assert_eq!(ucan.signature().alg(), case.assertions.header.algorithm); } } #[async_std::test] async fn invalid() { let cases: Vec = - serde_json::from_str(include_str!("../../tests/ucan-v0.9.0-invalid.json")).unwrap(); + serde_json::from_str(include_str!("../../tests/ucan-v0.10.0-invalid.json")).unwrap(); for case in cases { match Ucan::::decode(&case.token) { Ok(u) => { - if u.payload.validate_time(None).is_ok() - && u.verify_signature(DIDKey.to_resolver()).await.is_ok() + if u.payload.validate_time::(None).is_ok() + && Ucan::::decode_and_verify_jwt( + &case.token, + DIDKey.to_resolver(), + None, + ) + .await + .is_ok() { assert!(false, "{}", case.comment); } @@ -674,9 +115,28 @@ mod tests { #[async_std::test] async fn basic() { - let case = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wIn0.eyJhdHQiOltdLCJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJleHAiOjkwMDAwMDAwMDEuMCwiaXNzIjoiZGlkOmtleTp6Nk1ram16ZXBUcGc0NFJvejhKbk45QXhUS0QyMjk1Z2p6M3h0NDhQb2k3MjYxR1MiLCJwcmYiOltdfQ.V38liNHsdVO0Zk_davTBsewq-2XCxs_3qIRLuwUNj87aqdlMfa9X5O5IRR5u7apzWm7sUiR0FS3J3Nnu7IWtBQ"; - let u = Ucan::::decode(case).unwrap(); - u.verify_signature(DIDKey.to_resolver()).await.unwrap(); + let key = JWK::generate_ed25519().unwrap(); + let iss = DIDKey.generate(&Source::Key(&key)).unwrap(); + let aud = "did:example:123".to_string(); + let mut payload = Payload::::new(iss, aud); + payload.expiration = Some(now() + 60); + payload.not_before = Some(now() - 60); + payload + .capabilities + .with_action_convert("https://example.com/resource", "https/get", []) + .unwrap(); + payload.proof = Some(vec![canonical_cid("hello")]); + + let ucan = payload.sign_with_jwk(&key, Some(Algorithm::EdDSA)).unwrap(); + + let encoded = ucan.encode::().unwrap(); + Ucan::::decode_and_verify_jwt( + &encoded, + DIDKey.to_resolver(), + Some(Algorithm::EdDSA), + ) + .await + .unwrap(); } #[derive(Deserialize)] diff --git a/ssi-ucan/src/payload.rs b/ssi-ucan/src/payload.rs new file mode 100644 index 000000000..a38b7cb46 --- /dev/null +++ b/ssi-ucan/src/payload.rs @@ -0,0 +1,150 @@ +use super::{jose, jwt, version::SemanticVersion, Ucan}; +use libipld::Cid; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use serde_with::{serde_as, DisplayFromStr}; +use ssi_jwk::{Algorithm, JWK}; +use ssi_jws::sign_bytes; +use std::collections::BTreeMap; + +use capabilities::Capabilities; +pub use ucan_capabilities_object as capabilities; + +/// The Payload of a UCAN, with JWS registered claims and UCAN specific claims +#[serde_as] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash)] +#[serde(deny_unknown_fields)] +pub struct Payload { + #[serde(rename = "ucv")] + semantic_version: SemanticVersion, + #[serde(rename = "iss")] + pub issuer: String, + #[serde(rename = "aud")] + pub audience: String, + #[serde(rename = "iat", skip_serializing_if = "Option::is_none", default)] + pub issued_at: Option, + #[serde(rename = "nbf", skip_serializing_if = "Option::is_none", default)] + pub not_before: Option, + // no expiration should serialize to null in JSON + #[serde(rename = "exp")] + pub expiration: Option, + #[serde(rename = "nnc", skip_serializing_if = "Option::is_none", default)] + pub nonce: Option, + #[serde( + rename = "fct", + skip_serializing_if = "Option::is_none", + default = "Option::default" + )] + pub facts: Option>, + #[serde_as(as = "Option>")] + #[serde(rename = "prf", skip_serializing_if = "Option::is_none", default)] + pub proof: Option>, + #[serde(rename = "cap")] + pub capabilities: Capabilities, +} + +impl Payload { + /// Create a new UCAN payload + pub fn new(issuer: String, audience: String) -> Self { + Self { + semantic_version: SemanticVersion, + issuer, + audience, + issued_at: None, + not_before: None, + expiration: None, + nonce: None, + facts: None, + proof: None, + capabilities: Capabilities::new(), + } + } + + /// Validate the time bounds of the UCAN payload + /// + /// Passing `None` will use the current system time. + pub fn validate_time>(&self, time: Option) -> Result<(), TimeInvalid> { + match time { + Some(t) => cmp_time(t, self.not_before, self.expiration), + None => cmp_time(now(), self.not_before, self.expiration), + } + } + + /// Sign the payload with the given key and algorithm + /// + /// This will use the canonical form of the UCAN for signing + pub fn sign_with_jwk(self, key: &JWK, algorithm: Option) -> Result, Error> + where + F: Serialize, + A: Serialize, + { + let alg = algorithm.or(key.algorithm).ok_or(Error::AlgUnknown)?; + let signature = sign_bytes(alg, self.encode_for_signing_jwt(alg)?.as_ref(), key)?; + + Ok(self.sign(jose::Signature::new(alg, signature)?)) + } + + /// Encode the payload and header in cannonical form for signing + pub fn encode_for_signing_jwt(&self, alg: Alg) -> Result + where + F: Serialize, + A: Serialize, + Alg: Serialize, + { + Ok([ + base64::encode_config( + serde_jcs::to_string(&jwt::DummyHeader::new(alg))?, + base64::URL_SAFE_NO_PAD, + ), + base64::encode_config(serde_jcs::to_string(&self)?, base64::URL_SAFE_NO_PAD), + ] + .join(".")) + } + + /// Sign the payload with the given signature + /// + /// This will not ensure that the signature is valid for the payload and will + /// not canonicalize the payload before signing. + pub fn sign(self, signature: S) -> Ucan { + Ucan { + payload: self, + signature, + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Unable to infer algorithm")] + AlgUnknown, + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + Encoding(#[from] jwt::EncodeError), + #[error(transparent)] + InvalidSignature(#[from] jose::Error), +} + +#[derive(thiserror::Error, Debug)] +pub enum TimeInvalid { + #[error("UCAN not yet valid")] + TooEarly, + #[error("UCAN has expired")] + TooLate, +} + +pub(crate) fn now() -> u64 { + chrono::prelude::Utc::now().timestamp() as u64 +} + +fn cmp_time>( + t: T, + nbf: Option, + exp: Option, +) -> Result<(), TimeInvalid> { + match (nbf, exp) { + (_, Some(exp)) if t >= exp => Err(TimeInvalid::TooLate), + (Some(nbf), _) if t < nbf => Err(TimeInvalid::TooEarly), + _ => Ok(()), + } +} diff --git a/ssi-ucan/src/revocation.rs b/ssi-ucan/src/revocation.rs new file mode 100644 index 000000000..644e7c923 --- /dev/null +++ b/ssi-ucan/src/revocation.rs @@ -0,0 +1,83 @@ +use super::{util::get_verification_key, version::RevocationSemanticVersion}; +use libipld::Cid; +use serde::{Deserialize, Serialize}; +use serde_with::{ + base64::{Base64, UrlSafe}, + formats::Unpadded, + serde_as, DisplayFromStr, +}; +use ssi_dids::did_resolve::DIDResolver; +use ssi_jwk::{Algorithm, JWK}; +use ssi_jws::{sign_bytes, verify_bytes}; + +#[serde_as] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Revocation { + #[serde(rename = "urv")] + semantic_version: RevocationSemanticVersion, + #[serde(rename = "iss")] + pub issuer: String, + #[serde(rename = "rvk")] + #[serde_as(as = "DisplayFromStr")] + pub revoke: Cid, + #[serde(rename = "sig")] + #[serde_as(as = "Base64")] + pub signature: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + DID(#[from] ssi_dids::Error), + #[error("Unable to infer algorithm")] + AlgUnknown, +} + +impl Revocation { + pub fn new(issuer: String, revoke: Cid, signature: Vec) -> Self { + Self { + semantic_version: RevocationSemanticVersion, + issuer, + revoke, + signature, + } + } + + pub fn encode_for_signing(revoke: &Cid) -> String { + format!("REVOKE-UCAN:{}", revoke) + } + + pub fn sign_with_jwk( + issuer: String, + revoke: Cid, + jwk: &JWK, + algorithm: Option, + ) -> Result { + Ok(Self::new( + issuer, + revoke, + sign_bytes( + algorithm.or(jwk.algorithm).ok_or(Error::AlgUnknown)?, + Self::encode_for_signing(&revoke).as_bytes(), + jwk, + )?, + )) + } + + pub async fn verify_signature( + &self, + resolver: &dyn DIDResolver, + algorithm: Option, + ) -> Result<(), Error> { + let key: JWK = get_verification_key(&self.issuer, resolver).await?; + + Ok(verify_bytes( + algorithm.or(key.algorithm).ok_or(Error::AlgUnknown)?, + Self::encode_for_signing(&self.revoke).as_bytes(), + &key, + &self.signature, + )?) + } +} diff --git a/ssi-ucan/src/util.rs b/ssi-ucan/src/util.rs new file mode 100644 index 000000000..8bf997382 --- /dev/null +++ b/ssi-ucan/src/util.rs @@ -0,0 +1,66 @@ +use libipld::{ + multihash::{Code, MultihashDigest}, + Cid, +}; +use ssi_dids::{ + did_resolve::{dereference, Content, DIDResolver}, + Error, Resource, VerificationMethod, +}; +use ssi_jwk::JWK; + +/// Calculate the canonical CID of a UCAN +/// +/// This function does not verify that the given string is a valid UCAN. +pub fn canonical_cid(jwt: &str) -> Cid { + Cid::new_v1(0x55, Code::Sha2_256.digest(jwt.as_bytes())) +} + +/// Extract or resolve the JWK for the given DID, if possible +pub async fn get_verification_key(id: &str, resolver: &dyn DIDResolver) -> Result { + match ( + id.get(..4), + id.get(4..8), + dereference(resolver, id, &Default::default()).await.1, + ) { + // TODO here we will have some complicated cases w.r.t. did:pkh + // some did:pkh's have recoverable signatures, some don't and will need + // a query param on the did + // + // did:key without fragment + (Some("did:"), Some("key:"), Content::DIDDocument(d)) => d + .verification_method + .iter() + .flatten() + .next() + .and_then(|v| match v { + VerificationMethod::Map(vm) => Some(vm), + _ => None, + }) + .ok_or(Error::MissingKey)? + .get_jwk() + .map_err(Error::from), + // general case, did with fragment + (Some("did:"), Some(_), Content::Object(Resource::VerificationMethod(vm))) => { + Ok(vm.get_jwk()?) + } + _ => Err(Error::MissingKey), + } +} + +pub trait UcanDecode { + type Error; + type Encoded<'e>; + /// Decode the UCAN + fn decode(encoded: Self::Encoded<'_>) -> Result + where + Self: Sized; +} + +pub trait UcanEncode { + type Error; + type Encoded<'a> + where + Self: 'a; + /// Encode the UCAN + fn encode(&self) -> Result, Self::Error>; +} diff --git a/ssi-ucan/src/version.rs b/ssi-ucan/src/version.rs new file mode 100644 index 000000000..1b7ee2d04 --- /dev/null +++ b/ssi-ucan/src/version.rs @@ -0,0 +1,75 @@ +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + str::FromStr, +}; + +#[derive( + Clone, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Debug, + SerializeDisplay, + DeserializeFromStr, + Default, +)] +pub struct SemanticVersion; + +impl Display for SemanticVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "0.2.0") + } +} + +#[derive(thiserror::Error, Debug)] +pub enum VersionError { + #[error("Invalid version: expected {0}, found {1}")] + InvalidVersion(&'static str, String), +} + +impl FromStr for SemanticVersion { + type Err = VersionError; + + fn from_str(s: &str) -> Result { + if s == "0.2.0" { + Ok(Self) + } else { + Err(VersionError::InvalidVersion("0.2.0", s.to_string())) + } + } +} + +#[derive( + Clone, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + Debug, + SerializeDisplay, + DeserializeFromStr, + Default, +)] +pub struct RevocationSemanticVersion; + +impl Display for RevocationSemanticVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "1.0.0-rc1") + } +} + +impl FromStr for RevocationSemanticVersion { + type Err = VersionError; + + fn from_str(s: &str) -> Result { + if s == "1.0.0-rc1" { + Ok(Self) + } else { + Err(VersionError::InvalidVersion("1.0.0-rc1", s.to_string())) + } + } +} diff --git a/tests/ucan-v0.9.0-invalid.json b/tests/ucan-v0.10.0-invalid.json similarity index 100% rename from tests/ucan-v0.9.0-invalid.json rename to tests/ucan-v0.10.0-invalid.json diff --git a/tests/ucan-v0.10.0-valid.json b/tests/ucan-v0.10.0-valid.json new file mode 100644 index 000000000..f1c5ced4b --- /dev/null +++ b/tests/ucan-v0.10.0-valid.json @@ -0,0 +1,61 @@ +[ + { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJjYXAiOnt9LCJleHAiOm51bGwsImlzcyI6ImRpZDprZXk6ejZNa24yQ0dudnlWRUN6aXNFQU5Ib2ZEbzNDMlFMdFV6MWt6bmhueUhtejd6S3VLIiwidWN2IjoiMC4yLjAifQ.KfsbfN8D5wh5Jciu7sFN8B_IfOVYsnjvX-6Ib1EQ-Kfa8n_ETrqoiGH1Vp7JvPUqaVOq8cd4WcudiUxsFLE7Cg", + "assertions": { + "payload": { + "ucv": "0.2.0", + "iss": "did:key:z6Mkn2CGnvyVECzisEANHofDo3C2QLtUz1kznhnyHmz7zKuK", + "aud": "did:example:123", + "exp": null, + "cap": {} + }, + "header": { + "alg": "EdDSA", + "typ": "JWT" + } + } + }, + { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJjYXAiOnt9LCJleHAiOjE2ODkxNjAwMjcsImlzcyI6ImRpZDprZXk6ejZNa3JqZjZVVnIxTlRwTmFuWkZQZmROZ1V6aE53VGV1S0V6M2RNUjJOZm9meWZmIiwibmJmIjoxNjg5MTU5OTA3LCJ1Y3YiOiIwLjIuMCJ9.pPOWu9qchivhnUqNFhTD7sTSMLdUzGTzBeOl8nrlACHgrgKq5t00yI8vCaF6ykM533MOExI2vhcryiiH5MdtAQ", + "assertions": { + "payload": { + "ucv": "0.2.0", + "iss": "did:key:z6Mkrjf6UVr1NTpNanZFPfdNgUzhNwTeuKEz3dMR2Nfofyff", + "aud": "did:example:123", + "nbf": 1689159907, + "exp": 1689160027, + "cap": {} + }, + "header": { + "alg": "EdDSA", + "typ": "JWT" + } + } + }, + { + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJjYXAiOnsiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZXNvdXJjZSI6eyJodHRwcy9nZXQiOlt7fV19fSwiZXhwIjoxNjg5MTYwMDU5LCJpc3MiOiJkaWQ6a2V5Ono2TWt1WHRkcWNKUFpYVW5TRHZ4RUQ4Zkp5THQxcGk4ODdtUXdiSDE3Mno5REtXUiIsIm5iZiI6MTY4OTE1OTkzOSwicHJmIjpbImJhZmtyZWlibTZqZzN1eDVxdW1oY24yYjNmbGMzdHl1NmRtbGI0eGE3dTViZjQ0eWVnbnJqaGM0eWVxIl0sInVjdiI6IjAuMi4wIn0.1NGP4X41R-hwnEJ-jM0iGLjQtbBx35KtqkmElCSzMB776P1hLKNGPmTmrSfb4G5uHnh24NkZm7oum98Mi3WDAA", + "assertions": { + "payload": { + "ucv": "0.2.0", + "iss": "did:key:z6MkuXtdqcJPZXUnSDvxED8fJyLt1pi887mQwbH172z9DKWR", + "aud": "did:example:123", + "nbf": 1689159939, + "exp": 1689160059, + "prf": [ + "bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq" + ], + "cap": { + "https://example.com/resource": { + "https/get": [ + {} + ] + } + } + }, + "header": { + "alg": "EdDSA", + "typ": "JWT" + } + } + } +] diff --git a/tests/ucan-v0.9.0-valid.json b/tests/ucan-v0.9.0-valid.json deleted file mode 100644 index 0dc5cc92d..000000000 --- a/tests/ucan-v0.9.0-valid.json +++ /dev/null @@ -1,53 +0,0 @@ -[ - { - "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wIn0.eyJhdHQiOltdLCJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJleHAiOjkwMDAwMDAwMDEuMCwiaXNzIjoiZGlkOmtleTp6Nk1rZVhUbXhMZHh2YXl1NXB5UTc4RkprTEVLYXpUeHlrQUZ4a25YUmhpSDF5NXAiLCJwcmYiOltdfQ.vvrE5Datsn3Q_PSRSEm1cwacWTcXEkIencu9qzqFt033KPW2yk1Bt4SSUi4wKn_Ji4aeSCus7YXiT6jugikiDw", - "assertions": { - "payload": { - "iss": "did:key:z6MkeXTmxLdxvayu5pyQ78FJkLEKazTxykAFxknXRhiH1y5p", - "aud": "did:example:123", - "exp": 9000000001.0, - "prf": [], - "att": [] - }, - "header": { - "alg": "EdDSA", - "typ": "JWT", - "ucv": "0.9.0" - } - } - }, - { - "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wIn0.eyJhdHQiOltdLCJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJleHAiOjkwMDAwMDAwMDEuMCwiaXNzIjoiZGlkOmtleTp6Nk1rdXRwalBGRXUycExvaUMybWptYzVMZjhSQ0xYS3BuaWsxN0dGb2pqcmh5bm0iLCJwcmYiOltdfQ.Js6nvQNDKDw3jwqJWTSotDS2HwRRc4iKZEH_c5aIYshF_grAM3gS369I54CaT1ScUImZsSGwMT-oraS4x2a3BA", - "assertions": { - "payload": { - "iss": "did:key:z6MkutpjPFEu2pLoiC2mjmc5Lf8RCLXKpnik17GFojjrhynm", - "aud": "did:example:123", - "exp": 9000000001.0, - "prf": [], - "att": [] - }, - "header": { - "alg": "EdDSA", - "typ": "JWT", - "ucv": "0.9.0" - } - } - }, - { - "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wIn0.eyJhdHQiOltdLCJhdWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJleHAiOjkwMDAwMDAwMDEuMCwiaXNzIjoiZGlkOmtleTp6Nk1ram16ZXBUcGc0NFJvejhKbk45QXhUS0QyMjk1Z2p6M3h0NDhQb2k3MjYxR1MiLCJwcmYiOltdfQ.V38liNHsdVO0Zk_davTBsewq-2XCxs_3qIRLuwUNj87aqdlMfa9X5O5IRR5u7apzWm7sUiR0FS3J3Nnu7IWtBQ", - "assertions": { - "payload": { - "iss": "did:key:z6MkjmzepTpg44Roz8JnN9AxTKD2295gjz3xt48Poi7261GS", - "aud": "did:example:123", - "exp": 9000000001.0, - "prf": [], - "att": [] - }, - "header": { - "alg": "EdDSA", - "typ": "JWT", - "ucv": "0.9.0" - } - } -} - ]