use crate::{
dht::Validator,
utils::{generate_capability, generate_fact},
};
use anyhow;
use async_trait::async_trait;
use cid::Cid;
use noosphere_core::{
authority::{SPHERE_SEMANTICS, SUPPORTED_KEYS},
data::Did,
view::SPHERE_LIFETIME,
};
use serde::{
de::{self, Deserialize, Deserializer},
ser::{self, Serialize, Serializer},
};
use serde_json::Value;
use std::{convert::TryFrom, fmt::Display, str, str::FromStr};
use ucan::{
builder::UcanBuilder,
crypto::KeyMaterial,
store::{UcanJwtStore, UcanStore},
};
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, anyhow::Error> {
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())
}
#[instrument(skip(store, opt_did_parser), level = "trace")]
pub async fn validate<S: UcanJwtStore>(
&self,
store: &S,
opt_did_parser: Option<&mut DidParser>,
) -> Result<(), NsRecordError> {
if self.is_expired() {
return Err(NsRecordError::Expired);
}
if self.link.is_none() {
return Err(NsRecordError::MissingLink);
}
let mut fallback_did_parser = if opt_did_parser.is_none() {
Some(DidParser::new(SUPPORTED_KEYS))
} else {
None
};
let did_parser: &mut DidParser = if let Some(provided_parser) = opt_did_parser {
provided_parser
} else {
fallback_did_parser.as_mut().unwrap()
};
let identity = self.identity();
let proof = ProofChain::from_ucan(self.token.clone(), did_parser, store).await?;
{
let desired_capability = generate_capability(identity);
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(NsRecordError::Unauthorized);
}
}
self.token
.check_signature(did_parser)
.await
.map_err(|_| NsRecordError::InvalidSignature)?;
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, anyhow::Error> {
self.token.encode()
}
}
impl Display for NsRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let link = self
.link
.and_then(|cid| Some(cid.to_string()))
.unwrap_or_else(|| String::from("None"));
write!(
f,
"NsRecord {{\n \"sphere\": \"{}\",\n \"link\": \"{}\"\n}}",
self.token.audience(),
link
)
}
}
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 From<NsRecord> for Ucan {
fn from(record: NsRecord) -> Self {
record.token
}
}
impl TryFrom<Vec<u8>> for NsRecord {
type Error = anyhow::Error;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
NsRecord::try_from(&bytes[..])
}
}
impl TryFrom<NsRecord> for Vec<u8> {
type Error = anyhow::Error;
fn try_from(record: NsRecord) -> Result<Self, Self::Error> {
Vec::try_from(&record)
}
}
impl TryFrom<&NsRecord> for Vec<u8> {
type Error = anyhow::Error;
fn try_from(record: &NsRecord) -> Result<Vec<u8>, Self::Error> {
Ok(Vec::from(record.token.encode()?))
}
}
impl TryFrom<&[u8]> for NsRecord {
type Error = anyhow::Error;
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 = anyhow::Error;
fn try_from(ucan_token: &str) -> Result<Self, Self::Error> {
NsRecord::from_str(ucan_token)
}
}
impl TryFrom<String> for NsRecord {
type Error = anyhow::Error;
fn try_from(ucan_token: String) -> Result<Self, Self::Error> {
NsRecord::from_str(ucan_token.as_str())
}
}
impl TryFrom<NsRecord> for String {
type Error = anyhow::Error;
fn try_from(record: NsRecord) -> Result<String, Self::Error> {
record.try_to_string()
}
}
impl FromStr for NsRecord {
type Err = anyhow::Error;
fn from_str(ucan_token: &str) -> Result<Self, Self::Err> {
Ok(NsRecord::new(Ucan::from_str(ucan_token)?))
}
}
#[derive(thiserror::Error, Debug)]
pub enum NsRecordError {
#[error("Token is expired.")]
Expired,
#[error("Token is unauthorized to publish a record for the sphere.")]
Unauthorized,
#[error("Token does not contain a \"fact\" entry with sphere revision.")]
MissingLink,
#[error("Token was not signed by stated issuer.")]
InvalidSignature,
#[error("{0}")]
Other(anyhow::Error),
}
impl From<anyhow::Error> for NsRecordError {
fn from(error: anyhow::Error) -> Self {
NsRecordError::Other(error)
}
}
pub(crate) struct RecordValidator<S: UcanStore> {
store: S,
did_parser: DidParser,
}
impl<S> RecordValidator<S>
where
S: UcanStore,
{
pub fn new(store: S) -> Self {
RecordValidator {
store,
did_parser: DidParser::new(SUPPORTED_KEYS),
}
}
}
#[async_trait]
impl<S> Validator for RecordValidator<S>
where
S: UcanStore,
{
async fn validate(&mut self, record_value: &[u8]) -> bool {
if let Ok(record) = NsRecord::try_from(record_value) {
if let Err(error) = record
.validate(&self.store, Some(&mut self.did_parser))
.await
{
warn!("Validation error: {}", error);
return false;
} else {
return true;
}
}
return false;
}
}
#[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>, ucan: Ucan) {
assert!(
NsRecord::new(ucan).validate(store, None).await.is_err(),
"{}",
message
);
}
#[tokio::test]
async fn test_nsrecord_self_signed() -> Result<(), anyhow::Error> {
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, None).await?;
Ok(())
}
#[tokio::test]
async fn test_nsrecord_delegated() -> Result<(), anyhow::Error> {
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, Some(&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, Some(&mut did_parser)).await?;
Ok(())
}
#[tokio::test]
async fn test_nsrecord_failures() -> Result<(), anyhow::Error> {
let sphere_key = generate_ed25519_key();
let sphere_identity = Did::from(sphere_key.get_did().await?);
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,
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,
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,
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<(), anyhow::Error> {
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(())
}
}