use crate::{
capability::CapabilityIpld,
crypto::did::DidParser,
serde::{Base64Encode, DagJson},
time::now,
};
use anyhow::{anyhow, Result};
use base64::Engine;
use cid::{
multihash::{Code, MultihashDigest},
Cid,
};
use libipld_core::{codec::Codec, raw::RawCodec};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{convert::TryFrom, str::FromStr};
pub const UCAN_VERSION: &str = "0.9.0-canary";
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UcanHeader {
pub alg: String,
pub typ: String,
pub ucv: String,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UcanPayload {
pub iss: String,
pub aud: String,
pub exp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nnc: Option<String>,
pub att: Vec<CapabilityIpld>,
pub fct: Vec<Value>,
pub prf: Vec<String>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Ucan {
header: UcanHeader,
payload: UcanPayload,
signed_data: Vec<u8>,
signature: Vec<u8>,
}
impl Ucan {
pub fn new(
header: UcanHeader,
payload: UcanPayload,
signed_data: Vec<u8>,
signature: Vec<u8>,
) -> Self {
Ucan {
signed_data,
header,
payload,
signature,
}
}
pub async fn validate<'a>(&self, did_parser: &mut DidParser) -> Result<()> {
if self.is_expired() {
return Err(anyhow!("Expired"));
}
if self.is_too_early() {
return Err(anyhow!("Not active yet (too early)"));
}
self.check_signature(did_parser).await
}
pub async fn check_signature<'a>(&self, did_parser: &mut DidParser) -> Result<()> {
let key = did_parser.parse(&self.payload.iss)?;
key.verify(&self.signed_data, &self.signature).await
}
pub fn encode(&self) -> Result<String> {
let header = self.header.jwt_base64_encode()?;
let payload = self.payload.jwt_base64_encode()?;
let signature =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.signature.as_slice());
Ok(format!("{header}.{payload}.{signature}"))
}
pub fn is_expired(&self) -> bool {
self.payload.exp < now()
}
pub fn signed_data(&self) -> &[u8] {
&self.signed_data
}
pub fn signature(&self) -> &[u8] {
&self.signature
}
pub fn is_too_early(&self) -> bool {
match self.payload.nbf {
Some(nbf) => nbf > now(),
None => false,
}
}
pub fn lifetime_begins_before(&self, other: &Ucan) -> bool {
match (self.payload.nbf, other.payload.nbf) {
(Some(nbf), Some(other_nbf)) => nbf <= other_nbf,
(Some(_), None) => false,
_ => true,
}
}
pub fn lifetime_ends_after(&self, other: &Ucan) -> bool {
self.payload.exp >= other.payload.exp
}
pub fn lifetime_encompasses(&self, other: &Ucan) -> bool {
self.lifetime_begins_before(other) && self.lifetime_ends_after(other)
}
pub fn algorithm(&self) -> &str {
&self.header.alg
}
pub fn issuer(&self) -> &str {
&self.payload.iss
}
pub fn audience(&self) -> &str {
&self.payload.aud
}
pub fn proofs(&self) -> &Vec<String> {
&self.payload.prf
}
pub fn expires_at(&self) -> &u64 {
&self.payload.exp
}
pub fn not_before(&self) -> &Option<u64> {
&self.payload.nbf
}
pub fn nonce(&self) -> &Option<String> {
&self.payload.nnc
}
pub fn attenuation(&self) -> &Vec<CapabilityIpld> {
&self.payload.att
}
pub fn facts(&self) -> &Vec<Value> {
&self.payload.fct
}
pub fn version(&self) -> &str {
&self.header.ucv
}
}
impl TryFrom<&Ucan> for Cid {
type Error = anyhow::Error;
fn try_from(value: &Ucan) -> Result<Self, Self::Error> {
let codec = RawCodec::default();
let token = value.encode()?;
let encoded = codec.encode(token.as_bytes())?;
Ok(Cid::new_v1(codec.into(), Code::Blake2b256.digest(&encoded)))
}
}
impl TryFrom<Ucan> for Cid {
type Error = anyhow::Error;
fn try_from(value: Ucan) -> Result<Self, Self::Error> {
Cid::try_from(&value)
}
}
impl<'a> TryFrom<&'a str> for Ucan {
type Error = anyhow::Error;
fn try_from(ucan_token: &str) -> Result<Self, Self::Error> {
Ucan::from_str(ucan_token)
}
}
impl TryFrom<String> for Ucan {
type Error = anyhow::Error;
fn try_from(ucan_token: String) -> Result<Self, Self::Error> {
Ucan::from_str(ucan_token.as_str())
}
}
impl FromStr for Ucan {
type Err = anyhow::Error;
fn from_str(ucan_token: &str) -> Result<Self, Self::Err> {
let signed_data = ucan_token
.split('.')
.take(2)
.map(String::from)
.reduce(|l, r| format!("{l}.{r}"))
.ok_or_else(|| anyhow!("Could not parse signed data from token string"))?;
let mut parts = ucan_token.split('.').map(|str| {
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(str)
.map_err(|error| anyhow!(error))
});
let header = parts
.next()
.ok_or_else(|| anyhow!("Missing UCAN header in token part"))?
.map(|decoded| UcanHeader::from_dag_json(&decoded))
.map_err(|e| e.context("Could not decode UCAN header base64"))?
.map_err(|e| e.context("Could not parse UCAN header JSON"))?;
let payload = parts
.next()
.ok_or_else(|| anyhow!("Missing UCAN payload in token part"))?
.map(|decoded| UcanPayload::from_dag_json(&decoded))
.map_err(|e| e.context("Could not decode UCAN payload base64"))?
.map_err(|e| e.context("Could not parse UCAN payload JSON"))?;
let signature = parts
.next()
.ok_or_else(|| anyhow!("Missing UCAN signature in token part"))?
.map_err(|e| e.context("Could not parse UCAN signature base64"))?;
Ok(Ucan::new(
header,
payload,
signed_data.as_bytes().into(),
signature,
))
}
}