From d835817eeaabfe94d4148592fed92018d417aa17 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Tue, 16 Apr 2024 10:53:35 -0600 Subject: [PATCH] feat: update to use ipld-core crate The Rust crate libipld is no longer the _blessed_ implementation of IPLD in Rust. This change updates to use the ipld-core implementation so this crate can interoperate well with the ecosystem. --- Cargo.toml | 28 +++--- src/codec.rs | 18 +++- src/error.rs | 17 +++- src/lib.rs | 244 ++++++++++++++++++++++++++++------------------ tests/fixtures.rs | 22 ++--- 5 files changed, 199 insertions(+), 130 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b58c136..702bb0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,23 +9,21 @@ repository = "https://github.com/ceramicnetwork/rust-dag-jose" resolver = "2" [features] -dag-json = ["dep:libipld-json"] +dag-json = ["dep:serde_ipld_dagjson"] [dependencies] -anyhow = "1.0.69" -base64-url = { version = "2.0.2", feautres = ["std"] } -libipld = { version = "0.16.0", default-features = false, features = [ - "serde-codec", -] } -libipld-json = { version = "0.16.0", default-features = false, optional = true } -serde = "1.0.152" -serde_derive = "1.0.152" -serde_ipld_dagcbor = "0.3.0" -thiserror = "1.0.38" +anyhow = "1" +base64-url = { version = "2.0.2" } +ipld-core = { version = "0.4" } +serde_ipld_dagjson = { version = "0.2", default-features = false, optional = true } +serde_ipld_dagcbor = "0.6" +serde = "1" +serde_derive = "1" +thiserror = "1" [dev-dependencies] -assert-json-diff = "2.0.2" -hex = "0.4.3" -once_cell = "1.17.0" -serde_json = "1.0.92" +assert-json-diff = "2" +hex = "0.4" +once_cell = "1" +serde_json = "1" testmark = { git = "https://github.com/bsundsrud/rust-testmark" } diff --git a/src/codec.rs b/src/codec.rs index c86bcbe..281585c 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -2,7 +2,8 @@ #![deny(missing_docs)] #![deny(warnings)] -use libipld::{Cid, Ipld}; +use ipld_core::cid::Cid; +use ipld_core::ipld::Ipld; use serde_derive::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -25,6 +26,21 @@ pub struct Encoded { // // Within each grouping the fields are defined in their correct // sort order. + // + // Additionally according to the spec: + // + // > Any field which is represented as base64url() we map directly to Bytes. + // + // https://ipld.io/specs/codecs/dag-jose/spec/#mapping-from-the-jose-general-json-serialization-to-dag-jose-serialization + // + // This means that these fields are represented as a Bytes type of the raw bytes of the field + // so they can be DAB-CBOR encoded/decoded as raw bytes not a base64url encoded string.. + // + // When we convert the data to a Jose object we construct the base64url encoded string from the + // raw bytes. + // + // The dag-json feature takes advantage of this to encode the Jose structs directly preserving + // the base64url encoded string. // JWS fields #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/error.rs b/src/error.rs index d3aa574..e5add12 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,5 @@ //! JOSE error types. -use base64_url::base64::DecodeError; -use libipld::cid; +use ipld_core::cid; use thiserror::Error; #[derive(Error, Debug)] @@ -12,5 +11,17 @@ pub enum Error { #[error("invalid CID data in payload")] InvalidCid(#[from] cid::Error), #[error("invalid base64 url data")] - InvalidBase64Url(#[from] DecodeError), + InvalidBase64Url(#[from] base64_url::base64::DecodeError), + #[error("invalid cbor encoding")] + Codec(#[from] serde_ipld_dagcbor::error::CodecError), + #[error("failed encoding")] + CborEncode(#[from] serde_ipld_dagcbor::EncodeError), + #[error("failed decoding")] + CborDecode(#[from] serde_ipld_dagcbor::DecodeError), + #[cfg(feature = "dag-json")] + #[error("failed encoding")] + JsonEncode(#[from] serde_ipld_dagjson::EncodeError), + #[cfg(feature = "dag-json")] + #[error("failed decoding")] + JsonDecode(#[from] serde_ipld_dagjson::DecodeError), } diff --git a/src/lib.rs b/src/lib.rs index 6cb0ec5..18e1b71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,8 @@ //! Structures are provided for encoding and decoding JSON Web Signatures and Encryption values. //! //! ``` -//! use std::io::Cursor; //! use dag_jose::{DagJoseCodec, Jose}; -//! use libipld::codec::{Decode, Encode}; +//! use ipld_core::codec::Codec; //! //! let data = hex::decode(" //! a2677061796c6f616458240171122089556551c3926679cc52c72e182a5619056a4727409ee93a26 @@ -15,11 +14,10 @@ //! b505".chars().filter(|c| !c.is_whitespace()).collect::()).unwrap(); //! //! // Decode binary data into an JOSE value. -//! let jose = Jose::decode(DagJoseCodec, &mut Cursor::new(&data)).unwrap(); +//! let jose: Jose = DagJoseCodec::decode_from_slice(&data).unwrap(); //! //! // Encode an JOSE value into bytes -//! let mut bytes = Vec::new(); -//! jose.encode(DagJoseCodec, &mut bytes).unwrap(); +//! let bytes = DagJoseCodec::encode_to_vec(&jose).unwrap(); //! //! assert_eq!(data, bytes); //! ``` @@ -30,10 +28,9 @@ With the feature `dag-json` the JOSE values may also be encoded to DAG-JSON. ``` - use std::io::Cursor; use dag_jose::{DagJoseCodec, Jose}; - use libipld::codec::{Decode, Encode}; - use libipld_json::DagJsonCodec; + use ipld_core::codec::Codec; + use serde_ipld_dagjson::codec::DagJsonCodec; let data = hex::decode(\" a2677061796c6f616458240171122089556551c3926679cc52c72e182a5619056a4727409ee93a26 @@ -43,11 +40,10 @@ b505\".chars().filter(|c| !c.is_whitespace()).collect::()).unwrap(); // Decode binary data into an JOSE value. - let jose = Jose::decode(DagJoseCodec, &mut Cursor::new(&data)).unwrap(); + let jose: Jose = DagJoseCodec::decode_from_slice(&data).unwrap(); // Encode an JOSE value into DAG-JSON bytes - let mut bytes = Vec::new(); - jose.encode(DagJsonCodec, &mut bytes).unwrap(); + let bytes = DagJsonCodec::encode_to_vec(&jose).unwrap(); assert_eq!(String::from_utf8(bytes).unwrap(), r#\"{ \"link\":{\"/\":\"bafyreiejkvsvdq4smz44yuwhfymcuvqzavveoj2at3utujwqlllspsqr6q\"}, @@ -69,17 +65,18 @@ mod bytes; mod codec; mod error; -use std::{collections::BTreeMap, io::BufReader}; +use std::collections::BTreeMap; -use libipld::error::UnsupportedCodec; -use libipld::Cid; -use libipld::Ipld; -use libipld::{ - codec::{Codec, Decode, Encode}, +use ipld_core::{ + cid::Cid, + codec::{Codec, Links}, ipld, + ipld::Ipld, }; +use serde_derive::Serialize; +use serde_ipld_dagcbor::codec::DagCborCodec; #[cfg(feature = "dag-json")] -use libipld_json::DagJsonCodec; +use serde_ipld_dagjson::codec::DagJsonCodec; use codec::Encoded; @@ -87,34 +84,24 @@ use codec::Encoded; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct DagJoseCodec; -impl Codec for DagJoseCodec {} +impl Links for DagJoseCodec { + type LinksError = error::Error; -impl From for u64 { - fn from(_: DagJoseCodec) -> Self { - // Multicode comes from here https://github.com/multiformats/multicodec/blob/master/table.csv - 0x85 + fn links(bytes: &[u8]) -> Result, Self::LinksError> { + Ok(DagCborCodec::links(bytes)?) } } +impl Codec for DagJoseCodec { + const CODE: u64 = 0x85; -impl TryFrom for DagJoseCodec { - type Error = UnsupportedCodec; + type Error = error::Error; - fn try_from(_: u64) -> core::result::Result { - Ok(Self) + fn decode(reader: R) -> Result { + Ok(serde_ipld_dagcbor::from_reader(reader)?) } -} -impl Encode for Ipld { - fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { - Ok(serde_ipld_dagcbor::to_writer(w, self)?) - } -} -impl Decode for Ipld { - fn decode( - _c: DagJoseCodec, - r: &mut R, - ) -> anyhow::Result { - Ok(serde_ipld_dagcbor::from_reader(BufReader::new(r))?) + fn encode(writer: W, data: &Ipld) -> Result<(), Self::Error> { + Ok(serde_ipld_dagcbor::to_writer(writer, data)?) } } @@ -127,43 +114,51 @@ pub enum Jose { Encryption(JsonWebEncryption), } -impl Encode for Jose { - fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { - let encoded: Encoded = self.try_into()?; - Ok(serde_ipld_dagcbor::to_writer(w, &encoded)?) +impl Codec for DagJoseCodec { + const CODE: u64 = 0x85; + + type Error = error::Error; + + fn decode(reader: R) -> Result { + let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?; + encoded.try_into() } -} -impl Decode for Jose { - fn decode( - _c: DagJoseCodec, - r: &mut R, - ) -> anyhow::Result { - let encoded: Encoded = serde_ipld_dagcbor::from_reader(BufReader::new(r))?; - Ok(encoded.try_into()?) + + fn encode(writer: W, data: &Jose) -> Result<(), Self::Error> { + let encoded: Encoded = data.try_into()?; + Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?) } } #[cfg(feature = "dag-json")] -impl Encode for Jose { - fn encode(&self, c: DagJsonCodec, w: &mut W) -> anyhow::Result<()> { - match self { - Jose::Signature(jws) => jws.encode(c, w), - Jose::Encryption(jwe) => jwe.encode(c, w), +impl Codec for DagJsonCodec { + const CODE: u64 = 0x0129; + type Error = error::Error; + + fn decode(reader: R) -> Result { + let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?; + encoded.try_into() + } + + fn encode(writer: W, data: &Jose) -> Result<(), Self::Error> { + match data { + Jose::Signature(jws) => DagJsonCodec::encode(writer, jws), + Jose::Encryption(jwe) => DagJsonCodec::encode(writer, jwe), } } } /// A JSON Web Signature object as defined in RFC7515. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct JsonWebSignature { + /// CID link from the payload. + pub link: Cid, + /// The payload base64 url encoded. pub payload: String, /// The set of signatures. pub signatures: Vec, - - /// CID link from the payload. - pub link: Cid, } impl<'a> From<&'a JsonWebSignature> for Ipld { @@ -176,33 +171,46 @@ impl<'a> From<&'a JsonWebSignature> for Ipld { } } -impl Encode for JsonWebSignature { - fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { - let encoded: Encoded = self.try_into()?; - Ok(serde_ipld_dagcbor::to_writer(w, &encoded)?) +impl Codec for DagJoseCodec { + const CODE: u64 = 0x85; + + type Error = error::Error; + + fn decode(reader: R) -> Result { + let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?; + encoded.try_into() } -} -impl Decode for JsonWebSignature { - fn decode( - _c: DagJoseCodec, - r: &mut R, - ) -> anyhow::Result { - let encoded: Encoded = serde_ipld_dagcbor::from_reader(BufReader::new(r))?; - Ok(encoded.try_into()?) + + fn encode(writer: W, data: &JsonWebSignature) -> Result<(), Self::Error> { + let encoded: Encoded = data.try_into()?; + Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?) } } + #[cfg(feature = "dag-json")] -impl Encode for JsonWebSignature { - fn encode(&self, c: DagJsonCodec, w: &mut W) -> anyhow::Result<()> { - let data: Ipld = self.into(); - data.encode(c, w) +impl Codec for DagJsonCodec { + const CODE: u64 = 0x0129; + + type Error = error::Error; + + fn decode(reader: R) -> Result { + let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?; + encoded.try_into() + } + + fn encode(writer: W, data: &JsonWebSignature) -> Result<(), Self::Error> { + // Here we directly encode the JsonWebSignature type without using the Encoded type. + // This is because when encoding to DAG-JSON we do not want to encode the payload etc at + // raw bytes but instead encode them as base64url encoded strings. + Ok(serde_ipld_dagjson::to_writer(writer, &data)?) } } /// A signature part of a JSON Web Signature. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct Signature { /// The optional unprotected header. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub header: BTreeMap, /// The protected header as a JSON object base64 url encoded. pub protected: Option, @@ -225,9 +233,10 @@ impl<'a> From<&'a Signature> for Ipld { } /// A JSON Web Encryption object as defined in RFC7516. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct JsonWebEncryption { /// The optional additional authenticated data. + #[serde(skip_serializing_if = "Option::is_none")] pub aad: Option, /// The ciphertext value resulting from authenticated encryption of the @@ -241,12 +250,14 @@ pub struct JsonWebEncryption { pub protected: String, /// The set of recipients. + #[serde(skip_serializing_if = "Vec::is_empty")] pub recipients: Vec, /// The authentication tag value resulting from authenticated encryption. pub tag: String, /// The optional unprotected header. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub unprotected: BTreeMap, } @@ -281,37 +292,50 @@ impl<'a> From<&'a JsonWebEncryption> for Ipld { Ipld::Map(fields) } } -impl Encode for JsonWebEncryption { - fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { - let encoded: Encoded = self.try_into()?; - Ok(serde_ipld_dagcbor::to_writer(w, &encoded)?) + +impl Codec for DagJoseCodec { + const CODE: u64 = 0x85; + + type Error = error::Error; + + fn decode(reader: R) -> Result { + let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?; + encoded.try_into() } -} -impl Decode for JsonWebEncryption { - fn decode( - _c: DagJoseCodec, - r: &mut R, - ) -> anyhow::Result { - let encoded: Encoded = serde_ipld_dagcbor::from_reader(BufReader::new(r))?; - Ok(encoded.try_into()?) + + fn encode(writer: W, data: &JsonWebEncryption) -> Result<(), Self::Error> { + let encoded: Encoded = data.try_into()?; + Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?) } } #[cfg(feature = "dag-json")] -impl Encode for JsonWebEncryption { - fn encode(&self, c: DagJsonCodec, w: &mut W) -> anyhow::Result<()> { - let data: Ipld = self.into(); - data.encode(c, w) +impl Codec for DagJsonCodec { + const CODE: u64 = 0x0129; + + type Error = error::Error; + + fn decode(reader: R) -> Result { + let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?; + encoded.try_into() + } + + fn encode(writer: W, data: &JsonWebEncryption) -> Result<(), Self::Error> { + // Here we directly encode the JsonWebEncryption type without using the Encoded type. + // This is because when encoding to DAG-JSON we do not want to encode the protected field etc as + // raw bytes but instead encode them as base64url encoded strings. + Ok(serde_ipld_dagjson::to_writer(writer, &data)?) } } /// A recipient of a JSON Web Encryption message. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct Recipient { /// The encrypted content encryption key value. pub encrypted_key: Option, /// The optional unprotected header. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] pub header: BTreeMap, } @@ -334,8 +358,6 @@ mod tests { use super::*; - use libipld::{codec::assert_roundtrip, ipld}; - struct JwsFixture { payload: Box<[u8]>, protected: Box<[u8]>, @@ -460,4 +482,32 @@ mod tests { }), ); } + + // Utility for testing codecs. + // + // Encodes the `data` using the codec `c` and checks that it matches the `ipld`. + fn assert_roundtrip(_c: C, data: &T, ipld: &Ipld) + where + C: Codec, + C: Codec, + >::Error: std::fmt::Debug, + >::Error: std::fmt::Debug, + T: std::cmp::PartialEq + std::fmt::Debug, + { + let bytes = C::encode_to_vec(data).unwrap(); + let bytes2 = C::encode_to_vec(ipld).unwrap(); + if bytes != bytes2 { + panic!( + r#"assertion failed: `(left == right)` + left: `{}`, + right: `{}`"#, + hex::encode(&bytes), + hex::encode(&bytes2) + ); + } + let ipld2: Ipld = C::decode_from_slice(&bytes).unwrap(); + assert_eq!(&ipld2, ipld); + let data2: T = C::decode_from_slice(&bytes).unwrap(); + assert_eq!(&data2, data); + } } diff --git a/tests/fixtures.rs b/tests/fixtures.rs index 2af9202..cbbbd29 100644 --- a/tests/fixtures.rs +++ b/tests/fixtures.rs @@ -5,13 +5,12 @@ use anyhow::Result; use assert_json_diff::assert_json_eq; use dag_jose::{DagJoseCodec, Jose}; +use ipld_core::codec::Codec; use once_cell::sync::Lazy; -use std::{io::Cursor, path::PathBuf, sync::Mutex}; +use serde_ipld_dagjson::codec::DagJsonCodec; +use std::{path::PathBuf, sync::Mutex}; use testmark::{Document, Hunk}; -use libipld::codec::{Decode, Encode}; -use libipld_json::DagJsonCodec; - // Load the fixtures file once static FIXTURES: Lazy> = Lazy::new(|| { let fpath = PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -69,24 +68,19 @@ fn decode_re_encode(hex_name: &str, json_name: &str) { // Decode the hex data into a DAG-JOSE value let dag_jose_hex = remove_whitespace(fixtures.must_find_hunk(hex_name).data()) .expect("hex fixture data should be UTF8"); - let jose = Jose::decode( - DagJoseCodec, - &mut Cursor::new( - hex::decode(&dag_jose_hex).expect("hex fixture data should be hex encoded"), - ), + let jose: Jose = DagJoseCodec::decode_from_slice( + &hex::decode(&dag_jose_hex).expect("hex fixture data should be hex encoded"), ) .expect("hex fixture data should represent a DAG-JOSE value"); // Test the we can encode back to the same hex data. - let mut encoded_bytes = Vec::new(); - jose.encode(DagJoseCodec, &mut encoded_bytes) + let encoded_bytes = DagJoseCodec::encode_to_vec(&jose) .expect("encoded DAG-JOSE value should encode to DAG-CBOR"); assert_eq!(dag_jose_hex, hex::encode(encoded_bytes)); // Re-encode the DAG-JOSE value in DAG-JSON - let mut bytes = Vec::new(); - jose.encode(DagJsonCodec, &mut bytes) - .expect("decoded DAG-JOSE value should encode to DAG-CBOR"); + let bytes = DagJsonCodec::encode_to_vec(&jose) + .expect("decoded DAG-JOSE value should encode to DAG-JSON"); println!( "json: {}", String::from_utf8(bytes.clone()).expect("JSON bytes should be valid utf-8")