diff --git a/Cargo.lock b/Cargo.lock index b302f1f..884bd5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,22 +238,10 @@ checksum = "fd94671561e36e4e7de75f753f577edafb0e7c05d6e4547229fdf7938fbcd2c3" dependencies = [ "core2", "multibase", - "multihash 0.18.1", + "multihash", "serde", "serde_bytes", - "unsigned-varint 0.7.2", -] - -[[package]] -name = "cid" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "472ac98592f38dfd48f188d5713a328422ed22fa39eb52b8bca495370134762a" -dependencies = [ - "core2", - "multibase", - "multihash 0.19.1", - "unsigned-varint 0.8.0", + "unsigned-varint", ] [[package]] @@ -366,16 +354,15 @@ name = "dwn" version = "0.0.2" dependencies = [ "base64", - "cid 0.11.0", "iana-media-types", "libipld-cbor", "libipld-core", "libipld-json", "libipld-pb", - "multihash-codetable", "serde", "serde_ipld_dagcbor", "serde_json", + "tracing", ] [[package]] @@ -383,6 +370,7 @@ name = "dwn-server" version = "0.0.2" dependencies = [ "axum", + "base64", "dwn", "port_check", "reqwest", @@ -792,10 +780,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5acd707e8d8b092e967b2af978ed84709eaded82b75effe6cb6f6cc797ef8158" dependencies = [ "anyhow", - "cid 0.10.1", + "cid", "core2", "multibase", - "multihash 0.18.1", + "multihash", "thiserror", ] @@ -806,7 +794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25856def940047b07b25c33d4e66d248597049ab0202085215dc4dca0487731c" dependencies = [ "libipld-core", - "multihash 0.18.1", + "multihash", "serde", "serde_json", ] @@ -905,41 +893,12 @@ dependencies = [ "blake3", "core2", "digest", - "multihash-derive 0.8.0", + "multihash-derive", "serde", "serde-big-array", "sha2", "sha3", - "unsigned-varint 0.7.2", -] - -[[package]] -name = "multihash" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076d548d76a0e2a0d4ab471d0b1c36c577786dfc4471242035d97a12a735c492" -dependencies = [ - "core2", - "unsigned-varint 0.7.2", -] - -[[package]] -name = "multihash-codetable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d815ecb3c8238d00647f8630ede7060a642c9f704761cd6082cb4028af6935" -dependencies = [ - "blake2b_simd", - "blake2s_simd", - "blake3", - "core2", - "digest", - "multihash-derive 0.9.0", - "ripemd", - "sha1", - "sha2", - "sha3", - "strobe-rs", + "unsigned-varint", ] [[package]] @@ -956,31 +915,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "multihash-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "890e72cb7396cb99ed98c1246a97b243cc16394470d94e0bc8b0c2c11d84290e" -dependencies = [ - "core2", - "multihash 0.19.1", - "multihash-derive-impl", -] - -[[package]] -name = "multihash-derive-impl" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38685e08adb338659871ecfc6ee47ba9b22dcc8abcf6975d379cc49145c3040" -dependencies = [ - "proc-macro-crate", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -1265,15 +1199,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1403,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e880e0b1f9c7a8db874642c1217f7e19b29e325f24ab9f0fcb11818adec7f01" dependencies = [ "cbor4ii", - "cid 0.10.1", + "cid", "scopeguard", "serde", ] @@ -1441,17 +1366,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.8" @@ -1526,25 +1440,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "strobe-rs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabb238a1cccccfa4c4fb703670c0d157e1256c1ba695abf1b93bd2bb14bab2d" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "keccak", - "subtle", - "zeroize", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "1.0.109" @@ -1867,12 +1762,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" -[[package]] -name = "unsigned-varint" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" - [[package]] name = "url" version = "2.5.0" @@ -2165,23 +2054,3 @@ dependencies = [ "cfg-if", "windows-sys 0.48.0", ] - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.41", -] diff --git a/dwn-server/Cargo.toml b/dwn-server/Cargo.toml index e1fcaeb..fee6c89 100644 --- a/dwn-server/Cargo.toml +++ b/dwn-server/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true [dependencies] axum = "0.7.2" +base64 = "0.21.5" dwn = { version = "0.0.2", path = "../dwn" } serde_json = "1.0.108" tokio = { version = "1.35.0", features = ["full", "tracing"] } @@ -17,4 +18,3 @@ tracing-subscriber = "0.3.18" [dev-dependencies] port_check = "0.1.5" reqwest = { version = "0.11.22", features = ["json"] } -serde_json = "1.0.108" diff --git a/dwn-server/src/lib.rs b/dwn-server/src/lib.rs index 54ef765..ef2f9ad 100644 --- a/dwn-server/src/lib.rs +++ b/dwn-server/src/lib.rs @@ -1,16 +1,21 @@ -use std::{collections::BTreeMap, net::SocketAddr}; - use axum::{ http::StatusCode, response::{IntoResponse, Response}, routing::post, Json, Router, }; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; use dwn::{ - features::{FeatureDetection, Record}, - request::{Message, Method, RecordIdGenerator, RequestBody}, + data::{Data, JsonData}, + features::{FeatureDetection, Records}, + request::{ + media_types::{Application, MediaType}, + Message, Method, RecordIdGenerator, RequestBody, + }, response::{MessageResult, ResponseBody, Status}, }; +use std::{collections::BTreeMap, net::SocketAddr}; use tracing::{error, info, span, warn}; pub struct StartOptions { @@ -86,18 +91,39 @@ fn process_message(message: &Message) -> Result { - // TODO: Validate data_cid + let data_format = match &message.descriptor.data_format { + Some(data_format) => data_format, + None => { + return Err("Message has data but dataFormat is None".into()); } + }; + + let data_cid = match &message.descriptor.data_cid { + Some(data_cid) => data_cid, None => { return Err("Message has data but dataCid is None".into()); } }; - if message.descriptor.data_format.is_none() { - return Err("Message has data but dataFormat is None".into()); + // Validate data_cid + // Message data -(base64)-> raw data -> ipld -> ipld-pb -> cid + let raw_data = URL_SAFE_NO_PAD + .decode(message.data.as_ref().unwrap()) + .expect("Failed to decode base64url"); + + let calculated_data_cid = match data_format { + MediaType::Application(Application::Json) => { + let data = JsonData(serde_json::from_slice(&raw_data)?); + data.data_cid() + } + _ => { + return Err("Data format not supported".into()); + } }; + + if calculated_data_cid != *data_cid { + return Err("Data CID not valid".into()); + } } // Process message @@ -108,9 +134,9 @@ fn process_message(message: &Message) -> Result u16 { let port = port_check::free_local_port().expect("Failed to find free port"); @@ -47,6 +45,18 @@ fn empty_message() -> Message { builder.build().expect("Failed to build message") } +async fn expect_status(body: RequestBody, port: u16, status: StatusCode) { + let res = send_post(body, port) + .await + .json::() + .await + .expect("Failed to parse response body"); + + for reply in res.replies.unwrap().iter() { + assert_eq!(reply.status.code, status); + } +} + #[tokio::test] async fn recieve_post() { let port = spawn_server(); @@ -97,18 +107,6 @@ async fn feature_detection() { serde_json::from_value::(entry).expect("Failed to parse feature detection"); } -async fn expect_status(body: RequestBody, port: u16, status: StatusCode) { - let res = send_post(body, port) - .await - .json::() - .await - .expect("Failed to parse response body"); - - for reply in res.replies.unwrap().iter() { - assert_eq!(reply.status.code, status); - } -} - #[tokio::test] async fn requires_valid_record_id() { let port = spawn_server(); @@ -139,21 +137,28 @@ async fn requires_valid_record_id() { async fn requires_data_descriptors() { let port = spawn_server(); - let mut msg = empty_message(); - msg.data = Some("test data".to_string()); - msg.descriptor.data_cid = Some("test data cid".to_string()); - msg.descriptor.data_format = Some(DataFormat::MediaType(MediaType::Application( - Application::Json, - ))); + let msg = MessageBuilder::::new( + Interface::FeatureDetection, + Method::FeatureDetectionRead, + JsonData(json!({ + "foo": "bar", + })), + ) + .build() + .expect("Failed to build message"); let mut without_cid = msg.clone(); without_cid.descriptor.data_cid = None; + without_cid.record_id = without_cid.generate_record_id().unwrap(); let mut without_format = msg.clone(); without_format.descriptor.data_format = None; + without_format.record_id = without_format.generate_record_id().unwrap(); let mut without_both = msg.clone(); without_both.descriptor.data_cid = None; + without_both.descriptor.data_format = None; + without_both.record_id = without_both.generate_record_id().unwrap(); let body = RequestBody { messages: vec![without_cid, without_format, without_both], diff --git a/dwn/Cargo.toml b/dwn/Cargo.toml index 723d8ba..2910541 100644 --- a/dwn/Cargo.toml +++ b/dwn/Cargo.toml @@ -8,13 +8,12 @@ license.workspace = true [dependencies] base64 = "0.21.5" -cid = "0.11.0" iana-media-types = "0.1.2" libipld-cbor = "0.16.0" libipld-core = "0.16.0" libipld-json = "0.16.0" libipld-pb = "0.16.0" -multihash-codetable = { version = "0.1.1", features = ["digest", "sha2"] } serde = { version = "1.0.193", features = ["derive"] } serde_ipld_dagcbor = "0.4.2" serde_json = "1.0.108" +tracing = "0.1.40" diff --git a/dwn/src/data.rs b/dwn/src/data.rs index 854426e..2eda7ab 100644 --- a/dwn/src/data.rs +++ b/dwn/src/data.rs @@ -1,27 +1,107 @@ -use base64::Engine; -use iana_media_types::Application; +use std::collections::BTreeMap; + +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use iana_media_types::{Application, MediaType}; use libipld_core::{codec::Codec, ipld::Ipld}; use libipld_json::DagJsonCodec; +use libipld_pb::DagPbCodec; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::request::DataFormat; +use crate::util::cid_from_bytes; pub trait Data { /// Returns the data as a base64url-encoded string. fn to_base64url(&self) -> String; /// Returns the data as an IPLD object. fn to_ipld(&self) -> Ipld; + /// Returns the data as a DAG-PB encoded byte array. + fn to_pb(&self) -> Vec { + let ipld = self.to_ipld(); + + let data = ipld_to_pb(ipld); + + tracing::info!("DAG-PB: {:?}", data); + + DagPbCodec.encode(&data).expect("Failed to encode IPLD") + } + /// Returns the CID of the DAG-PB encoded data. + fn data_cid(&self) -> String { + let pb = self.to_pb(); + let cid = cid_from_bytes(DagPbCodec.into(), &pb); + cid.to_string() + } /// Returns the data format of this data. - fn data_format(&self) -> DataFormat; + fn data_format(&self) -> MediaType; +} + +/// Converts to a DAG-PB compatible IPLD object +fn ipld_to_pb(ipld: Ipld) -> Ipld { + let mut links = Vec::::new(); + + let data: Vec = match ipld { + Ipld::Null => Vec::new(), + Ipld::String(str) => str.into(), + Ipld::Bytes(bytes) => bytes, + Ipld::Integer(int) => int.to_be_bytes().to_vec(), + Ipld::Bool(boolean) => { + if boolean { + vec![1] + } else { + vec![0] + } + } + Ipld::Map(map) => { + for (key, value) in map { + let mut pb_link = BTreeMap::::new(); + pb_link.insert("Name".to_string(), key.into()); + + let value = ipld_to_pb(value); + let cid = cid_from_bytes(DagPbCodec.into(), &DagPbCodec.encode(&value).unwrap()); + pb_link.insert("Hash".to_string(), cid.into()); + + links.push(pb_link.into()); + } + + Vec::new() + } + Ipld::List(list) => { + for value in list { + let value = ipld_to_pb(value); + let cid = cid_from_bytes(DagPbCodec.into(), &DagPbCodec.encode(&value).unwrap()); + + links.push(cid.into()); + } + + Vec::new() + } + Ipld::Link(cid) => { + let mut pb_link = BTreeMap::::new(); + pb_link.insert("Hash".to_string(), cid.into()); + + links.push(pb_link.into()); + + Vec::new() + } + Ipld::Float(float) => float.to_be_bytes().to_vec(), + }; + + let mut pb_node = BTreeMap::::new(); + pb_node.insert("Links".to_string(), links.into()); + + if !data.is_empty() { + pb_node.insert("Data".to_string(), data.into()); + } + + pb_node.into() } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct JsonData(Value); +pub struct JsonData(pub Value); impl Data for JsonData { fn to_base64url(&self) -> String { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0.to_string()) + URL_SAFE_NO_PAD.encode(self.0.to_string()) } fn to_ipld(&self) -> Ipld { @@ -30,8 +110,8 @@ impl Data for JsonData { DagJsonCodec.decode(bytes).expect("Failed to decode JSON") } - fn data_format(&self) -> DataFormat { - DataFormat::MediaType(Application::Json.into()) + fn data_format(&self) -> MediaType { + Application::Json.into() } } @@ -47,7 +127,7 @@ mod tests { })); assert_eq!(data.to_base64url(), "eyJmb28iOiJiYXIifQ"); - assert_eq!(data.data_format().to_string(), "\"application/json\""); + assert_eq!(data.data_format().to_string(), "application/json"); let ipld = data.to_ipld(); let encoded = libipld_json::DagJsonCodec @@ -55,6 +135,6 @@ mod tests { .expect("Failed to encode IPLD"); let encoded_string = String::from_utf8(encoded).expect("Failed to convert to string"); - assert_eq!(encoded_string, "{\"foo\":\"bar\"}"); + assert_eq!(encoded_string, r#"{"foo":"bar"}"#); } } diff --git a/dwn/src/features.rs b/dwn/src/features.rs index d431461..801c3f3 100644 --- a/dwn/src/features.rs +++ b/dwn/src/features.rs @@ -54,14 +54,14 @@ impl Display for Protocol { } #[derive(Debug, Deserialize, Serialize)] -pub enum Record { +pub enum Records { RecordsCommit, RecordsDelete, RecordsQuery, RecordsWrite, } -impl Display for Record { +impl Display for Records { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { serde_json::to_string(self).unwrap().fmt(f) } diff --git a/dwn/src/request.rs b/dwn/src/request.rs index 2d5a6c1..5a15c0d 100644 --- a/dwn/src/request.rs +++ b/dwn/src/request.rs @@ -1,5 +1,6 @@ pub use iana_media_types as media_types; use libipld_cbor::DagCborCodec; +use media_types::MediaType; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -19,29 +20,43 @@ pub struct Message { pub descriptor: Descriptor, } +impl Message { + pub fn new(data: Option, descriptor: Descriptor) -> Self { + let mut msg = Message { + data, + descriptor, + record_id: "".to_string(), + }; + + msg.record_id = msg.generate_record_id().unwrap(); + + msg + } + + pub fn generate_record_id(&self) -> Result> { + let generator = RecordIdGenerator::try_from(&self.descriptor)?; + let record_id = generator.generate_cid()?; + Ok(record_id) + } +} + pub struct MessageBuilder { pub data: Option, pub descriptor: DescriptorBuilder, } impl MessageBuilder { - pub fn build(&self) -> Result> { - let data = self.data.as_ref().map(|d| d.to_base64url()); - let descriptor = self.descriptor.build(self.data.as_ref())?; - let record_id = self.generate_record_id()?; - - Ok(Message { - data, - descriptor, - record_id, - }) + pub fn new(interface: Interface, method: Method, data: T) -> MessageBuilder { + MessageBuilder { + data: Some(data), + descriptor: DescriptorBuilder { interface, method }, + } } - pub fn generate_record_id(&self) -> Result> { + pub fn build(&self) -> Result> { + let data = self.data.as_ref().map(|d| d.to_base64url()); let descriptor = self.descriptor.build(self.data.as_ref())?; - let generator = RecordIdGenerator::try_from(&descriptor)?; - let record_id = generator.generate_cid()?; - Ok(record_id) + Ok(Message::new(data, descriptor)) } } @@ -77,7 +92,7 @@ pub struct Descriptor { #[serde(rename = "dataCid", skip_serializing_if = "Option::is_none")] pub data_cid: Option, #[serde(rename = "dataFormat", skip_serializing_if = "Option::is_none")] - pub data_format: Option, + pub data_format: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -112,30 +127,6 @@ impl Display for Method { } } -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -#[serde(untagged)] -pub enum DataFormat { - /// JSON Web Token formatted Verifiable Credential - #[serde(rename = "application/vc+jwt")] - VcJWT, - /// JSON-LD formatted Verifiable Credential - #[serde(rename = "application/vc+ldp")] - VcLDP, - MediaType(media_types::MediaType), -} - -impl Display for DataFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - String::from(self).fmt(f) - } -} - -impl From<&DataFormat> for String { - fn from(data_format: &DataFormat) -> Self { - serde_json::to_string(data_format).unwrap() - } -} - pub struct DescriptorBuilder { pub interface: Interface, pub method: Method, @@ -146,12 +137,7 @@ impl DescriptorBuilder { &self, data: Option<&T>, ) -> Result> { - let data_cid = data.map(|d| { - // TODO: Generate CID - let _pb = d.to_ipld(); - "TODO".to_string() - }); - + let data_cid = data.map(|d| d.data_cid()); let data_format = data.map(|d| d.data_format()); Ok(Descriptor { diff --git a/dwn/src/util.rs b/dwn/src/util.rs index 58e70e1..21da7b7 100644 --- a/dwn/src/util.rs +++ b/dwn/src/util.rs @@ -1,5 +1,7 @@ -use cid::Cid; -use multihash_codetable::{Code, MultihashDigest}; +use libipld_core::{ + cid::Cid, + multihash::{Code, MultihashDigest}, +}; pub fn cid_from_bytes(codec: u64, bytes: &[u8]) -> Cid { let hash = Code::Sha2_256.digest(bytes);