From 3c47e2eff0d61197ccbe1e98e33fc16e03590c79 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Thu, 18 May 2023 13:31:49 -0700 Subject: [PATCH] feat: Promote NsRecord into a core object by extending noosphere_core::data::LinkRecord with its functionality, and removing the noosphere_ns dependency on validating records. Fixes #395 --- Cargo.lock | 1 + Cargo.toml | 3 +- rust/noosphere-api/Cargo.toml | 2 +- rust/noosphere-cli/Cargo.toml | 4 +- rust/noosphere-collections/Cargo.toml | 4 +- rust/noosphere-core/Cargo.toml | 3 +- .../src/authority/capability.rs | 33 +- rust/noosphere-core/src/data/address.rs | 525 +++++++++++++- rust/noosphere-gateway/Cargo.toml | 4 +- rust/noosphere-gateway/src/route/push.rs | 4 +- .../src/worker/name_system.rs | 71 +- rust/noosphere-ipfs/Cargo.toml | 4 +- rust/noosphere-ns/Cargo.toml | 4 +- .../src/bin/orb-ns/cli/cli_implementation.rs | 6 +- rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs | 27 +- rust/noosphere-ns/src/dht_client.rs | 46 +- rust/noosphere-ns/src/helpers.rs | 15 +- rust/noosphere-ns/src/lib.rs | 3 +- rust/noosphere-ns/src/name_resolver.rs | 44 +- rust/noosphere-ns/src/name_system.rs | 12 +- rust/noosphere-ns/src/records.rs | 648 ------------------ rust/noosphere-ns/src/server/client.rs | 8 +- rust/noosphere-ns/src/server/handlers.rs | 8 +- rust/noosphere-ns/src/utils.rs | 52 -- rust/noosphere-ns/src/validator.rs | 31 + rust/noosphere-ns/tests/ns_test.rs | 38 +- rust/noosphere-sphere/Cargo.toml | 4 +- rust/noosphere-sphere/src/context.rs | 69 +- rust/noosphere-sphere/src/helpers.rs | 52 +- rust/noosphere-sphere/src/petname/read.rs | 2 +- rust/noosphere-sphere/src/petname/write.rs | 13 +- rust/noosphere-sphere/src/sync/gateway.rs | 2 +- rust/noosphere-storage/Cargo.toml | 2 +- 33 files changed, 806 insertions(+), 938 deletions(-) delete mode 100644 rust/noosphere-ns/src/records.rs create mode 100644 rust/noosphere-ns/src/validator.rs diff --git a/Cargo.lock b/Cargo.lock index 383624ec8..db24b4cb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3926,6 +3926,7 @@ dependencies = [ "serde", "serde_bytes", "serde_ipld_dagcbor", + "serde_json", "strum", "strum_macros", "tiny-bip39", diff --git a/Cargo.toml b/Cargo.toml index 6907cf739..086012e85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,8 @@ void = { version = "1" } wnfs-namefilter = { version = "0.1.20" } strum = { version = "0.24" } strum_macros = { version = "0.24" } - +serde = { version = "^1" } +serde_json = { version = "^1" } [profile.release] opt-level = 'z' diff --git a/rust/noosphere-api/Cargo.toml b/rust/noosphere-api/Cargo.toml index a79701e0d..bca5df8a9 100644 --- a/rust/noosphere-api/Cargo.toml +++ b/rust/noosphere-api/Cargo.toml @@ -23,7 +23,7 @@ anyhow = "^1" thiserror = { workspace = true } cid = { workspace = true } url = "^2" -serde = "^1" +serde = { workspace = true } serde_urlencoded = "~0.7" tracing = { workspace = true } noosphere-core = { version = "0.11.0", path = "../noosphere-core" } diff --git a/rust/noosphere-cli/Cargo.toml b/rust/noosphere-cli/Cargo.toml index d994334f5..e55a58ebe 100644 --- a/rust/noosphere-cli/Cargo.toml +++ b/rust/noosphere-cli/Cargo.toml @@ -61,8 +61,8 @@ ucan-key-support = { workspace = true } cid = { workspace = true } subtext = "0.3.2" -serde = "^1" -serde_json = "^1" +serde = { workspace = true } +serde_json = { workspace = true } libipld-core = { workspace = true } libipld-cbor = { workspace = true } diff --git a/rust/noosphere-collections/Cargo.toml b/rust/noosphere-collections/Cargo.toml index baeedcd7b..647707f46 100644 --- a/rust/noosphere-collections/Cargo.toml +++ b/rust/noosphere-collections/Cargo.toml @@ -21,7 +21,7 @@ anyhow = "^1" sha2 = "0.10" cid = { workspace = true } forest_hash_utils = "0.1.0" -serde = "^1" +serde = { workspace = true } serde_bytes = "0.11" serde_ipld_dagcbor = "0.2" byteorder = "^1.4" @@ -48,4 +48,4 @@ tokio = { version = "^1", features = ["full"] } wasm-bindgen-test = "0.3" [features] -identity = [] \ No newline at end of file +identity = [] diff --git a/rust/noosphere-core/Cargo.toml b/rust/noosphere-core/Cargo.toml index b770bcbe8..995711cf5 100644 --- a/rust/noosphere-core/Cargo.toml +++ b/rust/noosphere-core/Cargo.toml @@ -33,7 +33,7 @@ anyhow = "^1" fastcdc = "3" futures = "~0.3" fvm_ipld_amt = "~0.5" -serde = "^1" +serde = { workspace = true } byteorder = "^1.4" base64 = "0.21" ed25519-zebra = "^3" @@ -56,6 +56,7 @@ ucan-key-support = { workspace = true } [dev-dependencies] wasm-bindgen-test = "~0.3" serde_bytes = "~0.11" +serde_json = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { version = "^1", features = ["full"] } diff --git a/rust/noosphere-core/src/authority/capability.rs b/rust/noosphere-core/src/authority/capability.rs index 91f599023..d1b4a0b75 100644 --- a/rust/noosphere-core/src/authority/capability.rs +++ b/rust/noosphere-core/src/authority/capability.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use ucan::capability::{Action, CapabilitySemantics, Scope}; +use ucan::capability::{Action, Capability, CapabilitySemantics, Resource, Scope, With}; use url::Url; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] @@ -80,3 +80,34 @@ pub struct SphereSemantics {} impl CapabilitySemantics for SphereSemantics {} pub const SPHERE_SEMANTICS: SphereSemantics = SphereSemantics {}; + +/// Generates a [Capability] struct representing permissions in a [LinkRecord]. +/// +/// ``` +/// use noosphere_core::{authority::{generate_capability, SphereAction, SphereReference}}; +/// use ucan::capability::{Capability, Resource, With}; +/// +/// let identity = "did:key:z6MkoE19WHXJzpLqkxbGP7uXdJX38sWZNUWwyjcuCmjhPpUP"; +/// let expected_capability = Capability { +/// with: With::Resource { +/// kind: Resource::Scoped(SphereReference { +/// did: identity.to_owned(), +/// }), +/// }, +/// can: SphereAction::Publish, +/// }; +/// assert_eq!(generate_capability(&identity, SphereAction::Publish), expected_capability); +/// ``` +pub fn generate_capability( + identity: &str, + action: SphereAction, +) -> Capability { + Capability { + with: With::Resource { + kind: Resource::Scoped(SphereReference { + did: identity.to_owned(), + }), + }, + can: action, + } +} diff --git a/rust/noosphere-core/src/data/address.rs b/rust/noosphere-core/src/data/address.rs index 268718142..772fdcdc1 100644 --- a/rust/noosphere-core/src/data/address.rs +++ b/rust/noosphere-core/src/data/address.rs @@ -1,10 +1,11 @@ +use crate::authority::{generate_capability, SphereAction, SPHERE_SEMANTICS, SUPPORTED_KEYS}; use anyhow::Result; use cid::Cid; use libipld_cbor::DagCborCodec; use noosphere_storage::BlockStore; -use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fmt::Display, ops::Deref}; -use ucan::{store::UcanJwtStore, Ucan}; +use serde::{de, ser, Deserialize, Serialize}; +use std::{convert::TryFrom, fmt::Display, ops::Deref, str::FromStr}; +use ucan::{chain::ProofChain, crypto::did::DidParser, store::UcanJwtStore, Ucan}; use super::{Did, IdentitiesIpld, Jwt, Link}; @@ -45,33 +46,86 @@ impl IdentityIpld { /// it from storage pub async fn link_record(&self, store: &S) -> Option { match &self.link_record { - Some(cid) => store - .read_token(cid) - .await - .unwrap_or(None) - .map(|jwt| LinkRecord(jwt.into())), + Some(cid) => match store.read_token(cid).await.unwrap_or(None) { + Some(jwt) => LinkRecord::from_str(&jwt).ok(), + None => None, + }, _ => None, } } } -/// A [LinkRecord] is a newtype that represents a JWT that ought to contain a -/// [Cid] reference to a sphere -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, Hash)] -#[serde(from = "Jwt", into = "Jwt")] +/// A [LinkRecord] is a wrapper around a decoded [Jwt] ([Ucan]), +/// representing a link address as a [Cid] to a sphere. +#[derive(Debug, Clone)] #[repr(transparent)] -pub struct LinkRecord(Jwt); +pub struct LinkRecord(Ucan); impl LinkRecord { - /// Parse the wrapped [Jwt] as a [Ucan] and looks for the referenced pointer - /// to some data in IPFS (via a [Cid] in the `fct` field). - pub async fn dereference(&self) -> Option { + /// Validates the [Ucan] token as a [LinkRecord], ensuring that + /// the sphere's owner authorized the publishing of a new + /// content address. Notably does not check the publishing timeframe + /// permissions, as an expired token can be considered valid. + /// Returns an `Err` if validation fails. + pub async fn validate(&self, store: &S) -> Result<()> { + let identity = self.sphere_identity(); let token = &self.0; - let ucan = match Ucan::try_from(token.to_string()) { - Ok(ucan) => ucan, - _ => return None, + + if self.get_link().is_none() { + return Err(anyhow::anyhow!("LinkRecord missing link.")); + } + + let mut did_parser = DidParser::new(SUPPORTED_KEYS); + + // We're interested in the validity of the proof at the time + // of publishing. + let now_time = if let Some(nbf) = token.not_before() { + nbf.to_owned() + } else { + token.expires_at() - 1 }; - let facts = ucan.facts(); + + let proof = + ProofChain::from_ucan(token.to_owned(), Some(now_time), &mut did_parser, store).await?; + + { + let desired_capability = generate_capability(identity, SphereAction::Publish); + 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::anyhow!("LinkRecord is not authorized.")); + } + } + + token + .check_signature(&mut did_parser) + .await + .map(|_| ()) + .map_err(|_| anyhow::anyhow!("LinkRecord has invalid signature.")) + } + + /// Returns true if the [Ucan] token is currently publishable + /// within the bounds of its expiry/not before time. + pub fn has_publishable_timeframe(&self) -> bool { + !self.0.is_expired(None) && !self.0.is_too_early() + } + + /// The DID key of the sphere that this record maps. + pub fn sphere_identity(&self) -> &str { + self.0.audience() + } + + /// The sphere revision address ([Cid]) that the sphere's identity maps to. + pub fn get_link(&self) -> Option { + let facts = self.0.facts(); for fact in facts { match fact.as_object() { @@ -99,15 +153,49 @@ impl LinkRecord { } } } - warn!("No facts contained a link!"); - None } } +impl ser::Serialize for LinkRecord { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + let encoded = self.encode().map_err(ser::Error::custom)?; + serializer.serialize_str(&encoded) + } +} + +impl<'de> de::Deserialize<'de> for LinkRecord { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let record = LinkRecord::try_from(s).map_err(de::Error::custom)?; + Ok(record) + } +} + +/// [LinkRecord]s compare their [Jwt] representations +/// for equality. If a record cannot be encoded as such, +/// they will not be considered equal to any other record. +impl PartialEq for LinkRecord { + fn eq(&self, other: &Self) -> bool { + if let Ok(encoded_a) = self.encode() { + if let Ok(encoded_b) = other.encode() { + return encoded_a == encoded_b; + } + } + false + } +} +impl Eq for LinkRecord {} + impl Deref for LinkRecord { - type Target = Jwt; + type Target = Ucan; fn deref(&self) -> &Self::Target { &self.0 @@ -116,18 +204,401 @@ impl Deref for LinkRecord { impl Display for LinkRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) + write!( + f, + "LinkRecord({}, {})", + self.sphere_identity(), + self.get_link() + .map_or_else(|| String::from("None"), String::from) + ) + } +} + +impl TryFrom<&Jwt> for LinkRecord { + type Error = anyhow::Error; + fn try_from(value: &Jwt) -> Result { + LinkRecord::from_str(value) + } +} + +impl TryFrom<&LinkRecord> for Jwt { + type Error = anyhow::Error; + fn try_from(value: &LinkRecord) -> Result { + Ok(Jwt(value.encode()?)) } } -impl From for LinkRecord { - fn from(value: Jwt) -> Self { +impl TryFrom for LinkRecord { + type Error = anyhow::Error; + fn try_from(value: Jwt) -> Result { + LinkRecord::try_from(&value) + } +} + +impl TryFrom for Jwt { + type Error = anyhow::Error; + fn try_from(value: LinkRecord) -> Result { + Jwt::try_from(&value) + } +} + +impl From<&Ucan> for LinkRecord { + fn from(value: &Ucan) -> Self { + LinkRecord::from(value.to_owned()) + } +} + +impl From<&LinkRecord> for Ucan { + fn from(value: &LinkRecord) -> Self { + value.0.clone() + } +} + +impl From for LinkRecord { + fn from(value: Ucan) -> Self { LinkRecord(value) } } -impl From for Jwt { +impl From for Ucan { fn from(value: LinkRecord) -> Self { value.0 } } + +impl TryFrom<&[u8]> for LinkRecord { + type Error = anyhow::Error; + fn try_from(value: &[u8]) -> Result { + LinkRecord::try_from(value.to_vec()) + } +} + +impl TryFrom> for LinkRecord { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + LinkRecord::from_str(&String::from_utf8(value)?) + } +} + +impl TryFrom for Vec { + type Error = anyhow::Error; + fn try_from(value: LinkRecord) -> Result { + Ok(value.encode()?.into_bytes()) + } +} + +impl FromStr for LinkRecord { + type Err = anyhow::Error; + fn from_str(value: &str) -> Result { + Ok(Ucan::from_str(value)?.into()) + } +} + +impl TryFrom for LinkRecord { + type Error = anyhow::Error; + fn try_from(value: String) -> Result { + Ok(Ucan::from_str(&value)?.into()) + } +} + +#[cfg(test)] +#[cfg(test)] +mod test { + use super::*; + use crate::{authority::generate_ed25519_key, data::Did, view::SPHERE_LIFETIME}; + use noosphere_storage::{MemoryStorage, SphereDb}; + use serde_json::json; + use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; + + pub async fn from_issuer( + issuer: &K, + sphere_id: &Did, + link: &Cid, + proofs: Option<&Vec>, + ) -> Result { + let capability = generate_capability(sphere_id, SphereAction::Publish); + let fact = json!({ "link": 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()) + } + + async fn expect_failure(message: &str, store: &SphereDb, record: LinkRecord) { + assert!(record.validate(store).await.is_err(), "{}", message); + } + + #[tokio::test] + async fn test_self_signed_link_record() -> Result<(), anyhow::Error> { + let sphere_key = generate_ed25519_key(); + let sphere_identity = Did::from(sphere_key.get_did().await?); + let link = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; + let cid_link: Cid = link.parse()?; + let store = SphereDb::new(&MemoryStorage::default()).await.unwrap(); + + let record = from_issuer(&sphere_key, &sphere_identity, &cid_link, None).await?; + + assert_eq!(&Did::from(record.sphere_identity()), &sphere_identity); + assert_eq!(LinkRecord::get_link(&record), Some(cid_link)); + LinkRecord::validate(&record, &store).await?; + Ok(()) + } + + #[tokio::test] + async fn test_delegated_link_record() -> 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 link = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; + let cid_link: Cid = link.parse()?; + let mut store = SphereDb::new(&MemoryStorage::default()).await.unwrap(); + + // First verify that `owner` cannot publish for `sphere` + // without delegation. + let record = from_issuer(&owner_key, &sphere_identity, &cid_link, None).await?; + + assert_eq!(record.sphere_identity(), &sphere_identity); + assert_eq!(record.get_link(), Some(cid_link.clone())); + if LinkRecord::validate(&record, &store).await.is_ok() { + panic!("Owner should not have authorization to publish record") + } + + // Delegate `sphere_key`'s publishing authority to `owner_key` + let delegate_ucan = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&owner_identity) + .with_lifetime(SPHERE_LIFETIME) + .claiming_capability(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .build()? + .sign() + .await?; + let _ = store.write_token(&delegate_ucan.encode()?).await?; + + // Attempt `owner` publishing `sphere` with the proper authorization. + let proofs = vec![delegate_ucan.clone()]; + let record = from_issuer(&owner_key, &sphere_identity, &cid_link, Some(&proofs)).await?; + + assert_eq!(record.sphere_identity(), &sphere_identity); + assert_eq!(record.get_link(), Some(cid_link.clone())); + assert!(LinkRecord::has_publishable_timeframe(&record)); + LinkRecord::validate(&record, &store).await?; + + // Now test a similar record that has an expired capability. + // It must still be valid. + let expired: LinkRecord = UcanBuilder::default() + .issued_by(&owner_key) + .for_audience(&sphere_identity) + .claiming_capability(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .with_fact(json!({ "link": &cid_link.to_string() })) + .witnessed_by(&delegate_ucan) + .with_expiration(ucan::time::now() - 1234) + .build()? + .sign() + .await? + .into(); + assert_eq!(expired.sphere_identity(), &sphere_identity); + assert_eq!(expired.get_link(), Some(cid_link)); + assert!(expired.has_publishable_timeframe() == false); + LinkRecord::validate(&record, &store).await?; + Ok(()) + } + + #[tokio::test] + async fn test_link_record_failures() -> Result<(), anyhow::Error> { + let sphere_key = generate_ed25519_key(); + let sphere_identity = Did::from(sphere_key.get_did().await?); + let cid_address = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; + let store = SphereDb::new(&MemoryStorage::default()).await.unwrap(); + + expect_failure( + "fails when expect `fact` is missing", + &store, + UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .with_lifetime(1000) + .claiming_capability(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .with_fact(json!({ "invalid_fact": cid_address })) + .build()? + .sign() + .await? + .into(), + ) + .await; + + let capability = generate_capability( + &Did(generate_ed25519_key().get_did().await?), + SphereAction::Publish, + ); + 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(json!({ "link": cid_address.clone() })) + .build()? + .sign() + .await? + .into(), + ) + .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(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .with_fact(json!({ "link": cid_address.clone() })) + .build()? + .sign() + .await? + .into(), + ) + .await; + + Ok(()) + } + + #[tokio::test] + async fn test_link_record_convert() -> Result<(), anyhow::Error> { + let sphere_key = generate_ed25519_key(); + let identity = Did::from(sphere_key.get_did().await?); + let capability = generate_capability(&identity, SphereAction::Publish); + let cid_address = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; + let link = Cid::from_str(cid_address)?; + let maybe_link = Some(link.clone()); + let fact = json!({ "link": cid_address }); + + let ucan = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&identity) + .with_lifetime(1000) + .claiming_capability(&capability) + .with_fact(fact) + .build()? + .sign() + .await?; + + let encoded = ucan.encode()?; + let base = LinkRecord::from(ucan.clone()); + + // from_str, String + { + let record: LinkRecord = encoded.parse()?; + assert_eq!(record.sphere_identity(), identity, "LinkRecord::from_str()"); + assert_eq!(record.get_link(), maybe_link, "LinkRecord::from_str()"); + let record: LinkRecord = String::from(encoded.clone()).try_into()?; + assert_eq!( + record.sphere_identity(), + identity, + "LinkRecord::try_from(String)" + ); + assert_eq!( + record.get_link(), + maybe_link, + "LinkRecord::try_from(String)" + ); + } + + // Ucan convert + { + let from_ucan_ref = LinkRecord::from(&ucan); + assert_eq!(base.sphere_identity(), identity, "LinkRecord::from(Ucan)"); + assert_eq!(base.get_link(), maybe_link, "LinkRecord::from(Ucan)"); + assert_eq!( + from_ucan_ref.sphere_identity(), + identity, + "LinkRecord::from(&Ucan)" + ); + assert_eq!( + from_ucan_ref.get_link(), + maybe_link, + "LinkRecord::from(&Ucan)" + ); + assert_eq!( + Ucan::from(base.clone()).encode()?, + encoded, + "Ucan::from(LinkRecord)" + ); + assert_eq!( + Ucan::from(&base).encode()?, + encoded, + "Ucan::from(&LinkRecord)" + ); + }; + + // Vec convert + { + let bytes = Vec::from(encoded.clone()); + let record = LinkRecord::try_from(bytes.clone())?; + assert_eq!( + record.sphere_identity(), + identity, + "LinkRecord::try_from(Vec)" + ); + assert_eq!( + record.get_link(), + maybe_link, + "LinkRecord::try_from(Vec)" + ); + + let record = LinkRecord::try_from(bytes.as_slice())?; + assert_eq!( + record.sphere_identity(), + identity, + "LinkRecord::try_from(&[u8])" + ); + assert_eq!(record.get_link(), maybe_link, "LinkRecord::try_from(&[u8])"); + + let bytes_from_record: Vec = record.try_into()?; + assert_eq!(bytes_from_record, bytes, "LinkRecord::try_into(Vec>)"); + }; + + // LinkRecord::serialize + // LinkRecord::deserialize + { + let serialized = serde_json::to_string(&base)?; + assert_eq!(serialized, format!("\"{}\"", encoded), "serialize()"); + let record: LinkRecord = serde_json::from_str(&serialized)?; + assert_eq!(record.sphere_identity(), identity, "deserialize()"); + assert_eq!(record.get_link(), maybe_link, "deserialize()"); + } + + Ok(()) + } +} diff --git a/rust/noosphere-gateway/Cargo.toml b/rust/noosphere-gateway/Cargo.toml index d259eab71..5c78440d1 100644 --- a/rust/noosphere-gateway/Cargo.toml +++ b/rust/noosphere-gateway/Cargo.toml @@ -61,8 +61,8 @@ ucan-key-support = { workspace = true } cid = { workspace = true } subtext = "0.3.2" -serde = "^1" -serde_json = "^1" +serde = { workspace = true } +serde_json = { workspace = true } libipld-core = { workspace = true } libipld-cbor = { workspace = true } diff --git a/rust/noosphere-gateway/src/route/push.rs b/rust/noosphere-gateway/src/route/push.rs index d501637de..776f47cec 100644 --- a/rust/noosphere-gateway/src/route/push.rs +++ b/rust/noosphere-gateway/src/route/push.rs @@ -8,7 +8,7 @@ use cid::Cid; use noosphere_api::data::{PushBody, PushError, PushResponse}; use noosphere_core::{ authority::{SphereAction, SphereReference}, - data::{Bundle, MapOperation}, + data::{Bundle, LinkRecord, MapOperation}, view::Sphere, }; use noosphere_sphere::{HasMutableSphereContext, SphereContentWrite, SphereCursor}; @@ -311,7 +311,7 @@ where if let Some(name_record) = &self.request_body.name_record { if let Err(error) = self.name_system_tx.send(NameSystemJob::Publish { context: self.sphere_context.clone(), - record: name_record.clone(), + record: LinkRecord::try_from(name_record)?, temporary_validate_expiry: false, }) { warn!("Failed to request name record publish: {}", error); diff --git a/rust/noosphere-gateway/src/worker/name_system.rs b/rust/noosphere-gateway/src/worker/name_system.rs index 0f1cbcf1a..f20809e55 100644 --- a/rust/noosphere-gateway/src/worker/name_system.rs +++ b/rust/noosphere-gateway/src/worker/name_system.rs @@ -2,10 +2,9 @@ use crate::try_or_reset::TryOrReset; use anyhow::anyhow; use anyhow::Result; use cid::Cid; -use noosphere_core::data::ContentType; -use noosphere_core::data::{Did, IdentityIpld, Jwt, LinkRecord, MapOperation}; +use noosphere_core::data::{ContentType, Did, IdentityIpld, LinkRecord, MapOperation}; use noosphere_ipfs::{IpfsStore, KuboClient}; -use noosphere_ns::{server::HttpClient as NameSystemHttpClient, NameResolver, NsRecord}; +use noosphere_ns::{server::HttpClient as NameSystemHttpClient, NameResolver}; use noosphere_sphere::{ HasMutableSphereContext, SphereCursor, SpherePetnameRead, SpherePetnameWrite, }; @@ -16,7 +15,6 @@ use std::fmt::Display; use std::future::Future; use std::{ collections::{BTreeMap, BTreeSet, VecDeque}, - str::FromStr, string::ToString, sync::Arc, time::Duration, @@ -86,7 +84,7 @@ pub enum NameSystemJob { /// Publish a link record (given as a [Jwt]) to the name system Publish { context: C, - record: Jwt, + record: LinkRecord, temporary_validate_expiry: bool, }, } @@ -243,14 +241,13 @@ where warn!("Could not set counterpart record on sphere: {error}"); } // TODO(#257) - let link_record = NsRecord::from_str(&record)?; let publishable = if temporary_validate_expiry { - link_record.has_publishable_timeframe() + record.has_publishable_timeframe() } else { true }; if publishable { - client.publish(link_record).await?; + client.publish(record).await?; } else { return Err(anyhow!("Record is expired and cannot be published.")); } @@ -384,14 +381,17 @@ where Some(record) => { // TODO(#257) if false { - if let Err(error) = record.validate(&ipfs_store, None).await { - error!("Failed record validation: {}", error); - continue; + match record.validate(&ipfs_store).await { + Ok(_) => {} + Err(error) => { + error!("Failed record validation: {}", error); + continue; + } } } // TODO(#258): Verify that the new value is the most recent value - Some(LinkRecord::from(Jwt(record.try_to_string()?))) + Some(record) } None => { // TODO(#259): Expire recorded value if we don't get an updated @@ -425,7 +425,7 @@ async fn fetch_record( client: Arc, name: String, identity: Did, -) -> Result> { +) -> Result> { debug!("Resolving record '{}' ({})...", name, identity); Ok(match client.resolve(&identity).await { Ok(Some(record)) => { @@ -466,7 +466,7 @@ impl OnDemandNameResolver { } } -async fn set_counterpart_record(context: C, record: &Jwt) -> Result<()> +async fn set_counterpart_record(context: C, record: &LinkRecord) -> Result<()> where C: HasMutableSphereContext, K: KeyMaterial + Clone + 'static, @@ -484,7 +484,7 @@ where .write( &counterpart_link_record_key, &ContentType::Text.to_string(), - record.as_bytes(), + record.encode()?.as_bytes(), None, ) .await?; @@ -493,7 +493,7 @@ where Ok(()) } -async fn get_counterpart_record(context: &C) -> Result> +async fn get_counterpart_record(context: &C) -> Result> where C: HasMutableSphereContext, K: KeyMaterial + Clone + 'static, @@ -510,7 +510,7 @@ where let mut buffer = String::new(); if let Some(mut file) = context.read(&counterpart_link_record_key).await? { file.contents.read_to_string(&mut buffer).await?; - Ok(Some(Jwt(buffer))) + Ok(Some(LinkRecord::try_from(buffer)?)) } else { Ok(None) } @@ -518,11 +518,10 @@ where #[cfg(test)] mod tests { - use noosphere_ns::{ - helpers::KeyValueNameResolver, - utils::{generate_capability, generate_fact}, - }; + use noosphere_core::authority::{generate_capability, SphereAction}; + use noosphere_ns::helpers::KeyValueNameResolver; use noosphere_sphere::helpers::{simulated_sphere_context, SimulationAccess}; + use serde_json::json; use ucan::builder::UcanBuilder; use super::*; @@ -531,44 +530,42 @@ mod tests { async fn it_publishes_to_the_name_system() -> Result<()> { let ipfs_url: Url = "http://127.0.0.1:5000".parse()?; let sphere = simulated_sphere_context(SimulationAccess::ReadWrite, None).await?; - let record = { + let record: LinkRecord = { let context = sphere.lock().await; let identity: &str = context.identity().into(); - Jwt(UcanBuilder::default() + UcanBuilder::default() .issued_by(&context.author().key) .for_audience(identity) - .claiming_capability(&generate_capability(identity)) + .claiming_capability(&generate_capability(identity, SphereAction::Publish)) .with_lifetime(1000) - .with_fact(generate_fact( - "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72", - )) + .with_fact( + json!({ "link": "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i" }), + ) .build() .unwrap() .sign() .await .unwrap() - .encode() - .unwrap()) + .into() }; - let expired = { + let expired: LinkRecord = { let context = sphere.lock().await; let identity: &str = context.identity().into(); - Jwt(UcanBuilder::default() + UcanBuilder::default() .issued_by(&context.author().key) .for_audience(identity) - .claiming_capability(&generate_capability(identity)) + .claiming_capability(&generate_capability(identity, SphereAction::Publish)) .with_expiration(ucan::time::now() - 1000) - .with_fact(generate_fact( - "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72", - )) + .with_fact( + json!({ "link": "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i" }), + ) .build() .unwrap() .sign() .await .unwrap() - .encode() - .unwrap()) + .into() }; let mut with_client = TryOrReset::new(|| async { Ok(KeyValueNameResolver::default()) }); diff --git a/rust/noosphere-ipfs/Cargo.toml b/rust/noosphere-ipfs/Cargo.toml index ad1780771..ef399001f 100644 --- a/rust/noosphere-ipfs/Cargo.toml +++ b/rust/noosphere-ipfs/Cargo.toml @@ -34,8 +34,8 @@ libipld-core = { workspace = true } libipld-cbor = { workspace = true } cid = { workspace = true } reqwest = { version = "~0.11", default-features = false, features = ["json", "rustls-tls", "stream"] } -serde = "^1" -serde_json = "^1" +serde = { workspace = true } +serde_json = { workspace = true } tokio = { version = "^1", features = ["io-util"] } tracing = { workspace = true } url = { version = "^2", features = [ "serde" ] } diff --git a/rust/noosphere-ns/Cargo.toml b/rust/noosphere-ns/Cargo.toml index 7b5fd33ec..66ceeb53d 100644 --- a/rust/noosphere-ns/Cargo.toml +++ b/rust/noosphere-ns/Cargo.toml @@ -29,8 +29,8 @@ tracing = { workspace = true } thiserror = { workspace = true } lazy_static = "^1" cid = { workspace = true } -serde = "^1" -serde_json = "^1" +serde = { workspace = true } +serde_json = { workspace = true } futures = "^0.3.26" async-trait = "~0.1" ucan = { workspace = true } diff --git a/rust/noosphere-ns/src/bin/orb-ns/cli/cli_implementation.rs b/rust/noosphere-ns/src/bin/orb-ns/cli/cli_implementation.rs index b3ffe4eff..7825151e7 100644 --- a/rust/noosphere-ns/src/bin/orb-ns/cli/cli_implementation.rs +++ b/rust/noosphere-ns/src/bin/orb-ns/cli/cli_implementation.rs @@ -4,8 +4,8 @@ use crate::cli::address::{ deserialize_multiaddr, deserialize_socket_addr, deserialize_url, parse_cli_address, }; use clap::{Parser, Subcommand}; -use noosphere_core::data::Did; -use noosphere_ns::{DhtConfig, Multiaddr, NsRecord}; +use noosphere_core::data::{Did, LinkRecord}; +use noosphere_ns::{DhtConfig, Multiaddr}; use serde::Deserialize; use std::net::SocketAddr; use std::path::PathBuf; @@ -94,7 +94,7 @@ pub enum CLIRecords { api_url: Url, }, Put { - record: NsRecord, + record: LinkRecord, #[arg(short, long, value_parser = parse_cli_address::)] api_url: Url, }, diff --git a/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs b/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs index c2b2faa37..43bd642a5 100644 --- a/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs +++ b/rust/noosphere-ns/src/bin/orb-ns/cli/mod.rs @@ -12,12 +12,16 @@ mod test { use anyhow::Result; use cid::Cid; use noosphere::key::{InsecureKeyStorage, KeyStorage}; - use noosphere_core::data::Did; - use noosphere_ns::{Multiaddr, NsRecord, PeerId}; + use noosphere_core::authority::{generate_capability, SphereAction}; + use noosphere_core::data::{Did, LinkRecord}; + use noosphere_core::view::SPHERE_LIFETIME; + use noosphere_ns::{Multiaddr, PeerId}; use serde::Deserialize; + use serde_json::json; use tempdir::TempDir; use tokio; use tokio::sync::oneshot; + use ucan::builder::UcanBuilder; use ucan::crypto::KeyMaterial; use url::Url; @@ -116,9 +120,18 @@ mod test { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } - let link = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72"; + let link = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; let cid_link: Cid = link.parse()?; - let record = NsRecord::from_issuer(&key_b, &id_b, &cid_link, None).await?; + let ucan = UcanBuilder::default() + .issued_by(&key_b) + .for_audience(&id_b) + .claiming_capability(&generate_capability(&id_b, SphereAction::Publish)) + .with_fact(json!({ "link": cid_link.to_string() })) + .with_lifetime(SPHERE_LIFETIME) + .build()? + .sign() + .await?; + let record = LinkRecord::try_from(ucan)?; // Push record from node B (for node B) assert!(process_command( @@ -144,9 +157,9 @@ mod test { .await .unwrap(); let value = res.value().unwrap(); - let fetched_record = serde_json::from_str::(value).unwrap(); - assert_eq!(fetched_record.link().unwrap(), &cid_link); - assert_eq!(fetched_record.identity(), &id_b); + let fetched = serde_json::from_str::(value).unwrap(); + assert_eq!(fetched.get_link().unwrap(), cid_link); + assert_eq!(fetched.sphere_identity(), &id_b); Ok(()) } diff --git a/rust/noosphere-ns/src/dht_client.rs b/rust/noosphere-ns/src/dht_client.rs index 966a1761e..3d7d2774d 100644 --- a/rust/noosphere-ns/src/dht_client.rs +++ b/rust/noosphere-ns/src/dht_client.rs @@ -1,12 +1,11 @@ use crate::{ dht::{NetworkInfo, Peer}, - records::NsRecord, PeerId, }; use anyhow::Result; use async_trait::async_trait; use libp2p::Multiaddr; -use noosphere_core::data::Did; +use noosphere_core::data::{Did, LinkRecord}; #[cfg(doc)] use crate::server::HttpClient; @@ -45,12 +44,12 @@ pub trait DhtClient: Send + Sync { /* Record APIs */ - /// Propagates the corresponding managed sphere's [NsRecord] on nearby peers + /// Propagates the corresponding managed sphere's [LinkRecord] on nearby peers /// in the DHT network. - async fn put_record(&self, record: NsRecord, quorum: usize) -> Result<()>; + async fn put_record(&self, record: LinkRecord, quorum: usize) -> Result<()>; - /// Returns an [NsRecord] for the provided identity if found. - async fn get_record(&self, identity: &Did) -> Result>; + /// Returns an [LinkRecord] for the provided identity if found. + async fn get_record(&self, identity: &Did) -> Result>; /* Operator APIs */ @@ -97,11 +96,17 @@ pub mod test { use crate::{utils::wait_for_peers, NameSystemBuilder}; use cid::Cid; use libp2p::multiaddr::Protocol; - use noosphere_core::{authority::generate_ed25519_key, data::Did, tracing::initialize_tracing}; + use noosphere_core::{ + authority::{generate_capability, generate_ed25519_key, SphereAction}, + data::Did, + tracing::initialize_tracing, + view::SPHERE_LIFETIME, + }; use noosphere_storage::{MemoryStorage, SphereDb}; + use serde_json::json; use std::sync::Arc; use tokio::sync::Mutex; - use ucan::crypto::KeyMaterial; + use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; pub async fn test_network_info(client: Arc>) -> Result<()> { initialize_tracing(None); @@ -165,20 +170,33 @@ pub mod test { client.listen("/ip4/127.0.0.1/tcp/0".parse()?).await?; let sphere_key = generate_ed25519_key(); - let sphere_id = Did::from(sphere_key.get_did().await?); - let link: Cid = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72" + let sphere_identity = Did::from(sphere_key.get_did().await?); + let link: Cid = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i" .parse() .unwrap(); - let record = NsRecord::from_issuer(&sphere_key, &sphere_id, &link, None).await?; + let ucan = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .claiming_capability(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .with_fact(json!({ "link": link.to_string() })) + .with_lifetime(SPHERE_LIFETIME) + .build()? + .sign() + .await?; + let record = LinkRecord::try_from(ucan)?; + client.put_record(record, 1).await?; let retrieved = client - .get_record(&sphere_id) + .get_record(&sphere_identity) .await? .expect("should be some"); - assert_eq!(retrieved.identity(), &sphere_id); - assert_eq!(retrieved.link(), Some(&link)); + assert_eq!(retrieved.sphere_identity(), sphere_identity); + assert_eq!(retrieved.get_link(), Some(link.clone())); Ok(()) } } diff --git a/rust/noosphere-ns/src/helpers.rs b/rust/noosphere-ns/src/helpers.rs index d7e4eeb0e..c712f4c17 100644 --- a/rust/noosphere-ns/src/helpers.rs +++ b/rust/noosphere-ns/src/helpers.rs @@ -1,8 +1,11 @@ -use crate::{DhtClient, DhtConfig, NameResolver, NameSystem, NsRecord}; +use crate::{DhtClient, DhtConfig, NameResolver, NameSystem}; use anyhow::Result; use async_trait::async_trait; use libp2p::Multiaddr; -use noosphere_core::{authority::generate_ed25519_key, data::Did}; +use noosphere_core::{ + authority::generate_ed25519_key, + data::{Did, LinkRecord}, +}; use std::collections::HashMap; use tokio::sync::Mutex; use ucan::store::UcanJwtStore; @@ -68,7 +71,7 @@ impl NameSystemNetwork { } pub struct KeyValueNameResolver { - store: Mutex>, + store: Mutex>, } impl KeyValueNameResolver { @@ -87,14 +90,14 @@ impl Default for KeyValueNameResolver { #[async_trait] impl NameResolver for KeyValueNameResolver { - async fn publish(&self, record: NsRecord) -> Result<()> { + async fn publish(&self, record: LinkRecord) -> Result<()> { let mut store = self.store.lock().await; - let did_id = Did(record.identity().into()); + let did_id = Did(record.sphere_identity().into()); store.insert(did_id, record); Ok(()) } - async fn resolve(&self, identity: &Did) -> Result> { + async fn resolve(&self, identity: &Did) -> Result> { let store = self.store.lock().await; Ok(store.get(identity).map(|record| record.to_owned())) } diff --git a/rust/noosphere-ns/src/lib.rs b/rust/noosphere-ns/src/lib.rs index 11067a598..86e863f10 100644 --- a/rust/noosphere-ns/src/lib.rs +++ b/rust/noosphere-ns/src/lib.rs @@ -12,8 +12,8 @@ mod dht_client; pub mod helpers; mod name_resolver; mod name_system; -mod records; pub mod utils; +mod validator; //#[cfg(feature = "api_server")] pub mod server; @@ -24,4 +24,3 @@ pub use dht_client::DhtClient; pub use libp2p::{multiaddr::Multiaddr, PeerId}; pub use name_resolver::NameResolver; pub use name_system::{NameSystem, NameSystemKeyMaterial, BOOTSTRAP_PEERS}; -pub use records::NsRecord; diff --git a/rust/noosphere-ns/src/name_resolver.rs b/rust/noosphere-ns/src/name_resolver.rs index d5d1dd682..7a60123dc 100644 --- a/rust/noosphere-ns/src/name_resolver.rs +++ b/rust/noosphere-ns/src/name_resolver.rs @@ -1,23 +1,23 @@ -use crate::{DhtClient, NsRecord}; +use crate::DhtClient; use anyhow::Result; use async_trait::async_trait; -use noosphere_core::data::Did; +use noosphere_core::data::{Did, LinkRecord}; #[async_trait] pub trait NameResolver: Send + Sync { /// Publishes a record to the name system. - async fn publish(&self, record: NsRecord) -> Result<()>; + async fn publish(&self, record: LinkRecord) -> Result<()>; /// Retrieves a record from the name system. - async fn resolve(&self, identity: &Did) -> Result>; + async fn resolve(&self, identity: &Did) -> Result>; } #[async_trait] impl NameResolver for T { - async fn publish(&self, record: NsRecord) -> Result<()> { + async fn publish(&self, record: LinkRecord) -> Result<()> { self.put_record(record, 0).await } - async fn resolve(&self, identity: &Did) -> Result> { + async fn resolve(&self, identity: &Did) -> Result> { self.get_record(identity).await } } @@ -42,21 +42,39 @@ macro_rules! name_resolver_tests { pub mod test { use super::*; use cid::Cid; - use noosphere_core::{authority::generate_ed25519_key, data::Did, tracing::initialize_tracing}; - use ucan::crypto::KeyMaterial; + use noosphere_core::{ + authority::{generate_capability, generate_ed25519_key, SphereAction}, + data::Did, + tracing::initialize_tracing, + view::SPHERE_LIFETIME, + }; + use serde_json::json; + use ucan::{builder::UcanBuilder, crypto::KeyMaterial}; pub async fn test_name_resolver_simple(resolver: N) -> Result<()> { initialize_tracing(None); let sphere_key = generate_ed25519_key(); - let sphere_id = Did::from(sphere_key.get_did().await?); - let link: Cid = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72" + let sphere_identity = Did::from(sphere_key.get_did().await?); + let link: Cid = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i" .parse() .unwrap(); - let record = NsRecord::from_issuer(&sphere_key, &sphere_id, &link, None).await?; + let ucan = UcanBuilder::default() + .issued_by(&sphere_key) + .for_audience(&sphere_identity) + .claiming_capability(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .with_fact(json!({ "link": link.to_string() })) + .with_lifetime(SPHERE_LIFETIME) + .build()? + .sign() + .await?; + let record = LinkRecord::try_from(ucan)?; resolver.publish(record).await?; - let resolved = resolver.resolve(&sphere_id).await?.unwrap(); - assert_eq!(resolved.link().unwrap(), &link); + let resolved = resolver.resolve(&sphere_identity).await?.unwrap(); + assert_eq!(resolved.get_link().unwrap(), link); Ok(()) } } diff --git a/rust/noosphere-ns/src/name_system.rs b/rust/noosphere-ns/src/name_system.rs index b1149fb6f..feeb084a7 100644 --- a/rust/noosphere-ns/src/name_system.rs +++ b/rust/noosphere-ns/src/name_system.rs @@ -1,13 +1,13 @@ use crate::{ dht::{DhtConfig, DhtError, DhtNode, DhtRecord, NetworkInfo, Peer}, - records::{NsRecord, RecordValidator}, utils::make_p2p_address, + validator::RecordValidator, DhtClient, PeerId, }; use anyhow::{anyhow, Result}; use async_trait::async_trait; use libp2p::{identity::Keypair, Multiaddr}; -use noosphere_core::data::Did; +use noosphere_core::data::{Did, LinkRecord}; use ucan::{crypto::KeyMaterial, store::UcanJwtStore}; use ucan_key_support::ed25519::Ed25519KeyMaterial; @@ -130,8 +130,8 @@ impl DhtClient for NameSystem { } } - async fn put_record(&self, record: NsRecord, quorum: usize) -> Result<()> { - let identity = Did::from(record.identity()); + async fn put_record(&self, record: LinkRecord, quorum: usize) -> Result<()> { + let identity = Did::from(record.sphere_identity()); let record_bytes: Vec = record.try_into()?; match self .dht @@ -143,10 +143,10 @@ impl DhtClient for NameSystem { } } - async fn get_record(&self, identity: &Did) -> Result> { + async fn get_record(&self, identity: &Did) -> Result> { match self.dht.get_record(identity.as_bytes()).await { Ok(DhtRecord { key: _, value }) => match value { - Some(value) => Ok(Some(NsRecord::try_from(value)?)), + Some(value) => Ok(Some(LinkRecord::try_from(value)?)), None => Ok(None), }, Err(e) => Err(anyhow!(e.to_string())), diff --git a/rust/noosphere-ns/src/records.rs b/rust/noosphere-ns/src/records.rs deleted file mode 100644 index f11e733fa..000000000 --- a/rust/noosphere-ns/src/records.rs +++ /dev/null @@ -1,648 +0,0 @@ -use crate::{ - dht::Validator, - utils::{generate_capability, generate_fact}, -}; -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::{Debug, Display}, - str, - str::FromStr, -}; -use ucan::{ - builder::UcanBuilder, - crypto::KeyMaterial, - store::{UcanJwtStore, UcanStore}, -}; -use ucan::{chain::ProofChain, crypto::did::DidParser, Ucan}; - -/// An [NsRecord] is the internal representation of a mapping from a -/// sphere's identity (DID key) to a sphere's revision as a -/// content address ([Cid]). The record wraps a [Ucan] token, -/// providing de/serialization for transmitting in the NS network, -/// and validates data ensuring the sphere's owner authorized the publishing -/// of a new content address. -/// -/// When transmitting through the distributed NS network, the record is -/// represented as the base64 encoded UCAN token. -/// -/// # Ucan Semantics -/// -/// An [NsRecord] is a small interface over a [Ucan] token, -/// with the following semantics: -/// -/// ```json -/// { -/// // The identity (DID) of the Principal that signed the token -/// "iss": "did:key:z6MkoE19WHXJzpLqkxbGP7uXdJX38sWZNUWwyjcuCmjhPpUP", -/// // The identity (DID) of the sphere this record maps. -/// "aud": "did:key:z6MkkVfktAC5rVNRmmTjkKPapT3bAyVkYH8ZVCF1UBNUfazp", -/// // Attenuation must contain a capability with a resource "sphere:{AUD}" -/// // and action "sphere/publish". -/// "att": [{ -/// "with": "sphere:did:key:z6MkkVfktAC5rVNRmmTjkKPapT3bAyVkYH8ZVCF1UBNUfazp", -/// "can": "sphere/publish" -/// }], -/// // Additional UCAN proofs needed to validate. -/// "prf": [], -/// // Facts contain a single entry with an "link" field containing -/// // the content address of a sphere revision (CID) associated with -/// // the sphere this record maps to. -/// "fct": [{ -/// "link": "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72" -/// }] -/// } -/// ``` -#[derive(Clone)] -pub struct NsRecord { - /// The wrapped UCAN token describing this record. - pub(crate) token: Ucan, - /// The resolved sphere revision this record maps to. - pub(crate) link: Option, -} - -impl NsRecord { - /// Creates a new [NsRecord]. - pub fn new(token: Ucan) -> Self { - // Cache the revision address if "fct" contains an entry matching - // the following object without any authority validation: - // `{ "link": "{VALID_CID}" }` - 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 } - } - - /// Creates and signs a new NsRecord from an issuer key. - /// - /// ``` - /// use noosphere_ns::NsRecord; - /// use noosphere_core::{data::Did, authority::generate_ed25519_key}; - /// use noosphere_storage::{SphereDb, MemoryStorage}; - /// use ucan_key_support::ed25519::Ed25519KeyMaterial; - /// use ucan::crypto::KeyMaterial; - /// use cid::Cid; - /// use tokio; - /// - /// #[tokio::main] - /// async fn main() { - /// let sphere_key = generate_ed25519_key(); - /// let sphere_id = Did::from(sphere_key.get_did().await.unwrap()); - /// let store = SphereDb::new(&MemoryStorage::default()).await.unwrap(); - /// let link: Cid = "bafy2bzacec4p5h37mjk2n6qi6zukwyzkruebvwdzqpdxzutu4sgoiuhqwne72".parse().unwrap(); - /// let record = NsRecord::from_issuer(&sphere_key, &sphere_id, &link, None).await.unwrap(); - /// } - /// ``` - pub async fn from_issuer( - issuer: &K, - sphere_id: &Did, - link: &Cid, - proofs: Option<&Vec>, - ) -> Result { - 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()) - } - - /// Validates the underlying [Ucan] token, ensuring that - /// the sphere's owner authorized the publishing of a new - /// content address. Returns an `Err` if validation fails. - #[instrument(skip(store, opt_did_parser), level = "trace")] - pub async fn validate( - &self, - store: &S, - opt_did_parser: Option<&mut DidParser>, - ) -> Result<(), NsRecordError> { - 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(); - - // We're interested in the validity of the proof at the time - // of publishing. - let now_time = if let Some(nbf) = self.token.not_before() { - nbf.to_owned() - } else { - self.token.expires_at() - 1 - }; - - let proof = - ProofChain::from_ucan(self.token.clone(), Some(now_time), 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(()) - } - - /// Returns true if the [Ucan] token is currently publishable - /// within the bounds of its expiry/not before time. - pub fn has_publishable_timeframe(&self) -> bool { - !self.token.is_expired(None) && !self.token.is_too_early() - } - - /// The DID key of the sphere that this record maps. - pub fn identity(&self) -> &str { - self.token.audience() - } - - /// The sphere revision address ([Cid]) that the sphere's identity maps to. - pub fn link(&self) -> Option<&Cid> { - self.link.as_ref() - } - - /// Encodes the underlying Ucan token back into a JWT string. - pub fn try_to_string(&self) -> Result { - self.token.encode() - } -} - -impl Debug for NsRecord { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let link = self.link.map(|cid| cid.to_string()); - write!( - f, - "NsRecord {{ \"sphere\": \"{}\", \"link\": \"{:?}\" }}", - self.token.audience(), - link - ) - } -} - -impl Display for NsRecord { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let link = self.link.map(|cid| cid.to_string()); - write!( - f, - "NsRecord {{\n \"sphere\": \"{}\",\n \"link\": \"{:?}\"\n}}", - self.token.audience(), - link - ) - } -} - -impl Serialize for NsRecord { - fn serialize(&self, serializer: S) -> Result - 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(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - let record = NsRecord::try_from(s).map_err(de::Error::custom)?; - Ok(record) - } -} - -impl From for NsRecord { - fn from(ucan: Ucan) -> Self { - Self::new(ucan) - } -} - -impl From for Ucan { - fn from(record: NsRecord) -> Self { - record.token - } -} - -/// Deserialize an encoded UCAN token byte vec into a [NsRecord]. -impl TryFrom> for NsRecord { - type Error = anyhow::Error; - - fn try_from(bytes: Vec) -> Result { - NsRecord::try_from(&bytes[..]) - } -} - -/// Serialize a [NsRecord] into an encoded UCAN token byte vec. -impl TryFrom for Vec { - type Error = anyhow::Error; - - fn try_from(record: NsRecord) -> Result { - Vec::try_from(&record) - } -} - -/// Serialize a [NsRecord] reference into an encoded UCAN token byte vec. -impl TryFrom<&NsRecord> for Vec { - type Error = anyhow::Error; - - fn try_from(record: &NsRecord) -> Result, Self::Error> { - Ok(Vec::from(record.token.encode()?)) - } -} - -/// Deserialize an encoded UCAN token byte vec reference into a [NsRecord]. -impl TryFrom<&[u8]> for NsRecord { - type Error = anyhow::Error; - - fn try_from(bytes: &[u8]) -> Result { - NsRecord::try_from(str::from_utf8(bytes)?) - } -} - -/// Deserialize an encoded UCAN token string reference into a [NsRecord]. -impl<'a> TryFrom<&'a str> for NsRecord { - type Error = anyhow::Error; - - fn try_from(ucan_token: &str) -> Result { - NsRecord::from_str(ucan_token) - } -} - -/// Deserialize an encoded UCAN token string into a [NsRecord]. -impl TryFrom for NsRecord { - type Error = anyhow::Error; - - fn try_from(ucan_token: String) -> Result { - NsRecord::from_str(ucan_token.as_str()) - } -} - -/// Serialize an NsRecord into a JWT-encoded string. -impl TryFrom for String { - type Error = anyhow::Error; - - fn try_from(record: NsRecord) -> Result { - record.try_to_string() - } -} - -impl FromStr for NsRecord { - type Err = anyhow::Error; - - fn from_str(ucan_token: &str) -> Result { - // Wait for next release of `ucan` which includes traits and - // removes `try_from_token_string`: - // https://github.com/ucan-wg/rs-ucan/commit/75e9afdb9da60c3d5d8c65b6704e412f0ef8189b - 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 for NsRecordError { - fn from(error: anyhow::Error) -> Self { - NsRecordError::Other(error) - } -} - -pub(crate) struct RecordValidator { - store: S, - did_parser: DidParser, -} - -impl RecordValidator -where - S: UcanStore, -{ - pub fn new(store: S) -> Self { - RecordValidator { - store, - did_parser: DidParser::new(SUPPORTED_KEYS), - } - } -} - -#[async_trait] -impl Validator for RecordValidator -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, 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 = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; - 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 = "bafyr4iagi6t6khdrtbhmyjpjgvdlwv6pzylxhuhstxhkdp52rju7er325i"; - let cid_link: Cid = link.parse()?; - let mut store = SphereDb::new(&MemoryStorage::default()).await.unwrap(); - - // First verify that `owner` cannot publish for `sphere` - // without delegation. - 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") - } - - // Delegate `sphere_key`'s publishing authority to `owner_key` - 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 out_cid = store.write_token(&delegate_ucan.encode()?).await?; - - // Attempt `owner` publishing `sphere` with the proper authorization. - let proofs = vec![delegate_ucan.clone()]; - 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)); - assert!(record.has_publishable_timeframe()); - record.validate(&store, Some(&mut did_parser)).await?; - - // Now test a similar record that has an expired capability. - // It must still be valid. - let expired: NsRecord = UcanBuilder::default() - .issued_by(&owner_key) - .for_audience(&sphere_identity) - .claiming_capability(&generate_capability(&sphere_identity)) - .with_fact(generate_fact(&cid_link.to_string())) - .witnessed_by(&delegate_ucan) - .with_expiration(ucan::time::now() - 1234) - .build()? - .sign() - .await? - .into(); - assert_eq!(expired.identity(), &sphere_identity); - assert_eq!(expired.link(), Some(&cid_link)); - assert!(expired.has_publishable_timeframe() == false); - expired.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()); - - // NsRecord::serialize - // NsRecord::deserialize - 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()"); - - // NsRecord::try_from::>() - let record = NsRecord::try_from(bytes.clone())?; - assert_eq!(base.identity(), record.identity(), "try_from::>()"); - assert_eq!(base.link(), record.link(), "try_from::>()"); - - // NsRecord::try_into::>() - let rec_bytes: Vec = base.clone().try_into()?; - assert_eq!(bytes, rec_bytes, "try_into::>()"); - - // NsRecord::try_from::<&[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]>()"); - - // &NsRecord::try_into::>() - let rec_bytes: Vec = (&base).try_into()?; - assert_eq!(bytes, rec_bytes, "&NsRecord::try_into::>()"); - - // NsRecord::from::() - let record = NsRecord::from(ucan); - assert_eq!(base.identity(), record.identity(), "from::()"); - assert_eq!(base.link(), record.link(), "from::()"); - - // NsRecord::try_from::<&str>() - 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>()"); - - // NsRecord::try_from::() - let record = NsRecord::try_from(encoded.clone())?; - assert_eq!(base.identity(), record.identity(), "try_from::()"); - assert_eq!(base.link(), record.link(), "try_from::()"); - - // NsRecord::from_str() - 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(()) - } -} diff --git a/rust/noosphere-ns/src/server/client.rs b/rust/noosphere-ns/src/server/client.rs index 33b880f61..4cc703f34 100644 --- a/rust/noosphere-ns/src/server/client.rs +++ b/rust/noosphere-ns/src/server/client.rs @@ -1,8 +1,8 @@ use crate::server::routes::Route; -use crate::{dht_client::DhtClient, Multiaddr, NetworkInfo, NsRecord, Peer, PeerId}; +use crate::{dht_client::DhtClient, Multiaddr, NetworkInfo, Peer, PeerId}; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use noosphere_core::data::Did; +use noosphere_core::data::{Did, LinkRecord}; use reqwest::Body; use url::Url; @@ -95,7 +95,7 @@ impl DhtClient for HttpClient { Ok(self.client.get(url).send().await?.json().await?) } - async fn get_record(&self, identity: &Did) -> Result> { + async fn get_record(&self, identity: &Did) -> Result> { let mut url = self.api_base.clone(); let path = Route::GetRecord .to_string() @@ -104,7 +104,7 @@ impl DhtClient for HttpClient { Ok(self.client.get(url).send().await?.json().await?) } - async fn put_record(&self, record: NsRecord, quorum: usize) -> Result<()> { + async fn put_record(&self, record: LinkRecord, quorum: usize) -> Result<()> { let mut url = self.api_base.clone(); url.set_path(&Route::PostRecord.to_string()); url.set_query(Some(&format!("quorum={quorum}"))); diff --git a/rust/noosphere-ns/src/server/handlers.rs b/rust/noosphere-ns/src/server/handlers.rs index ca016eeb3..a3feb807c 100644 --- a/rust/noosphere-ns/src/server/handlers.rs +++ b/rust/noosphere-ns/src/server/handlers.rs @@ -1,4 +1,4 @@ -use crate::{DhtClient, Multiaddr, NameSystem, NetworkInfo, NsRecord, Peer, PeerId}; +use crate::{DhtClient, Multiaddr, NameSystem, NetworkInfo, Peer, PeerId}; use anyhow::Result; use axum::response::{IntoResponse, Response}; use axum::{ @@ -6,7 +6,7 @@ use axum::{ http::StatusCode, Extension, Json, }; -use noosphere_core::data::Did; +use noosphere_core::data::{Did, LinkRecord}; use serde::Deserialize; use std::sync::Arc; @@ -92,7 +92,7 @@ pub async fn get_address( pub async fn get_record( Extension(name_system): Extension>, Path(did): Path, -) -> JsonResponse> { +) -> JsonResponse> { let record = name_system .get_record(&did) .await @@ -107,7 +107,7 @@ pub struct PostRecordQuery { pub async fn post_record( Extension(name_system): Extension>, - Json(record): Json, + Json(record): Json, Query(query): Query, ) -> JsonResponse<()> { name_system diff --git a/rust/noosphere-ns/src/utils.rs b/rust/noosphere-ns/src/utils.rs index aa1bd2d6e..bedf65970 100644 --- a/rust/noosphere-ns/src/utils.rs +++ b/rust/noosphere-ns/src/utils.rs @@ -4,59 +4,7 @@ use libp2p::{ multiaddr::{Multiaddr, Protocol}, PeerId, }; -use noosphere_core::authority::{SphereAction, SphereReference}; -use serde_json; use tokio::time; -use ucan::capability::{Capability, Resource, With}; - -#[cfg(doc)] -use cid::Cid; - -/// Generates a [Capability] struct representing permission to -/// publish a sphere. -/// -/// ``` -/// use noosphere_ns::utils::generate_capability; -/// use noosphere_core::{authority::{SphereAction, SphereReference}}; -/// use ucan::capability::{Capability, Resource, With}; -/// -/// let identity = "did:key:z6MkoE19WHXJzpLqkxbGP7uXdJX38sWZNUWwyjcuCmjhPpUP"; -/// let expected_capability = Capability { -/// with: With::Resource { -/// kind: Resource::Scoped(SphereReference { -/// did: identity.to_owned(), -/// }), -/// }, -/// can: SphereAction::Publish, -/// }; -/// assert_eq!(generate_capability(&identity), expected_capability); -/// ``` -pub fn generate_capability(identity: &str) -> Capability { - Capability { - with: With::Resource { - kind: Resource::Scoped(SphereReference { - did: identity.to_owned(), - }), - }, - can: SphereAction::Publish, - } -} - -/// Generates a UCAN `"fct"` struct for the NS network, representing -/// the resolved sphere's revision as a [Cid]. -/// -/// ``` -/// use noosphere_ns::utils::generate_fact; -/// use noosphere_storage::derive_cid; -/// use libipld_cbor::DagCborCodec; -/// use serde_json::json; -/// -/// let address = "bafy2bzaced25m65oooyocdin7uyehm7u6eak3iauyxbxxvoos6atwe7vvmv46"; -/// assert_eq!(generate_fact(address), json!({ "link": address })); -/// ``` -pub fn generate_fact(address: &str) -> serde_json::Value { - serde_json::json!({ "link": address }) -} /// A utility for [NameSystemClient] in tests. /// Async function returns once there are at least diff --git a/rust/noosphere-ns/src/validator.rs b/rust/noosphere-ns/src/validator.rs new file mode 100644 index 000000000..3d8ce6cd2 --- /dev/null +++ b/rust/noosphere-ns/src/validator.rs @@ -0,0 +1,31 @@ +use crate::dht::Validator; +use async_trait::async_trait; +use noosphere_core::data::LinkRecord; +use ucan::store::UcanStore; + +/// Implements [Validator] for the DHT. +pub(crate) struct RecordValidator { + store: S, +} + +impl RecordValidator +where + S: UcanStore, +{ + pub fn new(store: S) -> Self { + RecordValidator { store } + } +} + +#[async_trait] +impl Validator for RecordValidator +where + S: UcanStore, +{ + async fn validate(&mut self, record_value: &[u8]) -> bool { + match LinkRecord::try_from(record_value) { + Ok(record) => record.validate(&self.store).await.is_ok(), + _ => false, + } + } +} diff --git a/rust/noosphere-ns/tests/ns_test.rs b/rust/noosphere-ns/tests/ns_test.rs index 198052306..923aa06ec 100644 --- a/rust/noosphere-ns/tests/ns_test.rs +++ b/rust/noosphere-ns/tests/ns_test.rs @@ -4,16 +4,16 @@ use anyhow::Result; use cid::Cid; use noosphere_core::{ - authority::generate_ed25519_key, data::Did, tracing::initialize_tracing, view::SPHERE_LIFETIME, -}; -use noosphere_ns::{ - helpers::NameSystemNetwork, - utils::{generate_capability, generate_fact}, - DhtClient, + authority::{generate_capability, generate_ed25519_key, SphereAction}, + data::Did, + tracing::initialize_tracing, + view::SPHERE_LIFETIME, }; +use noosphere_ns::{helpers::NameSystemNetwork, DhtClient}; use noosphere_storage::{derive_cid, MemoryStorage, SphereDb}; use libipld_cbor::DagCborCodec; +use serde_json::json; use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore, time::now, Ucan}; use ucan_key_support::ed25519::Ed25519KeyMaterial; @@ -35,7 +35,7 @@ impl PseudoSphere { let sphere_id = Did(sphere_key.get_did().await?); // Delegate `sphere_key`'s publishing authority to `owner_key` - let delegate_capability = generate_capability(&sphere_id); + let delegate_capability = generate_capability(&sphere_id, SphereAction::Publish); let delegation = UcanBuilder::default() .issued_by(&sphere_key) .for_audience(&owner_id) @@ -57,8 +57,8 @@ impl PseudoSphere { UcanBuilder::default() .issued_by(&self.owner_key) .for_audience(&self.sphere_id) - .claiming_capability(&generate_capability(&self.sphere_id)) - .with_fact(generate_fact(&cid.to_string())) + .claiming_capability(&generate_capability(&self.sphere_id, SphereAction::Publish)) + .with_fact(json!({ "link": &cid.to_string() })) .witnessed_by(&self.delegation) } @@ -94,7 +94,7 @@ async fn test_name_system_peer_propagation() -> Result<()> { .build()? .sign() .await? - .into(), + .try_into()?, 1, ) .await?; @@ -110,9 +110,9 @@ async fn test_name_system_peer_propagation() -> Result<()> { ns_2.get_record(&sphere_1.sphere_id) .await? .expect("to be some") - .link() + .get_link() .unwrap(), - &sphere_1_cid_1, + sphere_1_cid_1, "first record found" ); @@ -124,7 +124,7 @@ async fn test_name_system_peer_propagation() -> Result<()> { .build()? .sign() .await? - .into(), + .try_into()?, 1, ) .await?; @@ -133,9 +133,9 @@ async fn test_name_system_peer_propagation() -> Result<()> { ns_2.get_record(&sphere_1.sphere_id) .await? .expect("to be some") - .link() + .get_link() .unwrap(), - &sphere_1_cid_2, + sphere_1_cid_2, "latest record is found from network" ); @@ -147,7 +147,7 @@ async fn test_name_system_peer_propagation() -> Result<()> { .build()? .sign() .await? - .into(), + .try_into()?, 1, ) .await?; @@ -158,9 +158,9 @@ async fn test_name_system_peer_propagation() -> Result<()> { ns_1.get_record(&sphere_2.sphere_id) .await? .expect("to be some") - .link() + .get_link() .unwrap(), - &sphere_2_cid_1, + sphere_2_cid_1, "non-cached record found for sphere_2" ); @@ -187,7 +187,7 @@ async fn test_name_system_validation() -> Result<()> { .build()? .sign() .await? - .into(), + .try_into()?, 1 ) .await diff --git a/rust/noosphere-sphere/Cargo.toml b/rust/noosphere-sphere/Cargo.toml index 5db72afe5..cf7897a23 100644 --- a/rust/noosphere-sphere/Cargo.toml +++ b/rust/noosphere-sphere/Cargo.toml @@ -39,8 +39,8 @@ futures-util = "0.3.27" libipld-core = { workspace = true } libipld-cbor = { workspace = true } bytes = "^1" -serde_json = "^1" -serde = "1" +serde_json = { workspace = true } +serde = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/rust/noosphere-sphere/src/context.rs b/rust/noosphere-sphere/src/context.rs index 10914f26f..7cdc052f1 100644 --- a/rust/noosphere-sphere/src/context.rs +++ b/rust/noosphere-sphere/src/context.rs @@ -147,7 +147,7 @@ where debug!("Petname assigned to {:?}", identity); let resolved_version = match identity.link_record(self.db()).await { - Some(link_record) => link_record.dereference().await, + Some(link_record) => link_record.get_link(), None => None, }; @@ -378,17 +378,14 @@ pub mod tests { use std::sync::Arc; use noosphere_core::{ - authority::{SphereAction, SphereReference}, - data::{ContentType, Jwt}, + authority::{generate_capability, SphereAction}, + data::{ContentType, LinkRecord}, tracing::initialize_tracing, }; use noosphere_storage::{MemoryStorage, TrackingStorage}; use serde_json::json; use tokio::{io::AsyncReadExt, sync::Mutex}; - use ucan::{ - builder::UcanBuilder, - capability::{Capability, Resource, With}, - }; + use ucan::builder::UcanBuilder; use ucan_key_support::ed25519::Ed25519KeyMaterial; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; @@ -451,37 +448,33 @@ pub mod tests { .clone(); let next_identity = next_sphere_context.identity().await.unwrap(); - let link_record = Jwt(UcanBuilder::default() - .issued_by(&next_author.key) - .for_audience(&next_identity) - .witnessed_by( - &next_author - .authorization - .as_ref() - .unwrap() - .resolve_ucan(&db) - .await - .unwrap(), - ) - .claiming_capability(&Capability { - with: With::Resource { - kind: Resource::Scoped(SphereReference { - did: next_identity.into(), - }), - }, - can: SphereAction::Publish, - }) - .with_lifetime(120) - .with_fact(json!({ - "link": version.to_string() - })) - .build() - .unwrap() - .sign() - .await - .unwrap() - .encode() - .unwrap()); + let link_record = LinkRecord::from( + UcanBuilder::default() + .issued_by(&next_author.key) + .for_audience(&next_identity) + .witnessed_by( + &next_author + .authorization + .as_ref() + .unwrap() + .resolve_ucan(&db) + .await + .unwrap(), + ) + .claiming_capability(&generate_capability( + &next_identity, + SphereAction::Publish, + )) + .with_lifetime(120) + .with_fact(json!({ + "link": version.to_string() + })) + .build() + .unwrap() + .sign() + .await + .unwrap(), + ); let mut name = String::new(); let mut file = next_sphere_context.read("my-name").await.unwrap().unwrap(); diff --git a/rust/noosphere-sphere/src/helpers.rs b/rust/noosphere-sphere/src/helpers.rs index 6f46eb394..e4d1f6019 100644 --- a/rust/noosphere-sphere/src/helpers.rs +++ b/rust/noosphere-sphere/src/helpers.rs @@ -4,19 +4,14 @@ use std::sync::Arc; use anyhow::Result; use noosphere_core::{ - authority::{generate_ed25519_key, Author, SphereAction, SphereReference}, - data::{Did, Jwt, Link, LinkRecord}, + authority::{generate_capability, generate_ed25519_key, Author, SphereAction}, + data::{Did, Link, LinkRecord}, view::Sphere, }; use noosphere_storage::{MemoryStorage, SphereDb, TrackingStorage}; use serde_json::json; use tokio::sync::Mutex; -use ucan::{ - builder::UcanBuilder, - capability::{Capability, Resource, With}, - crypto::KeyMaterial, - store::UcanJwtStore, -}; +use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; use ucan_key_support::ed25519::Ed25519KeyMaterial; use crate::SphereContext; @@ -80,28 +75,25 @@ where let sphere_identity = sphere.get_identity().await?; - let link_record = LinkRecord::from(Jwt(UcanBuilder::default() - .issued_by(&owner_key) - .for_audience(&sphere_identity) - .witnessed_by(&ucan_proof) - .claiming_capability(&Capability { - with: With::Resource { - kind: Resource::Scoped(SphereReference { - did: sphere_identity.to_string(), - }), - }, - can: SphereAction::Publish, - }) - .with_lifetime(120) - .with_fact(json!({ - "link": sphere.cid().to_string() - })) - .build()? - .sign() - .await? - .encode()?)); - - let link = Link::from(store.write_token(&link_record).await?); + let link_record = LinkRecord::from( + UcanBuilder::default() + .issued_by(&owner_key) + .for_audience(&sphere_identity) + .witnessed_by(&ucan_proof) + .claiming_capability(&generate_capability( + &sphere_identity, + SphereAction::Publish, + )) + .with_lifetime(120) + .with_fact(json!({ + "link": sphere.cid().to_string() + })) + .build()? + .sign() + .await?, + ); + + let link = Link::from(store.write_token(&link_record.encode()?).await?); Ok((sphere_identity, link_record, link)) } diff --git a/rust/noosphere-sphere/src/petname/read.rs b/rust/noosphere-sphere/src/petname/read.rs index 3c9239c71..c0bd7f04f 100644 --- a/rust/noosphere-sphere/src/petname/read.rs +++ b/rust/noosphere-sphere/src/petname/read.rs @@ -130,7 +130,7 @@ where .await; match link_record { - Some(link_record) => link_record.dereference().await, + Some(link_record) => link_record.get_link(), None => None, } } diff --git a/rust/noosphere-sphere/src/petname/write.rs b/rust/noosphere-sphere/src/petname/write.rs index b843fd4fb..1f07d3b31 100644 --- a/rust/noosphere-sphere/src/petname/write.rs +++ b/rust/noosphere-sphere/src/petname/write.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use noosphere_core::data::{Did, IdentityIpld, Jwt}; +use noosphere_core::data::{Did, IdentityIpld, LinkRecord}; use noosphere_storage::Storage; -use ucan::{crypto::KeyMaterial, store::UcanJwtStore, Ucan}; +use ucan::{crypto::KeyMaterial, store::UcanJwtStore}; use crate::{internal::SphereContextInternal, HasMutableSphereContext, SpherePetnameRead}; @@ -35,7 +35,7 @@ where /// associated [Jwt] to a known value. The [Jwt] must be a valid UCAN that /// publishes a name record and grants sufficient authority from the /// configured [Did] to the publisher. - async fn adopt_petname(&mut self, name: &str, record: &Jwt) -> Result>; + async fn adopt_petname(&mut self, name: &str, record: &LinkRecord) -> Result>; } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -76,18 +76,17 @@ where Ok(()) } - async fn adopt_petname(&mut self, name: &str, record: &Jwt) -> Result> { + async fn adopt_petname(&mut self, name: &str, record: &LinkRecord) -> Result> { self.assert_write_access().await?; validate_petname(name)?; - let ucan = Ucan::try_from(record.as_str())?; - let identity = Did::from(ucan.audience()); + let identity = Did::from(record.audience()); let cid = self .sphere_context_mut() .await? .db_mut() - .write_token(record) + .write_token(&record.encode()?) .await?; // TODO: Verify that a record for an existing address is actually newer than the old one diff --git a/rust/noosphere-sphere/src/sync/gateway.rs b/rust/noosphere-sphere/src/sync/gateway.rs index b2c4fa564..44f2d719b 100644 --- a/rust/noosphere-sphere/src/sync/gateway.rs +++ b/rust/noosphere-sphere/src/sync/gateway.rs @@ -275,7 +275,7 @@ where for (name, address) in updated_names.into_iter() { if let Some(link_record) = address.link_record(&db).await { if context.get_petname(&name).await?.is_some() { - context.adopt_petname(&name, &link_record.into()).await?; + context.adopt_petname(&name, &link_record).await?; } else { debug!("Not adopting link record for {name}, which is no longer present in the address book") } diff --git a/rust/noosphere-storage/Cargo.toml b/rust/noosphere-storage/Cargo.toml index f90f63f1d..15c446439 100644 --- a/rust/noosphere-storage/Cargo.toml +++ b/rust/noosphere-storage/Cargo.toml @@ -30,7 +30,7 @@ tracing = "~0.1" ucan = { workspace = true } libipld-core = { workspace = true } libipld-cbor = { workspace = true } -serde = "^1" +serde = { workspace = true } base64 = "=0.13.0" url = { version = "^2" }