diff --git a/ucan/src/builder.rs b/ucan/src/builder.rs index a06424e9..9ab5c651 100644 --- a/ucan/src/builder.rs +++ b/ucan/src/builder.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use crate::{ capability::{ proof::ProofDelegationSemantics, Action, Capability, CapabilityIpld, CapabilitySemantics, @@ -6,7 +8,7 @@ use crate::{ crypto::KeyMaterial, serde::Base64Encode, time::now, - ucan::{Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, + ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, }; use anyhow::{anyhow, Result}; use base64::Engine; @@ -14,7 +16,6 @@ use cid::multihash::Code; use log::warn; use rand::Rng; use serde::{de::DeserializeOwned, Serialize}; -use serde_json::Value; /// A signable is a UCAN that has all the state it needs in order to be signed, /// but has not yet been signed. @@ -33,7 +34,7 @@ where pub expiration: u64, pub not_before: Option, - pub facts: Vec, + pub facts: FactsMap, pub proofs: Vec, pub add_nonce: bool, } @@ -47,7 +48,6 @@ where UcanHeader { alg: self.issuer.get_jwt_algorithm_name(), typ: "JWT".into(), - ucv: UCAN_VERSION.into(), } } @@ -74,6 +74,7 @@ where }; Ok(UcanPayload { + ucv: UCAN_VERSION.into(), aud: self.audience.clone(), iss: self.issuer.get_did().await?, exp: self.expiration, @@ -121,7 +122,7 @@ where expiration: Option, not_before: Option, - facts: Vec, + facts: FactsMap, proofs: Vec, add_nonce: bool, } @@ -149,7 +150,7 @@ where expiration: None, not_before: None, - facts: Vec::new(), + facts: BTreeMap::new(), proofs: Vec::new(), add_nonce: false, } @@ -198,9 +199,11 @@ where } /// Add a fact or proof of knowledge to this UCAN. - pub fn with_fact(mut self, fact: T) -> Self { + pub fn with_fact(mut self, key: &str, fact: T) -> Self { match serde_json::to_value(fact) { - Ok(value) => self.facts.push(value), + Ok(value) => { + self.facts.insert(key.to_owned(), value); + } Err(error) => warn!("Could not add fact to UCAN: {}", error), } self diff --git a/ucan/src/ipld/ucan.rs b/ucan/src/ipld/ucan.rs index 2750284e..aa9f20a3 100644 --- a/ucan/src/ipld/ucan.rs +++ b/ucan/src/ipld/ucan.rs @@ -3,11 +3,10 @@ use crate::{ crypto::JwtSignatureAlgorithm, ipld::{Principle, Signature}, serde::Base64Encode, - ucan::{Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, + ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION}, }; use cid::Cid; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::{convert::TryFrom, str::FromStr}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -21,7 +20,7 @@ pub struct UcanIpld { pub att: Vec, pub prf: Option>, pub exp: u64, - pub fct: Option>, + pub fct: Option, pub nnc: Option, pub nbf: Option, @@ -72,10 +71,10 @@ impl TryFrom<&UcanIpld> for Ucan { let header = UcanHeader { alg: algorithm.to_string(), typ: "JWT".into(), - ucv: UCAN_VERSION.into(), }; let payload = UcanPayload { + ucv: UCAN_VERSION.into(), iss: value.iss.to_string(), aud: value.aud.to_string(), exp: value.exp, @@ -131,10 +130,13 @@ mod tests { let other_builder = scaffold_ucan_builder(&identities).await.unwrap(); let canon_jwt = canon_builder - .with_fact(json!({ - "baz": true, - "foo": "bar" - })) + .with_fact( + "abc/challenge", + json!({ + "baz": true, + "foo": "bar" + }), + ) .build() .unwrap() .sign() @@ -144,10 +146,13 @@ mod tests { .unwrap(); let other_jwt = other_builder - .with_fact(json!({ - "foo": "bar", - "baz": true - })) + .with_fact( + "abc/challenge", + json!({ + "foo": "bar", + "baz": true + }), + ) .build() .unwrap() .sign() @@ -166,10 +171,13 @@ mod tests { let builder = scaffold_ucan_builder(&identities).await.unwrap(); let jwt = builder - .with_fact(json!({ - "baz": true, - "foo": "bar" - })) + .with_fact( + "abc/challenge", + json!({ + "baz": true, + "foo": "bar" + }), + ) .with_nonce() .build() .unwrap() diff --git a/ucan/src/tests/builder.rs b/ucan/src/tests/builder.rs index 9512ac5b..b8b368f3 100644 --- a/ucan/src/tests/builder.rs +++ b/ucan/src/tests/builder.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use crate::{ builder::UcanBuilder, capability::{CapabilityIpld, CapabilitySemantics}, @@ -52,8 +54,8 @@ async fn it_builds_with_a_simple_example() { .for_audience(identities.bob_did.as_str()) .with_expiration(expiration) .not_before(not_before) - .with_fact(fact_1.clone()) - .with_fact(fact_2.clone()) + .with_fact("abc/challenge", fact_1.clone()) + .with_fact("def/challenge", fact_2.clone()) .claiming_capability(&cap_1) .claiming_capability(&cap_2) .with_nonce() @@ -67,7 +69,13 @@ async fn it_builds_with_a_simple_example() { assert_eq!(ucan.expires_at(), &expiration); assert!(ucan.not_before().is_some()); assert_eq!(ucan.not_before().unwrap(), not_before); - assert_eq!(ucan.facts(), &Some(vec![fact_1, fact_2])); + assert_eq!( + ucan.facts(), + &Some(BTreeMap::from([ + (String::from("abc/challenge"), fact_1), + (String::from("def/challenge"), fact_2), + ])) + ); let expected_attenuations = Vec::from([CapabilityIpld::from(&cap_1), CapabilityIpld::from(&cap_2)]); diff --git a/ucan/src/tests/ucan.rs b/ucan/src/tests/ucan.rs index f41f0def..208e12fd 100644 --- a/ucan/src/tests/ucan.rs +++ b/ucan/src/tests/ucan.rs @@ -7,6 +7,7 @@ mod validate { ucan::Ucan, }; + use serde_json::json; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; @@ -81,6 +82,7 @@ mod validate { .for_audience(identities.bob_did.as_str()) .not_before(now() / 1000) .with_lifetime(30) + .with_fact("abc/challenge", json!({ "foo": "bar" })) .build() .unwrap() .sign() @@ -94,15 +96,18 @@ mod validate { serde_json::json!({ "header": { "alg": "EdDSA", - "typ": "JWT", - "ucv": crate::ucan::UCAN_VERSION + "typ": "JWT" }, "payload": { + "ucv": crate::ucan::UCAN_VERSION, "iss": ucan.issuer(), "aud": ucan.audience(), "exp": ucan.expires_at(), "nbf": ucan.not_before(), "att": [], + "fct": { + "abc/challenge": { "foo": "bar" } + } }, "signed_data": ucan.signed_data(), "signature": ucan.signature() diff --git a/ucan/src/ucan.rs b/ucan/src/ucan.rs index c31899bc..e5ea3cf5 100644 --- a/ucan/src/ucan.rs +++ b/ucan/src/ucan.rs @@ -13,19 +13,21 @@ use cid::{ use libipld_core::{codec::Codec, raw::RawCodec}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{convert::TryFrom, str::FromStr}; +use std::{collections::BTreeMap, convert::TryFrom, str::FromStr}; -pub const UCAN_VERSION: &str = "0.9.0-canary"; +pub const UCAN_VERSION: &str = "0.10.0-canary"; + +pub type FactsMap = BTreeMap; #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct UcanHeader { pub alg: String, pub typ: String, - pub ucv: String, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct UcanPayload { + pub ucv: String, pub iss: String, pub aud: String, pub exp: u64, @@ -35,7 +37,7 @@ pub struct UcanPayload { pub nnc: Option, pub att: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub fct: Option>, + pub fct: Option, #[serde(skip_serializing_if = "Option::is_none")] pub prf: Option>, } @@ -175,12 +177,12 @@ impl Ucan { &self.payload.att } - pub fn facts(&self) -> &Option> { + pub fn facts(&self) -> &Option { &self.payload.fct } pub fn version(&self) -> &str { - &self.header.ucv + &self.payload.ucv } pub fn to_cid(&self, hasher: Code) -> Result {