use crate::utils::{generate_capability, generate_fact};
use anyhow::{anyhow, Error as AnyhowError};
use cid::Cid;
use noosphere_core::{authority::SPHERE_SEMANTICS, data::Did, view::SPHERE_LIFETIME};
use noosphere_storage::{SphereDb, Storage};
use serde::{
de::{self, Deserialize, Deserializer},
ser::{self, Serialize, Serializer},
};
use serde_json::Value;
use std::{convert::TryFrom, str, str::FromStr};
use ucan::{builder::UcanBuilder, crypto::KeyMaterial};
use ucan::{chain::ProofChain, crypto::did::DidParser, Ucan};
#[derive(Debug, Clone)]
pub struct NsRecord {
pub(crate) token: Ucan,
pub(crate) link: Option<Cid>,
}
impl NsRecord {
pub fn new(token: Ucan) -> Self {
let mut link = None;
for ref fact in token.facts() {
if let Value::Object(map) = fact {
if let Some(Value::String(addr)) = map.get(&String::from("link")) {
if let Ok(cid) = Cid::from_str(addr) {
link = Some(cid);
break;
}
}
}
}
Self { token, link }
}
pub async fn from_issuer<K: KeyMaterial>(
issuer: &K,
sphere_id: &Did,
link: &Cid,
proofs: Option<&Vec<Ucan>>,
) -> Result<NsRecord, AnyhowError> {
let capability = generate_capability(sphere_id);
let fact = generate_fact(&link.to_string());
let mut builder = UcanBuilder::default()
.issued_by(issuer)
.for_audience(sphere_id)
.claiming_capability(&capability)
.with_fact(fact);
if let Some(proofs) = proofs {
let mut earliest_expiry: u64 = u64::MAX;
for token in proofs {
earliest_expiry = *token.expires_at().min(&earliest_expiry);
builder = builder.witnessed_by(token);
}
builder = builder.with_expiration(earliest_expiry);
} else {
builder = builder.with_lifetime(SPHERE_LIFETIME);
}
Ok(builder.build()?.sign().await?.into())
}
pub async fn validate<S: Storage>(
&self,
store: &SphereDb<S>,
did_parser: &mut DidParser,
) -> Result<(), AnyhowError> {
if self.is_expired() {
return Err(anyhow!("Token is expired."));
}
let identity = self.identity();
let desired_capability = generate_capability(identity);
let proof = ProofChain::from_ucan(self.token.clone(), did_parser, store).await?;
let mut has_capability = false;
for capability_info in proof.reduce_capabilities(&SPHERE_SEMANTICS) {
let capability = capability_info.capability;
if capability_info.originators.contains(identity)
&& capability.enables(&desired_capability)
{
has_capability = true;
break;
}
}
if !has_capability {
return Err(anyhow!("Token is not authorized to publish this sphere."));
}
if self.link.is_none() {
return Err(anyhow!(
"Missing a valid fact entry with record sphere revision. {} {:?}",
identity,
self.token.facts()
));
}
self.token.check_signature(did_parser).await?;
Ok(())
}
pub fn identity(&self) -> &str {
self.token.audience()
}
pub fn link(&self) -> Option<&Cid> {
self.link.as_ref()
}
pub fn is_expired(&self) -> bool {
self.token.is_expired()
}
pub fn try_to_string(&self) -> Result<String, AnyhowError> {
self.token.encode()
}
}
impl Serialize for NsRecord {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let encoded = self.try_to_string().map_err(ser::Error::custom)?;
serializer.serialize_str(&encoded)
}
}
impl<'de> Deserialize<'de> for NsRecord {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let record = NsRecord::try_from(s).map_err(de::Error::custom)?;
Ok(record)
}
}
impl From<Ucan> for NsRecord {
fn from(ucan: Ucan) -> Self {
Self::new(ucan)
}
}
impl TryFrom<Vec<u8>> for NsRecord {
type Error = AnyhowError;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
NsRecord::try_from(&bytes[..])
}
}
impl TryFrom<NsRecord> for Vec<u8> {
type Error = AnyhowError;
fn try_from(record: NsRecord) -> Result<Self, Self::Error> {
Vec::try_from(&record)
}
}
impl TryFrom<&NsRecord> for Vec<u8> {
type Error = AnyhowError;
fn try_from(record: &NsRecord) -> Result<Vec<u8>, Self::Error> {
Ok(Vec::from(record.token.encode()?))
}
}
impl TryFrom<&[u8]> for NsRecord {
type Error = AnyhowError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
NsRecord::try_from(str::from_utf8(bytes)?)
}
}
impl<'a> TryFrom<&'a str> for NsRecord {
type Error = AnyhowError;
fn try_from(ucan_token: &str) -> Result<Self, Self::Error> {
NsRecord::from_str(ucan_token)
}
}
impl TryFrom<String> for NsRecord {
type Error = AnyhowError;
fn try_from(ucan_token: String) -> Result<Self, Self::Error> {
NsRecord::from_str(ucan_token.as_str())
}
}
impl TryFrom<NsRecord> for String {
type Error = AnyhowError;
fn try_from(record: NsRecord) -> Result<String, Self::Error> {
record.try_to_string()
}
}
impl FromStr for NsRecord {
type Err = AnyhowError;
fn from_str(ucan_token: &str) -> Result<Self, Self::Err> {
Ok(NsRecord::new(Ucan::from_str(ucan_token)?))
}
}
#[cfg(test)]
mod test {
use super::*;
use noosphere_core::{
authority::{generate_ed25519_key, SUPPORTED_KEYS},
data::Did,
};
use noosphere_storage::{MemoryStorage, SphereDb};
use serde_json::json;
use std::str::FromStr;
use ucan::{
builder::UcanBuilder, crypto::did::DidParser, crypto::KeyMaterial, store::UcanJwtStore,
};
async fn expect_failure(
message: &str,
store: &SphereDb<MemoryStorage>,
did_parser: &mut DidParser,
ucan: Ucan,
) {
assert!(
NsRecord::new(ucan)
.validate(store, did_parser)
.await
.is_err(),
"{}",
message
);
}
#[tokio::test]
async fn test_nsrecord_self_signed() -> Result<(), AnyhowError> {
let sphere_key = generate_ed25519_key();
let sphere_identity = Did::from(sphere_key.get_did().await?);
let link = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72";
let cid_link: Cid = link.parse()?;
let store = SphereDb::new(&MemoryStorage::default()).await.unwrap();
let record = NsRecord::from_issuer(&sphere_key, &sphere_identity, &cid_link, None).await?;
assert_eq!(&Did::from(record.identity()), &sphere_identity);
assert_eq!(record.link(), Some(&cid_link));
record
.validate(&store, &mut DidParser::new(SUPPORTED_KEYS))
.await?;
Ok(())
}
#[tokio::test]
async fn test_nsrecord_delegated() -> Result<(), AnyhowError> {
let owner_key = generate_ed25519_key();
let owner_identity = Did::from(owner_key.get_did().await?);
let sphere_key = generate_ed25519_key();
let sphere_identity = Did::from(sphere_key.get_did().await?);
let mut did_parser = DidParser::new(SUPPORTED_KEYS);
let link = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72";
let cid_link: Cid = link.parse()?;
let mut store = SphereDb::new(&MemoryStorage::default()).await.unwrap();
let record = NsRecord::from_issuer(&owner_key, &sphere_identity, &cid_link, None).await?;
assert_eq!(record.identity(), &sphere_identity);
assert_eq!(record.link(), Some(&cid_link));
if record.validate(&store, &mut did_parser).await.is_ok() {
panic!("Owner should not have authorization to publish record")
}
let delegate_capability = generate_capability(&sphere_identity);
let delegate_ucan = UcanBuilder::default()
.issued_by(&sphere_key)
.for_audience(&owner_identity)
.with_lifetime(SPHERE_LIFETIME)
.claiming_capability(&delegate_capability)
.build()?
.sign()
.await?;
let _ = store.write_token(&delegate_ucan.encode()?).await?;
let proofs = vec![delegate_ucan];
let record =
NsRecord::from_issuer(&owner_key, &sphere_identity, &cid_link, Some(&proofs)).await?;
assert_eq!(record.identity(), &sphere_identity);
assert_eq!(record.link(), Some(&cid_link));
record.validate(&store, &mut did_parser).await?;
Ok(())
}
#[tokio::test]
async fn test_nsrecord_failures() -> Result<(), AnyhowError> {
let sphere_key = generate_ed25519_key();
let sphere_identity = Did::from(sphere_key.get_did().await?);
let mut did_parser = DidParser::new(SUPPORTED_KEYS);
let cid_address = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72";
let store = SphereDb::new(&MemoryStorage::default()).await.unwrap();
let sphere_capability = generate_capability(&sphere_identity);
expect_failure(
"fails when expect `fact` is missing",
&store,
&mut did_parser,
UcanBuilder::default()
.issued_by(&sphere_key)
.for_audience(&sphere_identity)
.with_lifetime(1000)
.claiming_capability(&sphere_capability)
.with_fact(json!({ "invalid_fact": cid_address }))
.build()?
.sign()
.await?,
)
.await;
let capability = generate_capability(&Did(generate_ed25519_key().get_did().await?));
expect_failure(
"fails when capability resource does not match sphere identity",
&store,
&mut did_parser,
UcanBuilder::default()
.issued_by(&sphere_key)
.for_audience(&sphere_identity)
.with_lifetime(1000)
.claiming_capability(&capability)
.with_fact(generate_fact(cid_address))
.build()?
.sign()
.await?,
)
.await;
let non_auth_key = generate_ed25519_key();
expect_failure(
"fails when a non-authorized key signs the record",
&store,
&mut did_parser,
UcanBuilder::default()
.issued_by(&non_auth_key)
.for_audience(&sphere_identity)
.with_lifetime(1000)
.claiming_capability(&sphere_capability)
.with_fact(generate_fact(cid_address))
.build()?
.sign()
.await?,
)
.await;
Ok(())
}
#[tokio::test]
async fn test_nsrecord_convert() -> Result<(), AnyhowError> {
let sphere_key = generate_ed25519_key();
let sphere_identity = Did::from(sphere_key.get_did().await?);
let capability = generate_capability(&sphere_identity);
let cid_address = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72";
let fact = generate_fact(cid_address);
let ucan = UcanBuilder::default()
.issued_by(&sphere_key)
.for_audience(&sphere_identity)
.with_lifetime(1000)
.claiming_capability(&capability)
.with_fact(fact)
.build()?
.sign()
.await?;
let base = NsRecord::new(ucan.clone());
let encoded = ucan.encode()?;
let bytes = Vec::from(encoded.clone());
let serialized = serde_json::to_string(&base)?;
assert_eq!(format!("\"{}\"", encoded), serialized, "serialize()");
let record: NsRecord = serde_json::from_str(&serialized)?;
assert_eq!(base.identity(), record.identity(), "deserialize()");
assert_eq!(base.link(), record.link(), "deserialize()");
let record = NsRecord::try_from(bytes.clone())?;
assert_eq!(base.identity(), record.identity(), "try_from::<Vec<u8>>()");
assert_eq!(base.link(), record.link(), "try_from::<Vec<u8>>()");
let rec_bytes: Vec<u8> = base.clone().try_into()?;
assert_eq!(bytes, rec_bytes, "try_into::<Vec<u8>>()");
let record = NsRecord::try_from(&bytes[..])?;
assert_eq!(base.identity(), record.identity(), "try_from::<&[u8]>()");
assert_eq!(base.link(), record.link(), "try_from::<&[u8]>()");
let rec_bytes: Vec<u8> = (&base).try_into()?;
assert_eq!(bytes, rec_bytes, "&NsRecord::try_into::<Vec<u8>>()");
let record = NsRecord::from(ucan);
assert_eq!(base.identity(), record.identity(), "from::<Ucan>()");
assert_eq!(base.link(), record.link(), "from::<Ucan>()");
let record = NsRecord::try_from(encoded.as_str())?;
assert_eq!(base.identity(), record.identity(), "try_from::<&str>()");
assert_eq!(base.link(), record.link(), "try_from::<&str>()");
let record = NsRecord::try_from(encoded.clone())?;
assert_eq!(base.identity(), record.identity(), "try_from::<String>()");
assert_eq!(base.link(), record.link(), "try_from::<String>()");
let record = NsRecord::from_str(encoded.as_str())?;
assert_eq!(base.identity(), record.identity(), "from_str()");
assert_eq!(base.link(), record.link(), "from_str()");
Ok(())
}
}