ucan 0.4.0

Implement UCAN-based authorization with conciseness and ease!
Documentation
use crate::{
    capability::Capabilities,
    crypto::JwtSignatureAlgorithm,
    ipld::{Principle, Signature},
    serde::Base64Encode,
    ucan::{FactsMap, Ucan, UcanHeader, UcanPayload, UCAN_VERSION},
};
use cid::Cid;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, str::FromStr};

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UcanIpld {
    pub v: String,

    pub iss: Principle,
    pub aud: Principle,
    pub s: Signature,

    pub cap: Capabilities,
    pub prf: Option<Vec<Cid>>,
    pub exp: Option<u64>,
    pub fct: Option<FactsMap>,

    pub nnc: Option<String>,
    pub nbf: Option<u64>,
}

impl TryFrom<&Ucan> for UcanIpld {
    type Error = anyhow::Error;

    fn try_from(ucan: &Ucan) -> Result<Self, Self::Error> {
        let prf = if let Some(proofs) = ucan.proofs() {
            let mut prf = Vec::new();
            for cid_string in proofs {
                prf.push(Cid::try_from(cid_string.as_str())?);
            }
            if prf.is_empty() {
                None
            } else {
                Some(prf)
            }
        } else {
            None
        };

        Ok(UcanIpld {
            v: ucan.version().to_string(),
            iss: Principle::from_str(ucan.issuer())?,
            aud: Principle::from_str(ucan.audience())?,
            s: Signature::try_from((
                JwtSignatureAlgorithm::from_str(ucan.algorithm())?,
                ucan.signature(),
            ))?,
            cap: ucan.capabilities().clone(),
            prf,
            exp: *ucan.expires_at(),
            fct: ucan.facts().clone(),
            nnc: ucan.nonce().as_ref().cloned(),
            nbf: *ucan.not_before(),
        })
    }
}

impl TryFrom<&UcanIpld> for Ucan {
    type Error = anyhow::Error;

    fn try_from(value: &UcanIpld) -> Result<Self, Self::Error> {
        let (algorithm, signature) = value.s.decode()?;

        let header = UcanHeader {
            alg: algorithm.to_string(),
            typ: "JWT".into(),
        };

        let payload = UcanPayload {
            ucv: UCAN_VERSION.into(),
            iss: value.iss.to_string(),
            aud: value.aud.to_string(),
            exp: value.exp,
            nbf: value.nbf,
            nnc: value.nnc.clone(),
            cap: value.cap.clone(),
            fct: value.fct.clone(),
            prf: value
                .prf
                .clone()
                .map(|prf| prf.iter().map(|cid| cid.to_string()).collect()),
        };

        let signed_data = format!(
            "{}.{}",
            header.jwt_base64_encode()?,
            payload.jwt_base64_encode()?
        )
        .as_bytes()
        .to_vec();

        Ok(Ucan::new(header, payload, signed_data, signature))
    }
}

#[cfg(test)]
mod tests {
    use std::convert::TryFrom;

    use serde_json::json;

    use crate::{
        tests::{
            fixtures::Identities,
            helpers::{dag_cbor_roundtrip, scaffold_ucan_builder},
        },
        Ucan,
    };

    use super::UcanIpld;

    #[cfg(target_arch = "wasm32")]
    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};

    #[cfg(target_arch = "wasm32")]
    wasm_bindgen_test_configure!(run_in_browser);

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_produces_canonical_jwt_despite_json_ambiguity() {
        let identities = Identities::new().await;
        let canon_builder = scaffold_ucan_builder(&identities).await.unwrap();
        let other_builder = scaffold_ucan_builder(&identities).await.unwrap();

        let canon_jwt = canon_builder
            .with_fact(
                "abc/challenge",
                json!({
                    "baz": true,
                    "foo": "bar"
                }),
            )
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap()
            .encode()
            .unwrap();

        let other_jwt = other_builder
            .with_fact(
                "abc/challenge",
                json!({
                    "foo": "bar",
                    "baz": true
                }),
            )
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap()
            .encode()
            .unwrap();

        assert_eq!(canon_jwt, other_jwt);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
    async fn it_stays_canonical_when_converting_between_jwt_and_ipld() {
        let identities = Identities::new().await;
        let builder = scaffold_ucan_builder(&identities).await.unwrap();

        let jwt = builder
            .with_fact(
                "abc/challenge",
                json!({
                    "baz": true,
                    "foo": "bar"
                }),
            )
            .with_nonce()
            .build()
            .unwrap()
            .sign()
            .await
            .unwrap()
            .encode()
            .unwrap();

        let ucan = Ucan::try_from(jwt.as_str()).unwrap();
        let ucan_ipld = UcanIpld::try_from(&ucan).unwrap();

        let decoded_ucan_ipld = dag_cbor_roundtrip(&ucan_ipld).unwrap();

        let decoded_ucan = Ucan::try_from(&decoded_ucan_ipld).unwrap();

        let decoded_jwt = decoded_ucan.encode().unwrap();

        assert_eq!(jwt, decoded_jwt);
    }
}