From f7dd8235cbaa7e74866decb8cdf72386ecef2928 Mon Sep 17 00:00:00 2001 From: Nathaniel Cook Date: Wed, 25 Jan 2023 08:10:22 -0700 Subject: [PATCH] feat: add DAG-JOSE implementation This is an implementation of DAG-JOSE. There are also a few minor changes to existing code. * An update to the dag-cbor-derive code in order to allow usage of the macro from within this repo. * Add a `Bytes` type to dag-cbor to make declaring bytes within a struct more straightforward. * A dag-jose feature There are some things left to do before I would consider this PR ready. However I would like feedback on the general design first. You can see its usage in the fixtures.rs test case. * Provide complete examples in docs * Determine what to do about the fixture tests. We likely want to move that logic to https://github.com/ipld/codec-fixtures * Add Ipld conversion to the types to avoid round-tripping through CBOR for JSON encoding. --- Cargo.toml | 4 + dag-cbor-derive/Cargo.toml | 1 + dag-cbor-derive/examples/basic.rs | 6 +- .../examples/internal-package/Cargo.toml | 13 + .../examples/internal-package/src/lib.rs | 12 + dag-cbor-derive/src/gen.rs | 40 +- dag-cbor-derive/src/lib.rs | 42 +- dag-cbor/src/lib.rs | 5 + dag-jose/Cargo.toml | 28 + dag-jose/src/codec.rs | 356 +++++++++++ dag-jose/src/error.rs | 16 + dag-jose/src/lib.rs | 400 +++++++++++++ dag-jose/tests/fixtures.rs | 109 ++++ dag-jose/tests/fixtures/dag-jose.md | 558 ++++++++++++++++++ src/lib.rs | 2 + 15 files changed, 1566 insertions(+), 26 deletions(-) create mode 100644 dag-cbor-derive/examples/internal-package/Cargo.toml create mode 100644 dag-cbor-derive/examples/internal-package/src/lib.rs create mode 100644 dag-jose/Cargo.toml create mode 100644 dag-jose/src/codec.rs create mode 100644 dag-jose/src/error.rs create mode 100644 dag-jose/src/lib.rs create mode 100644 dag-jose/tests/fixtures.rs create mode 100644 dag-jose/tests/fixtures/dag-jose.md diff --git a/Cargo.toml b/Cargo.toml index 07312ec9..706d51b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ fnv = "1.0.7" libipld-cbor = { version = "0.16.0", path = "dag-cbor", optional = true } libipld-cbor-derive = { version = "0.16.0", path = "dag-cbor-derive", optional = true } libipld-core = { version = "0.16.0", path = "core" } +libipld-jose = { version = "0.16.0", path = "dag-jose", optional = true } libipld-json = { version = "0.16.0", path = "dag-json", optional = true } libipld-macro = { version = "0.16.0", path = "macro" } libipld-pb = { version = "0.16.0", path = "dag-pb", optional = true } @@ -33,6 +34,7 @@ multihash = "0.18.0" [features] default = ["dag-cbor", "dag-json", "dag-pb", "derive"] dag-cbor = ["libipld-cbor"] +dag-jose = ["libipld-jose"] dag-json = ["libipld-json"] dag-pb = ["libipld-pb"] derive = ["libipld-cbor-derive"] @@ -44,10 +46,12 @@ members = [ "core", "dag-cbor", "dag-cbor-derive", + "dag-jose", "dag-json", "dag-pb", "macro", "dag-cbor-derive/examples/renamed-package", + "dag-cbor-derive/examples/internal-package", ] [profile.release] diff --git a/dag-cbor-derive/Cargo.toml b/dag-cbor-derive/Cargo.toml index 57dcd643..9069ead0 100644 --- a/dag-cbor-derive/Cargo.toml +++ b/dag-cbor-derive/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/ipfs-rust/rust-ipld" proc-macro = true [dependencies] +anyhow = "1.0.68" proc-macro-crate = "1.1.0" proc-macro2 = "1.0.27" quote = "1.0.9" diff --git a/dag-cbor-derive/examples/basic.rs b/dag-cbor-derive/examples/basic.rs index 18858bcf..473b5bfc 100644 --- a/dag-cbor-derive/examples/basic.rs +++ b/dag-cbor-derive/examples/basic.rs @@ -1,4 +1,4 @@ -use libipld::cbor::DagCborCodec; +use libipld::cbor::{Bytes, DagCborCodec}; use libipld::codec::assert_roundtrip; use libipld::{ipld, DagCbor, Ipld}; use std::collections::BTreeMap; @@ -9,7 +9,7 @@ struct NamedStruct { integer: u32, float: f64, string: String, - bytes: Vec, + bytes: Bytes, list: Vec, map: BTreeMap, //link: Cid, @@ -44,7 +44,7 @@ fn main() { "integer": 0, "float": 0.0, "string": "", - "bytes": [], + "bytes": vec![].into_boxed_slice(), "list": [], "map": {}, }), diff --git a/dag-cbor-derive/examples/internal-package/Cargo.toml b/dag-cbor-derive/examples/internal-package/Cargo.toml new file mode 100644 index 00000000..eeb5282b --- /dev/null +++ b/dag-cbor-derive/examples/internal-package/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "internal-package" +version = "0.1.0" +edition = "2021" +publish = false + +[package.metadata.release] +release = false + +[dependencies] +libipld-core = { path = "../../../core"} +libipld-cbor = { path = "../../../dag-cbor"} +libipld-cbor-derive = { path = "../../../dag-cbor-derive"} diff --git a/dag-cbor-derive/examples/internal-package/src/lib.rs b/dag-cbor-derive/examples/internal-package/src/lib.rs new file mode 100644 index 00000000..cb31fe00 --- /dev/null +++ b/dag-cbor-derive/examples/internal-package/src/lib.rs @@ -0,0 +1,12 @@ +//! The purpose of this example is to test whether the derive compiles if the libipld package was +//! imported from within this repo as libipld_core +use libipld_cbor; +use libipld_cbor_derive::DagCborInternal; + +#[derive(Clone, DagCborInternal, Debug, Default, PartialEq)] +struct NamedStruct { + boolean: bool, + integer: u32, + float: f64, + string: String, +} diff --git a/dag-cbor-derive/src/gen.rs b/dag-cbor-derive/src/gen.rs index 529e53e2..dbbd48de 100644 --- a/dag-cbor-derive/src/gen.rs +++ b/dag-cbor-derive/src/gen.rs @@ -4,49 +4,57 @@ use crate::ast::*; use proc_macro2::TokenStream; use quote::quote; -pub fn gen_encode(ast: &SchemaType, libipld: &syn::Ident) -> TokenStream { +pub fn gen_encode( + ast: &SchemaType, + libipld_core: &TokenStream, + libipld_cbor: &TokenStream, +) -> TokenStream { let (ident, generics, body) = match ast { SchemaType::Struct(s) => (&s.name, s.generics.as_ref().unwrap(), gen_encode_struct(s)), SchemaType::Union(u) => (&u.name, &u.generics, gen_encode_union(u)), }; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let trait_name = quote!(#libipld::codec::Encode<#libipld::cbor::DagCborCodec>); + let trait_name = quote!(#libipld_core::codec::Encode<#libipld_cbor::DagCborCodec>); quote! { impl #impl_generics #trait_name for #ident #ty_generics #where_clause { fn encode( &self, - c: #libipld::cbor::DagCborCodec, + c: #libipld_cbor::DagCborCodec, w: &mut W, - ) -> #libipld::Result<()> { - use #libipld::codec::Encode; - use #libipld::cbor::cbor::MajorKind; - use #libipld::cbor::encode::{write_null, write_u8, write_u64}; + ) -> #libipld_core::error::Result<()> { + use #libipld_core::codec::Encode; + use #libipld_cbor::cbor::MajorKind; + use #libipld_cbor::encode::{write_null, write_u8, write_u64}; #body } } } } -pub fn gen_decode(ast: &SchemaType, libipld: &syn::Ident) -> TokenStream { +pub fn gen_decode( + ast: &SchemaType, + libipld_core: &TokenStream, + libipld_cbor: &TokenStream, +) -> TokenStream { let (ident, generics, body) = match ast { SchemaType::Struct(s) => (&s.name, s.generics.as_ref().unwrap(), gen_decode_struct(s)), SchemaType::Union(u) => (&u.name, &u.generics, gen_decode_union(u)), }; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let trait_name = quote!(#libipld::codec::Decode<#libipld::cbor::DagCborCodec>); + let trait_name = quote!(#libipld_core::codec::Decode<#libipld_cbor::DagCborCodec>); quote! { impl #impl_generics #trait_name for #ident #ty_generics #where_clause { fn decode( - c: #libipld::cbor::DagCborCodec, + c: #libipld_cbor::DagCborCodec, r: &mut R, - ) -> #libipld::Result { - use #libipld::cbor::cbor::{MajorKind, NULL}; - use #libipld::cbor::decode::{read_uint, read_major}; - use #libipld::cbor::error::{LengthOutOfRange, MissingKey, UnexpectedCode, UnexpectedKey}; - use #libipld::codec::Decode; - use #libipld::error::Result; + ) -> #libipld_core::error::Result { + use #libipld_cbor::cbor::{MajorKind, NULL}; + use #libipld_cbor::decode::{read_uint, read_major}; + use #libipld_cbor::error::{LengthOutOfRange, MissingKey, UnexpectedCode, UnexpectedKey}; + use #libipld_core::codec::Decode; + use #libipld_core::error::Result; use std::io::SeekFrom; #body } diff --git a/dag-cbor-derive/src/lib.rs b/dag-cbor-derive/src/lib.rs index 25c0f453..a27d9fc3 100644 --- a/dag-cbor-derive/src/lib.rs +++ b/dag-cbor-derive/src/lib.rs @@ -1,23 +1,47 @@ +#![deny(warnings)] + +use anyhow::anyhow; use proc_macro2::{Span, TokenStream}; use proc_macro_crate::{crate_name, FoundCrate}; -use quote::quote; +use quote::{quote, ToTokens}; use synstructure::{decl_derive, Structure}; decl_derive!([DagCbor, attributes(ipld)] => dag_cbor_derive); +decl_derive!([DagCborInternal, attributes(ipld)] => dag_cbor_derive_internal); + mod ast; mod attr; mod gen; mod parse; +// Entry point for the DagCbor derive macro fn dag_cbor_derive(s: Structure) -> TokenStream { - let libipld = match use_crate("libipld") { + let libipld_core = match use_crate("libipld") { Ok(ident) => ident, Err(error) => return error, }; + let libipld_cbor = quote!(#libipld_core::cbor); + let ast = parse::parse(&s); + let encode = gen::gen_encode(&ast, &libipld_core, &libipld_cbor); + let decode = gen::gen_decode(&ast, &libipld_core, &libipld_cbor); + quote! { + #encode + #decode + } +} + +// Entry point for the DagCborCrate derive macro +// This variant of the macro may be used within libipld itself +// as it uses the API exposed by the sub-crates within the workspace directly +// instead of the API exposed by the top level libipld crate. +fn dag_cbor_derive_internal(s: Structure) -> TokenStream { + let libipld_core = quote!(libipld_core); + let libipld_cbor = quote!(libipld_cbor); + let ast = parse::parse(&s); - let encode = gen::gen_encode(&ast, &libipld); - let decode = gen::gen_decode(&ast, &libipld); + let encode = gen::gen_encode(&ast, &libipld_core, &libipld_cbor); + let decode = gen::gen_decode(&ast, &libipld_core, &libipld_cbor); quote! { #encode #decode @@ -28,10 +52,14 @@ fn dag_cbor_derive(s: Structure) -> TokenStream { /// /// This works even if the crate was renamed in the `Cargo.toml` file. If the crate is not a /// dependency, it will lead to a compile-time error. -fn use_crate(name: &str) -> Result { +fn use_crate(name: &str) -> Result { match crate_name(name) { - Ok(FoundCrate::Name(n)) => Ok(syn::Ident::new(&n, Span::call_site())), - Ok(FoundCrate::Itself) => Ok(syn::Ident::new("crate", Span::call_site())), + Ok(FoundCrate::Name(n)) => Ok(syn::Ident::new(&n, Span::call_site()).to_token_stream()), + Ok(FoundCrate::Itself) => Err(syn::Error::new( + Span::call_site(), + anyhow!("unsupported use of dag-cbor-derive macro from within libipld crate"), + ) + .to_compile_error()), Err(err) => Err(syn::Error::new(Span::call_site(), err).to_compile_error()), } } diff --git a/dag-cbor/src/lib.rs b/dag-cbor/src/lib.rs index 00cf7547..3efa24ba 100644 --- a/dag-cbor/src/lib.rs +++ b/dag-cbor/src/lib.rs @@ -36,6 +36,11 @@ pub trait DagCbor: Encode + Decode {} impl + Decode> DagCbor for T {} +/// Bytes is sequence of byte values. +/// +/// Implements Encode and Decode to/from CBOR byte strings +pub type Bytes = Box<[u8]>; + #[cfg(test)] mod tests { use super::*; diff --git a/dag-jose/Cargo.toml b/dag-jose/Cargo.toml new file mode 100644 index 00000000..1de8a2fc --- /dev/null +++ b/dag-jose/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "libipld-jose" +version = "0.16.0" +authors = ["Nathaniel Cook "] +edition = "2021" +license = "MIT OR Apache-2.0" +description = "ipld dag-json codec" +repository = "https://github.com/ipfs-rust/rust-ipld" + +[dependencies] +anyhow = "1.0.68" +base64-url = "1.4.13" +libipld-core = { version = "0.16.0", path = "../core" } +libipld-cbor = { version = "0.16.0", path = "../dag-cbor" } +libipld-cbor-derive = { version = "0.16.0", path = "../dag-cbor-derive" } +# TODO gate behind the dag-json feature flag +libipld-json = { version = "0.16.0", path = "../dag-json" } +multihash = "0.18.0" +thiserror = "1.0.38" + +[dev-dependencies] +libipld-json = { version = "0.16.0", path = "../dag-json" } +libipld-macro = { version = "0.16.0", path = "../macro" } +assert-json-diff = "2.0.2" +hex = "0.4.3" +once_cell = "1.17.0" +serde_json = "1.0.91" +testmark = {git = "https://github.com/bsundsrud/rust-testmark" } diff --git a/dag-jose/src/codec.rs b/dag-jose/src/codec.rs new file mode 100644 index 00000000..aeedd70f --- /dev/null +++ b/dag-jose/src/codec.rs @@ -0,0 +1,356 @@ +//! Codec provides two flavors of structures for encoding and decoding. +//! +//! Encoded* structures represent structures that use binary data +//! Decoded* structures represent structures that use base64 data +//! +//! From implementation are provided between Encoded and Decoded types. +#![deny(missing_docs)] +#![deny(warnings)] + +use std::collections::BTreeMap; + +use libipld_cbor::Bytes; +use libipld_cbor_derive::DagCborInternal; +use libipld_core::cid::Cid; +use libipld_core::ipld::Ipld; + +use crate::{error::Error, JsonWebEncryption}; +use crate::{Jose, Signature}; +use crate::{JsonWebSignature, Recipient}; + +/// Encoded represents the union of fields from JSON Web Signature (JWS) +/// and JSON Web Encryption (JWE) objects. +/// +/// The data is repsented as bytes and therefore can be encoded +/// into a DAG-JOSE object using DAG-CBOR. +/// +/// See https://ipld.io/specs/codecs/dag-jose/spec/#format +#[derive(PartialEq, Default, Debug, DagCborInternal)] +pub struct Encoded { + // JWS fields + #[ipld(default = None)] + pub payload: Option, + #[ipld(default = Vec::new())] + pub signatures: Vec, + + // JWE fields + #[ipld(default = None)] + pub aad: Option, + #[ipld(default = None)] + pub ciphertext: Option, + #[ipld(default = None)] + pub iv: Option, + #[ipld(default = None)] + pub protected: Option, + #[ipld(default = Vec::new())] + pub recipients: Vec, + #[ipld(default = None)] + pub tag: Option, + #[ipld(default = BTreeMap::new())] + pub unprotected: BTreeMap, +} + +impl TryFrom for Encoded { + type Error = Error; + + fn try_from(mut value: Decoded) -> Result { + Ok(Self { + payload: Option::from_base64(value.payload)?, + signatures: value + .signatures + .drain(..) + .map(EncodedSignature::try_from) + .collect::, Self::Error>>()?, + aad: Option::from_base64(value.aad)?, + ciphertext: Option::from_base64(value.ciphertext)?, + iv: Option::from_base64(value.iv)?, + protected: Option::from_base64(value.protected)?, + recipients: value + .recipients + .drain(..) + .map(EncodedRecipient::try_from) + .collect::, Self::Error>>()?, + tag: Option::from_base64(value.tag)?, + unprotected: value.unprotected, + }) + } +} + +impl TryFrom for Encoded { + type Error = Error; + + fn try_from(value: JsonWebSignature) -> Result { + let decoded: Decoded = value.into(); + decoded.try_into() + } +} + +impl TryFrom for Encoded { + type Error = Error; + + fn try_from(value: JsonWebEncryption) -> Result { + let decoded: Decoded = value.into(); + decoded.try_into() + } +} +impl TryFrom for Encoded { + type Error = Error; + + fn try_from(value: Jose) -> Result { + let decoded: Decoded = value.into(); + decoded.try_into() + } +} + +#[derive(PartialEq, Default, Debug, DagCborInternal)] +pub struct EncodedSignature { + #[ipld(default = BTreeMap::new())] + header: BTreeMap, + #[ipld(default = None)] + protected: Option, + signature: Bytes, +} + +impl TryFrom for EncodedSignature { + type Error = Error; + + fn try_from(value: DecodedSignature) -> Result { + Ok(Self { + header: value.header, + protected: Option::from_base64(value.protected)?, + signature: Bytes::from_base64(value.signature)?, + }) + } +} + +#[derive(PartialEq, Default, Debug, DagCborInternal)] +pub struct EncodedRecipient { + #[ipld(default = None)] + encrypted_key: Option, + #[ipld(default = BTreeMap::new())] + header: BTreeMap, +} + +impl TryFrom for EncodedRecipient { + type Error = Error; + + fn try_from(value: DecodedRecipient) -> Result { + Ok(Self { + encrypted_key: Option::from_base64(value.encrypted_key)?, + header: value.header, + }) + } +} + +/// Decoded represents the union of fields from JSON Web Signature (JWS) +/// and JSON Web Encryption (JWE) objects. +/// +/// The data is repsented as base64 URL encoded strings and enabling +/// direct conversion into a publicly exposed struct. +/// +/// See https://ipld.io/specs/codecs/dag-jose/spec/#decoded-jose +#[derive(PartialEq, Default, Debug, DagCborInternal)] +pub struct Decoded { + // JWS fields + #[ipld(default = None)] + pub payload: Option, + #[ipld(default = Vec::new())] + pub signatures: Vec, + #[ipld(default = None)] + pub link: Option, + + // JWE fields + #[ipld(default = None)] + pub aad: Option, + #[ipld(default = None)] + pub ciphertext: Option, + #[ipld(default = None)] + pub iv: Option, + #[ipld(default = None)] + pub protected: Option, + #[ipld(default = Vec::new())] + pub recipients: Vec, + #[ipld(default = None)] + pub tag: Option, + #[ipld(default = BTreeMap::new())] + pub unprotected: BTreeMap, +} + +impl From for Decoded { + fn from(mut value: Encoded) -> Self { + let link = value + .payload + .as_ref() + .map(|v| Cid::try_from(&**v)) + .transpose() + .expect("TODO"); + Self { + payload: value.payload.to_base64(), + signatures: value + .signatures + .drain(..) + .map(DecodedSignature::from) + .collect(), + link, + aad: value.aad.to_base64(), + ciphertext: value.ciphertext.to_base64(), + iv: value.iv.to_base64(), + protected: value.protected.to_base64(), + recipients: value + .recipients + .drain(..) + .map(DecodedRecipient::from) + .collect(), + tag: value.tag.to_base64(), + unprotected: value.unprotected, + } + } +} + +impl From for Decoded { + fn from(mut value: JsonWebSignature) -> Self { + Self { + payload: Some(value.payload), + signatures: value + .signatures + .drain(..) + .map(DecodedSignature::from) + .collect(), + link: Some(value.link), + aad: None, + ciphertext: None, + iv: None, + protected: None, + recipients: vec![], + tag: None, + unprotected: BTreeMap::new(), + } + } +} +impl From for Decoded { + fn from(mut value: JsonWebEncryption) -> Self { + Self { + payload: None, + signatures: vec![], + link: None, + aad: value.aad, + ciphertext: Some(value.ciphertext), + iv: Some(value.iv), + protected: Some(value.protected), + recipients: value + .recipients + .drain(..) + .map(DecodedRecipient::from) + .collect(), + tag: Some(value.tag), + unprotected: value.unprotected, + } + } +} +impl From for Decoded { + fn from(value: Jose) -> Self { + match value { + Jose::Signature(jws) => Decoded::from(jws), + Jose::Encryption(jwe) => Decoded::from(jwe), + } + } +} + +/// Decoded form of a JWS signature +#[derive(PartialEq, Default, Debug, DagCborInternal)] +pub struct DecodedSignature { + #[ipld(default = BTreeMap::new())] + pub header: BTreeMap, + #[ipld(default = None)] + pub protected: Option, + pub signature: String, +} + +impl From for DecodedSignature { + fn from(value: EncodedSignature) -> Self { + Self { + header: value.header, + protected: value.protected.to_base64(), + signature: value.signature.to_base64(), + } + } +} + +impl From for DecodedSignature { + fn from(value: Signature) -> Self { + Self { + header: value.header, + protected: value.protected, + signature: value.signature, + } + } +} + +/// Decoded form of a JWE recipient +#[derive(PartialEq, Default, Debug, DagCborInternal)] +pub struct DecodedRecipient { + #[ipld(default = None)] + pub encrypted_key: Option, + #[ipld(default = BTreeMap::new())] + pub header: BTreeMap, +} +impl From for DecodedRecipient { + fn from(value: EncodedRecipient) -> Self { + Self { + encrypted_key: value.encrypted_key.to_base64(), + header: value.header, + } + } +} +impl From for DecodedRecipient { + fn from(value: Recipient) -> Self { + Self { + encrypted_key: value.encrypted_key, + header: value.header, + } + } +} + +trait FromBase64: Sized { + type Error; + + /// Decode a value from base64 + fn from_base64(value: T) -> Result; +} + +impl FromBase64 for Bytes { + type Error = Error; + + fn from_base64(value: String) -> Result { + Ok(base64_url::decode(value.as_str())?.into_boxed_slice()) + } +} + +impl FromBase64> for Option +where + U: FromBase64, +{ + type Error = U::Error; + + fn from_base64(value: Option) -> Result { + value.map(|v| U::from_base64(v)).transpose() + } +} + +trait ToBase64: Sized { + /// Encode value to base64 + fn to_base64(self) -> T; +} +impl ToBase64 for Bytes { + fn to_base64(self) -> String { + base64_url::encode(self.as_ref()) + } +} +impl ToBase64> for Option +where + U: ToBase64, +{ + fn to_base64(self) -> Option { + self.map(|v| v.to_base64()) + } +} diff --git a/dag-jose/src/error.rs b/dag-jose/src/error.rs new file mode 100644 index 00000000..0eee96df --- /dev/null +++ b/dag-jose/src/error.rs @@ -0,0 +1,16 @@ +//! JOSE error types. +use base64_url::base64::DecodeError; +use libipld_core::cid; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("data not a JWE value")] + NotJwe, + #[error("data not a JWE value")] + NotJws, + #[error("invalid CID data in payload")] + InvalidCid(#[from] cid::Error), + #[error("invalid base64 url data")] + InvalidBase64Url(#[from] DecodeError), +} diff --git a/dag-jose/src/lib.rs b/dag-jose/src/lib.rs new file mode 100644 index 00000000..6ddfc79c --- /dev/null +++ b/dag-jose/src/lib.rs @@ -0,0 +1,400 @@ +//! Jose codec. +//! TODO +#![deny(missing_docs)] +#![deny(warnings)] + +mod codec; +mod error; + +use std::collections::BTreeMap; + +use libipld_cbor::DagCborCodec; +use libipld_core::cid::Cid; +use libipld_core::codec::Codec; +use libipld_core::codec::{Decode, Encode}; +use libipld_core::error::UnsupportedCodec; +use libipld_core::ipld::Ipld; +use libipld_json::DagJsonCodec; + +use codec::{Decoded, Encoded}; + +use crate::{ + codec::{DecodedRecipient, DecodedSignature, EncodedRecipient, EncodedSignature}, + error::Error, +}; + +/// DAG-JOSE codec +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct DagJoseCodec; + +impl Codec for DagJoseCodec {} + +impl From for u64 { + fn from(_: DagJoseCodec) -> Self { + // Multicode comes from here https://github.com/multiformats/multicodec/blob/master/table.csv + 0x85 + } +} + +impl TryFrom for DagJoseCodec { + type Error = UnsupportedCodec; + + fn try_from(_: u64) -> core::result::Result { + Ok(Self) + } +} + +impl Encode for Ipld { + fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { + self.encode(DagCborCodec, w) + } +} +impl Decode for Ipld { + fn decode( + _c: DagJoseCodec, + r: &mut R, + ) -> anyhow::Result { + Ipld::decode(DagCborCodec, r) + } +} + +/// A JSON Object Signging and Encryption value as defined in RFC7165. +#[derive(Clone, Debug, PartialEq)] +pub enum Jose { + /// JSON Web Signature value + Signature(JsonWebSignature), + /// JSON Web Encryption value + Encryption(JsonWebEncryption), +} + +impl Encode for Jose { + fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { + let encoded: Encoded = self.clone().try_into()?; + encoded.encode(DagCborCodec, w) + } +} +impl Decode for Jose { + fn decode( + _c: DagJoseCodec, + r: &mut R, + ) -> anyhow::Result { + let encoded = Encoded::decode(DagCborCodec, r)?; + match encoded.payload { + Some(_) => Ok(Jose::Signature(encoded.try_into()?)), + None => Ok(Jose::Encryption(encoded.try_into()?)), + } + } +} +// TODO put this behind feature flag +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), + } + } +} + +/// A JSON Web Signature object as defined in RFC7515. +#[derive(Clone, Debug, PartialEq)] +pub struct JsonWebSignature { + /// The payload base64 url encoded. + // TODO Create a Base64Url encoded string type? + pub payload: String, + + /// The set of signatures. + pub signatures: Vec, + + /// CID link from the payload. + pub link: Cid, +} + +impl Encode for JsonWebSignature { + fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { + let encoded: Encoded = self.clone().try_into()?; + encoded.encode(DagCborCodec, w) + } +} +impl Decode for JsonWebSignature { + fn decode( + _c: DagJoseCodec, + r: &mut R, + ) -> anyhow::Result { + Ok(Encoded::decode(DagCborCodec, r)?.try_into()?) + } +} +// TODO put this behind feature flag +impl Encode for JsonWebSignature { + fn encode(&self, c: DagJsonCodec, w: &mut W) -> anyhow::Result<()> { + let decoded: Decoded = self.clone().try_into()?; + // TODO: add direct conversion of Decoded type to Ipld + let mut bytes = Vec::new(); + decoded.encode(DagCborCodec, &mut bytes)?; + let data: Ipld = DagCborCodec.decode(&bytes)?; + data.encode(c, w) + } +} + +impl TryFrom for JsonWebSignature { + type Error = Error; + + fn try_from(mut value: Decoded) -> Result { + Ok(Self { + payload: value.payload.ok_or(Error::NotJws)?, + signatures: value.signatures.drain(..).map(Signature::from).collect(), + link: value.link.ok_or(Error::NotJws)?, + }) + } +} + +impl TryFrom for JsonWebSignature { + type Error = Error; + + fn try_from(value: Encoded) -> Result { + let decoded: Decoded = value.into(); + decoded.try_into() + } +} + +/// A signature part of a JSON Web Signature. +#[derive(Clone, Debug, PartialEq)] +pub struct Signature { + /// The optional unprotected header. + pub header: BTreeMap, + /// The protected header as a JSON object base64 url encoded. + pub protected: Option, + /// The web signature base64 url encoded. + pub signature: String, +} + +impl From for Signature { + fn from(value: DecodedSignature) -> Self { + Self { + header: value.header, + protected: value.protected, + signature: value.signature, + } + } +} +impl From for Signature { + fn from(value: EncodedSignature) -> Self { + let decoded: DecodedSignature = value.into(); + decoded.into() + } +} + +/// A JSON Web Encryption object as defined in RFC7516. +#[derive(Clone, Debug, PartialEq)] +pub struct JsonWebEncryption { + /// The optional additional authenticated data. + pub aad: Option, + + /// The ciphertext value resulting from authenticated encryption of the + /// plaintext with additional authenticated data. + pub ciphertext: String, + + /// Initialization Vector value used when encrypting the plaintext base64 url encoded. + pub iv: String, + + /// The protected header as a JSON object base64 url encoded. + pub protected: String, + + /// The set of recipients. + pub recipients: Vec, + + /// The authentication tag value resulting from authenticated encryption. + pub tag: String, + + /// The optional unprotected header. + pub unprotected: BTreeMap, +} +impl Encode for JsonWebEncryption { + fn encode(&self, _c: DagJoseCodec, w: &mut W) -> anyhow::Result<()> { + let encoded: Encoded = self.clone().try_into()?; + encoded.encode(DagCborCodec, w) + } +} +impl Decode for JsonWebEncryption { + fn decode( + _c: DagJoseCodec, + r: &mut R, + ) -> anyhow::Result { + Ok(Encoded::decode(DagCborCodec, r)?.try_into()?) + } +} +// TODO put this behind feature flag +impl Encode for JsonWebEncryption { + fn encode(&self, c: DagJsonCodec, w: &mut W) -> anyhow::Result<()> { + let decoded: Decoded = self.clone().try_into()?; + // TODO: add direct conversion of Decoded type to Ipld + let mut bytes = Vec::new(); + decoded.encode(DagCborCodec, &mut bytes)?; + let data: Ipld = DagCborCodec.decode(&bytes)?; + data.encode(c, w) + } +} + +impl TryFrom for JsonWebEncryption { + type Error = Error; + + fn try_from(mut value: Decoded) -> Result { + Ok(Self { + aad: value.aad, + ciphertext: value.ciphertext.ok_or(Error::NotJwe)?, + iv: value.iv.ok_or(Error::NotJwe)?, + protected: value.protected.ok_or(Error::NotJwe)?, + recipients: value.recipients.drain(..).map(Recipient::from).collect(), + tag: value.tag.ok_or(Error::NotJwe)?, + unprotected: value.unprotected, + }) + } +} +impl TryFrom for JsonWebEncryption { + type Error = Error; + + fn try_from(value: Encoded) -> Result { + let decoded: Decoded = value.into(); + decoded.try_into() + } +} + +/// A recipient of a JSON Web Encryption message. +#[derive(Clone, Debug, PartialEq)] +pub struct Recipient { + /// The encrypted content encryption key value. + pub encrypted_key: Option, + + /// The optional unprotected header. + pub header: BTreeMap, +} + +impl From for Recipient { + fn from(value: DecodedRecipient) -> Self { + Self { + encrypted_key: value.encrypted_key, + header: value.header, + } + } +} + +impl From for Recipient { + fn from(value: EncodedRecipient) -> Self { + let decoded: DecodedRecipient = value.into(); + decoded.into() + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + + use libipld_core::{cid::Cid, codec::assert_roundtrip}; + use libipld_macro::ipld; + + fn fixture_jws() -> (Box<[u8]>, Box<[u8]>, Box<[u8]>) { + let payload = + base64_url::decode("AXESIIlVZVHDkmZ5zFLHLhgqVhkFakcnQJ7pOibQWtcnyhH0").unwrap(); + let protected = base64_url::decode("eyJhbGciOiJFZERTQSJ9").unwrap(); + let signature = base64_url::decode("-_9J5OZcl5lVuRlgI1NJEzc0FqEb6_2yVskUaQPducRQ4oe-N5ynCl57wDm4SPtm1L1bltrphpQeBOeWjVW1BQ").unwrap(); + ( + payload.into_boxed_slice(), + protected.into_boxed_slice(), + signature.into_boxed_slice(), + ) + } + fn fixture_jws_base64( + payload: &Box<[u8]>, + protected: &Box<[u8]>, + signature: &Box<[u8]>, + ) -> (String, String, String) { + ( + base64_url::encode(payload.as_ref()), + base64_url::encode(protected.as_ref()), + base64_url::encode(signature.as_ref()), + ) + } + fn fixture_jwe() -> (Box<[u8]>, Box<[u8]>, Box<[u8]>, Box<[u8]>) { + let ciphertext = base64_url::decode("3XqLW28NHP-raqW8vMfIHOzko4N3IRaR").unwrap(); + let iv = base64_url::decode("PSWIuAyO8CpevzCL").unwrap(); + let protected = base64_url::decode("eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0").unwrap(); + let tag = base64_url::decode("WZAMBblhzDCsQWOAKdlkSA").unwrap(); + ( + ciphertext.into_boxed_slice(), + iv.into_boxed_slice(), + protected.into_boxed_slice(), + tag.into_boxed_slice(), + ) + } + fn fixture_jwe_base64( + ciphertext: &Box<[u8]>, + iv: &Box<[u8]>, + protected: &Box<[u8]>, + tag: &Box<[u8]>, + ) -> (String, String, String, String) { + ( + base64_url::encode(ciphertext.as_ref()), + base64_url::encode(iv.as_ref()), + base64_url::encode(protected.as_ref()), + base64_url::encode(tag.as_ref()), + ) + } + #[test] + fn roundtrip_jws() { + let (payload, protected, signature) = fixture_jws(); + let (payload_b64, protected_b64, signature_b64) = + fixture_jws_base64(&payload, &protected, &signature); + let link = Cid::try_from(base64_url::decode(&payload_b64).unwrap()).unwrap(); + assert_roundtrip( + DagJoseCodec, + &JsonWebSignature { + payload: payload_b64, + signatures: vec![Signature { + header: BTreeMap::from([ + ("k0".to_string(), Ipld::from("v0")), + ("k1".to_string(), Ipld::from(1)), + ]), + protected: Some(protected_b64), + signature: signature_b64, + }], + link, + }, + &ipld!({ + "payload": payload, + "signatures": [{ + "header": { + "k0": "v0", + "k1": 1 + }, + "protected": protected, + "signature": signature, + }], + }), + ); + } + #[test] + fn roundtrip_jwe() { + let (ciphertext, iv, protected, tag) = fixture_jwe(); + let (ciphertext_b64, iv_b64, protected_b64, tag_b64) = + fixture_jwe_base64(&ciphertext, &iv, &protected, &tag); + assert_roundtrip( + DagJoseCodec, + &JsonWebEncryption { + aad: None, + ciphertext: ciphertext_b64, + iv: iv_b64, + protected: protected_b64, + recipients: vec![], + tag: tag_b64, + unprotected: BTreeMap::new(), + }, + &ipld!({ + "ciphertext": ciphertext, + "iv": iv, + "protected": protected, + "tag": tag, + }), + ); + } +} diff --git a/dag-jose/tests/fixtures.rs b/dag-jose/tests/fixtures.rs new file mode 100644 index 00000000..4c49a5c1 --- /dev/null +++ b/dag-jose/tests/fixtures.rs @@ -0,0 +1,109 @@ +#![deny(missing_docs)] +#![deny(warnings)] + +use anyhow::Result; +use assert_json_diff::assert_json_eq; +use once_cell::sync::Lazy; +use std::{io::Cursor, path::PathBuf, sync::Mutex}; +use testmark::{Document, Hunk}; + +use libipld_core::codec::{Decode, Encode}; +use libipld_json::DagJsonCodec; + +use libipld_jose::*; + +// Load the fixtures file once +static FIXTURES: Lazy> = Lazy::new(|| { + let fpath = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("dag-jose.md"); + Document::from_file(&fpath) + .expect("fixture file dag-jose.md should be a markdown file") + .into() +}); + +// Find hunks of data +trait HunkFinder<'a>: Sized { + fn find_hunk(self, name: &str) -> Option<&'a Hunk>; + fn must_find_hunk(self, name: &str) -> &'a Hunk { + self.find_hunk(name) + .expect(format!("fixture should have hunk: {}", name).as_str()) + } +} +// Implement hunk finder for a Document +impl<'a> HunkFinder<'a> for &'a Document { + fn find_hunk(self, name: &str) -> Option<&'a Hunk> { + self.hunks().iter().find(|h| h.name() == name) + } +} + +// remove whitespace from anywhere inside of an utf-8 encoded byte slice. +fn remove_whitespace(data: &[u8]) -> Result { + let s = String::from_utf8(data.to_vec())?; + Ok(s.chars().filter(|c| !c.is_whitespace()).collect()) +} + +macro_rules! test_fixture { + ($fname:ident,$name:expr) => { + #[test] + fn $fname() { + decode_re_encode( + concat!($name, "/serial.dag-jose.hex"), + concat!($name, "/datamodel.dag-json.pretty"), + ) + } + }; +} + +// Decode hex data into DAG-JOSE and re-encode into DAG-JSON +// in order to compare against fixture data. +fn decode_re_encode(hex_name: &str, json_name: &str) { + let fixtures = match FIXTURES.lock() { + Ok(f) => f, + // We can ignore poisoned errors since + // we only need read only access to the fixture and + // any failed test will point the mutex. + Err(poisoned) => poisoned.into_inner(), + }; + // 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"), + ), + ) + .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) + .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"); + + // Load expected JSON data + let dag_json = fixtures.must_find_hunk(json_name); + // Compare JSON representations are the same + assert_json_eq!( + serde_json::from_slice::(dag_json.data()) + .expect("DAG-JSON data should be JSON"), + serde_json::from_slice::(&bytes).expect("bytes should be JSON"), + ); +} + +test_fixture!(jws, "jws"); +test_fixture!(jws_signature_1, "jws-signature-1"); +test_fixture!(jws_signature_2, "jws-signature-2"); +test_fixture!(jws_signatures, "jws-signatures"); +test_fixture!(jwe_symmetric, "jwe-symmetric"); +test_fixture!(jwe_asymmetric, "jwe-asymmetric"); +test_fixture!(jwe_no_recipients, "jwe-no-recipients"); +test_fixture!(jwe_recipient, "jwe-recipient"); +test_fixture!(jwe_recipients, "jwe-recipients"); diff --git a/dag-jose/tests/fixtures/dag-jose.md b/dag-jose/tests/fixtures/dag-jose.md new file mode 100644 index 00000000..744d2f1e --- /dev/null +++ b/dag-jose/tests/fixtures/dag-jose.md @@ -0,0 +1,558 @@ +DAG-JOSE Fixtures +================= + +About this document +------------------- + +This document contains test fixtures for the [DAG-JOSE](..) codec, and documentation describing them. + +These fixtures are executable by parsing them using the [testmark format](https://github.com/warpfork/go-testmark). +If you're reading this file as markdown, or rendered to the web, +know that all of the codeblocks here are data that can be easily programmatically extracted and used to drive tests in your implementation. + +These fixtures offer several different kinds of data: + +- DAG-JOSE serial data -- in hexadecimal. (DAG-JOSE is a binary format, so hexadecimal is easier to work with here than the possibly-unprintable binary raw form.) + - These should be used as both an decode fixture, and as an encode fixture (the data should round-trip). +- The list of paths that should be seen in that data, when parsed as [data model](/docs/data-model/). (This is a good fixture because it's very human-readable, very easy to test, and also provides a form of documentation.) +- Prettyprinted [DAG-JSON](/docs/codecs/known/dag-json/) data, which is the DAG-JOSE data, parsed, then re-encoded as DAG-JSON, and pretty-printed. (This gives deep coverage, because it effectively examines every bit of the [data model](/docs/data-model/) view of the data.) + +Fixtures +-------- + +### JWS + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jws/serial.dag-jose.cid) +``` +bagcqceraxvt5izt4sz7kjfrm42dxrutp6ijywgsacllkznzekmfojypkvfea +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jws/serial.dag-jose.hex) +``` +a2677061796c6f616458240171122089556551c3926679cc52c72e182a5619056a4727409ee93a26 +d05ad727ca11f46a7369676e61747572657381a26970726f7465637465644f7b22616c67223a2245 +64445341227d697369676e61747572655840fbff49e4e65c979955b9196023534913373416a11beb +fdb256c9146903ddb9c450e287be379ca70a5e7bc039b848fb66d4bd5b96dae986941e04e7968d55 +b505 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jws/paths) +```text +link +payload +signatures +signatures/0 +signatures/0/protected +signatures/0/signature +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jws/datamodel.dag-json.pretty) +```json +{ + "link": { + "/": "bafyreiejkvsvdq4smz44yuwhfymcuvqzavveoj2at3utujwqlllspsqr6q" + }, + "payload": "AXESIIlVZVHDkmZ5zFLHLhgqVhkFakcnQJ7pOibQWtcnyhH0", + "signatures": [ + { + "protected": "eyJhbGciOiJFZERTQSJ9", + "signature": "-_9J5OZcl5lVuRlgI1NJEzc0FqEb6_2yVskUaQPducRQ4oe-N5ynCl57wDm4SPtm1L1bltrphpQeBOeWjVW1BQ" + } + ] +} +``` + +### JWS with one signature + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jws-signature-1/serial.dag-jose.cid) +``` +bagcqcerauben4l6ee2wjf2fnkj7vaels4p7lnytenk35j3gl2lzcbtbgyoea +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jws-signature-1/serial.dag-jose.hex) +``` +a2677061796c6f6164582401701220debd7adb3ce56544d22a6f6b93396f6980a8067c2cc134f0f7 +801b6331092b956a7369676e61747572657381a26970726f746563746564507b22616c67223a2245 +533235364b227d697369676e617475726558404a26065d6ed88be2b16e92252cd9aed25121adac95 +ef2a5a002e3d180710feaa53b2d656f3d333e82a7c5655045fea95b2062373ef7ed73bcb703625c4 +eb2bd6 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jws-signature-1/paths) +```text +link +payload +signatures +signatures/0 +signatures/0/protected +signatures/0/signature +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jws-signature-1/datamodel.dag-json.pretty) +```json +{ + "link": { + "/": "bafybeig6xv5nwphfmvcnektpnojts33jqcuam7bmye2pb54adnrtccjlsu" + }, + "payload": "AXASIN69ets85WVE0ipva5M5b2mAqAZ8LME08PeAG2MxCSuV", + "signatures": [ + { + "protected": "eyJhbGciOiJFUzI1NksifQ", + "signature": "SiYGXW7Yi-KxbpIlLNmu0lEhrayV7ypaAC49GAcQ_qpTstZW89Mz6Cp8VlUEX-qVsgYjc-9-1zvLcDYlxOsr1g" + } + ] +} +``` + +### JWS with another signature + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jws-signature-2/serial.dag-jose.cid) +``` +bagcqceravvw4bx7jgkxxjwfuqo2yoja6w4cmvmu3gkew3s7yu3vt2ce7riwa +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jws-signature-2/serial.dag-jose.hex) +``` +a2677061796c6f6164582401701220debd7adb3ce56544d22a6f6b93396f6980a8067c2cc134f0f7 +801b6331092b956a7369676e61747572657381a26970726f746563746564507b22616c67223a2245 +533235364b227d697369676e6174757265584043c3dd4c4e40e4dddad24b4edb035d5329ae987952 +c4d17d4a2dfc22fcec31a4990badf2430f9b24da4a7fe51e2453c7edc0f363b8cb8361bfbe27a3a7 +b36a5e +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jws-signature-2/paths) +```text +link +payload +signatures +signatures/0 +signatures/0/protected +signatures/0/signature +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jws-signature-2/datamodel.dag-json.pretty) +```json +{ + "link": { + "/": "bafybeig6xv5nwphfmvcnektpnojts33jqcuam7bmye2pb54adnrtccjlsu" + }, + "payload": "AXASIN69ets85WVE0ipva5M5b2mAqAZ8LME08PeAG2MxCSuV", + "signatures": [ + { + "protected": "eyJhbGciOiJFUzI1NksifQ", + "signature": "Q8PdTE5A5N3a0ktO2wNdUymumHlSxNF9Si38IvzsMaSZC63yQw-bJNpKf-UeJFPH7cDzY7jLg2G_viejp7NqXg" + } + ] +} +``` + +### JWS with multiple signatures + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jws-signatures/serial.dag-jose.cid) +``` +bagcqcera542h3xc57nudkgjcceexyzyxrkwi4ikbn773ag6dqdcyjt6z6rga +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jws-signatures/serial.dag-jose.hex) +``` +a2677061796c6f6164582401701220debd7adb3ce56544d22a6f6b93396f6980a8067c2cc134f0f7 +801b6331092b956a7369676e61747572657382a26970726f746563746564507b22616c67223a2245 +533235364b227d697369676e617475726558404a26065d6ed88be2b16e92252cd9aed25121adac95 +ef2a5a002e3d180710feaa53b2d656f3d333e82a7c5655045fea95b2062373ef7ed73bcb703625c4 +eb2bd6a26970726f746563746564507b22616c67223a2245533235364b227d697369676e61747572 +65584043c3dd4c4e40e4dddad24b4edb035d5329ae987952c4d17d4a2dfc22fcec31a4990badf243 +0f9b24da4a7fe51e2453c7edc0f363b8cb8361bfbe27a3a7b36a5e +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jws-signatures/paths) +```text +link +payload +signatures +signatures/0 +signatures/0/protected +signatures/0/signature +signatures/1 +signatures/1/protected +signatures/1/signature +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jws-signatures/datamodel.dag-json.pretty) +```json +{ + "link": { + "/": "bafybeig6xv5nwphfmvcnektpnojts33jqcuam7bmye2pb54adnrtccjlsu" + }, + "payload": "AXASIN69ets85WVE0ipva5M5b2mAqAZ8LME08PeAG2MxCSuV", + "signatures": [ + { + "protected": "eyJhbGciOiJFUzI1NksifQ", + "signature": "SiYGXW7Yi-KxbpIlLNmu0lEhrayV7ypaAC49GAcQ_qpTstZW89Mz6Cp8VlUEX-qVsgYjc-9-1zvLcDYlxOsr1g" + }, + { + "protected": "eyJhbGciOiJFUzI1NksifQ", + "signature": "Q8PdTE5A5N3a0ktO2wNdUymumHlSxNF9Si38IvzsMaSZC63yQw-bJNpKf-UeJFPH7cDzY7jLg2G_viejp7NqXg" + } + ] +} +``` + +### JWE symmetric + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jwe-symmetric/serial.dag-jose.cid) +``` +bagcqceraxazmu67crshzqdeg3kwnfschs25epy5sbtqtjre2qw3d62kzplva +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jwe-symmetric/serial.dag-jose.hex) +``` +a46269764c3d2588b80c8ef02a5ebf308b637461675059900c05b961cc30ac41638029d964486970 +726f746563746564581d7b22616c67223a22646972222c22656e63223a224131323847434d227d6a +636970686572746578745818dd7a8b5b6f0d1cffab6aa5bcbcc7c81cece4a38377211691 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jwe-symmetric/paths) +```text +ciphertext +iv +protected +tag +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jwe-symmetric/datamodel.dag-json.pretty) +```json +{ + "ciphertext": "3XqLW28NHP-raqW8vMfIHOzko4N3IRaR", + "iv": "PSWIuAyO8CpevzCL", + "protected": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0", + "tag": "WZAMBblhzDCsQWOAKdlkSA" +} +``` + +### JWE asymmetric + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jwe-asymmetric/serial.dag-jose.cid) +``` +bagcqceraqfknq7xaemcihmq2albau32ttrutxnco7xeoik6mlejismmvw5zq +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jwe-asymmetric/serial.dag-jose.hex) +``` +a56269764c43cc693edff366b7ef1e047e63746167508f11e3715bacbb4cab381cf0f84c79cc6970 +726f74656374656458267b22616c67223a225253412d4f4145502d323536222c22656e63223a2241 +32353647434d227d6a6369706865727465787458185936b0e967aa85a6430e179dcc6627b2dcb848 +c47e4733b06a726563697069656e747381a16d656e637279707465645f6b657959010012a61a3787 +45107d2f5f88d4dddefaf21c0e61281984496f543cea748280e8f147b0be0f3f027b108b9e6cbaf1 +c00049a97581346d245014631ee0afe75565c6f5fd5a81fd8a4ed07b0d3244e086063fc025e7f5e4 +f899cd4554430b5e1d50ee490b3839cb0c7e3d7ccdcfce6ec907bd431a3743dd08103e1bfffbac64 +76b5c61224ecb06efc6aa9310db0264dbdab0ac4748eb80fdb8d5b1199696850cc0b264adb4313c0 +b15730a0f8c0605d9a810f03e340061551331a68500cb23e3a95c8f807d9601bab13b7ff7d4fcdb1 +8b6a9858f9bd9f65bd5095d50b196bf1d1850941047c634cac8206e7d8fb41b8a26b769f3621c8ef +93687e3879487cd47173c3 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jwe-asymmetric/paths) +```text +ciphertext +iv +protected +recipients +recipients/0 +recipients/0/encrypted_key +tag +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jwe-asymmetric/datamodel.dag-json.pretty) +```json +{ + "ciphertext": "WTaw6WeqhaZDDhedzGYnsty4SMR-RzOw", + "iv": "Q8xpPt_zZrfvHgR-", + "protected": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0", + "recipients": [ + { + "encrypted_key": "EqYaN4dFEH0vX4jU3d768hwOYSgZhElvVDzqdIKA6PFHsL4PPwJ7EIuebLrxwABJqXWBNG0kUBRjHuCv51VlxvX9WoH9ik7Qew0yROCGBj_AJef15PiZzUVUQwteHVDuSQs4OcsMfj18zc_ObskHvUMaN0PdCBA-G__7rGR2tcYSJOywbvxqqTENsCZNvasKxHSOuA_bjVsRmWloUMwLJkrbQxPAsVcwoPjAYF2agQ8D40AGFVEzGmhQDLI-OpXI-AfZYBurE7f_fU_NsYtqmFj5vZ9lvVCV1QsZa_HRhQlBBHxjTKyCBufY-0G4omt2nzYhyO-TaH44eUh81HFzww" + } + ], + "tag": "jxHjcVusu0yrOBzw-Ex5zA" +} +``` + +### JWE with no recipients + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jwe-no-recipients/serial.dag-jose.cid) +``` +bagcqcerakjv2mmdlbai3urym22bw5kaw7nqov73yaxf6xjnp7e56sclsrooa +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jwe-no-recipients/serial.dag-jose.hex) +``` +a4626976581888973bef4a3aa96b35e709599cdac66986339c874ad12aef6374616750127fb33242 +5d0e8ed041ef6bab18cb3c6970726f746563746564581b7b22616c67223a22646972222c22656e63 +223a225843323050227d6a636970686572746578745824342a13fe20e72eab6a9161d04680bfe356 +1f199cb97462d2e894f7a89a18af39c7cf90d6 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jwe-no-recipients/paths) +```text +ciphertext +iv +protected +tag +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jwe-no-recipients/datamodel.dag-json.pretty) +```json +{ + "ciphertext": "NCoT_iDnLqtqkWHQRoC_41YfGZy5dGLS6JT3qJoYrznHz5DW", + "iv": "iJc770o6qWs15wlZnNrGaYYznIdK0Srv", + "protected": "eyJhbGciOiJkaXIiLCJlbmMiOiJYQzIwUCJ9", + "tag": "En-zMkJdDo7QQe9rqxjLPA" +} +``` + +### JWE with one recipient + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jwe-recipient/serial.dag-jose.cid) +``` +bagcqcera7azagcqlpu4ivvh4xp4iv6psmb5d7eki6ln3fnfnsnbb2hzv4nxq +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jwe-recipient/serial.dag-jose.hex) +``` +a5626976581851f600419a8d3f70ccaee5aec5ae140800db167d0f28f8ff6374616750961d1d6a0a +53ed277b6f6329a62337bf6970726f7465637465644f7b22656e63223a225843323050227d6a6369 +706865727465787458244a12a86f8ea14ae9a91349a877adddc875221b9062c52c175191276aced6 +fd0c1f71ecb66a726563697069656e747381a266686561646572a4626976782057587a76414e595f +7466736f3765764861764f66372d627858556b2d786e4f5063616c676f454344482d45532b584332 +30504b576365706ba36178782b6b6d614b427478426c4566477056527044305a4d6b316266774e33 +5667466359556b505a49396f5544456f6363727666583235353139636b7479634f4b506374616776 +3371595348304f374a5a4b5764355f3734436d2d5a676d656e637279707465645f6b65795820c27c +58cc7f55108f13720fd46836fe8d534b91fa5a837c5e63f6b5a7b00c1f67 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jwe-recipient/paths) +```text +ciphertext +iv +protected +recipients +recipients/0 +recipients/0/header +recipients/0/header/iv +recipients/0/header/alg +recipients/0/header/epk +recipients/0/header/epk/x +recipients/0/header/epk/crv +recipients/0/header/epk/kty +recipients/0/header/tag +recipients/0/encrypted_key +tag +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jwe-recipient/datamodel.dag-json.pretty) +```json +{ + "ciphertext": "ShKob46hSumpE0mod63dyHUiG5BixSwXUZEnas7W_Qwfcey2", + "iv": "UfYAQZqNP3DMruWuxa4UCADbFn0PKPj_", + "protected": "eyJlbmMiOiJYQzIwUCJ9", + "recipients": [ + { + "encrypted_key": "wnxYzH9VEI8Tcg_UaDb-jVNLkfpag3xeY_a1p7AMH2c", + "header": { + "alg": "ECDH-ES+XC20PKW", + "epk": { + "crv": "X25519", + "kty": "OKP", + "x": "kmaKBtxBlEfGpVRpD0ZMk1bfwN3VgFcYUkPZI9oUDEo" + }, + "iv": "WXzvANY_tfso7evHavOf7-bxXUk-xnOP", + "tag": "3qYSH0O7JZKWd5_74Cm-Zg" + } + } + ], + "tag": "lh0dagpT7Sd7b2MppiM3vw" +} +``` + +### JWE with multiple recipients + +This is the base64url-encoded CID for a DAG-JOSE object, when using SHA2-256 (multihash code 0x12): + +[testmark]:# (jwe-recipients/serial.dag-jose.cid) +``` +bagcqcera5uvz2qai6l4vmqjigwpowluilxngz3dyjnva2s3uwbfb5u4ao4fa +``` + +This is a DAG-JOSE object, in hexadecimal: + +[testmark]:# (jwe-recipients/serial.dag-jose.hex) +``` +a56269765818f3f3c92467c191b2a33f703edc72bf09f6538392160737746374616750803e4b5f3e +3f87518fe1776ff52feef96970726f7465637465644f7b22656e63223a225843323050227d6a6369 +706865727465787458246dfdc48e4b46a6b77ba5443c9b3538ba47bcff8283c0de99f8f1c15fae56 +d0994da891bd6a726563697069656e747382a266686561646572a4626976782041674c795961746c +4a6e4771586f39466159356173794e4d6a694b664d64745a63616c676f454344482d45532b584332 +30504b576365706ba36178782b45707255565a424b7a644b575766644a6437724a672d2d5f385a68 +6b414e65356e7a686c653056704179676363727666583235353139636b7479634f4b506374616776 +365f7532354f747055425756587a76765064707053416d656e637279707465645f6b657958200c5c +d81201eb63af7b2dcecc4f29f3bce66f00fc39646085e5c9b0d69ae414daa266686561646572a462 +697678206c7244625738456c63385330675437695f794a52684c4e516b576c41516a4a3363616c67 +6f454344482d45532b58433230504b576365706ba36178782b7a426475443459577068372d4f5349 +703346674646325656417a55443778684766792d6a327061347a5141636372766658323535313963 +6b7479634f4b5063746167764b334b396b593331505137476d50625f4741496f76516d656e637279 +707465645f6b65795820987d496475ef5e759153c355d2493edb6a7007b2e8a188a01026fa6636f7 +7be0 +``` + +When it is parsed, we should see these paths within the data +when we walk over it at the [data model](/docs/data-model/) level: + +[testmark]:# (jwe-recipients/paths) +```text +ciphertext +iv +protected +recipients +recipients/0 +recipients/0/header +recipients/0/header/iv +recipients/0/header/alg +recipients/0/header/epk +recipients/0/header/epk/x +recipients/0/header/epk/crv +recipients/0/header/epk/kty +recipients/0/header/tag +recipients/0/encrypted_key +recipients/1 +recipients/1/header +recipients/1/header/iv +recipients/1/header/alg +recipients/1/header/epk +recipients/1/header/epk/x +recipients/1/header/epk/crv +recipients/1/header/epk/kty +recipients/1/header/tag +recipients/1/encrypted_key +tag +``` + +If we re-encoded this data in [DAG-JSON](/docs/codecs/known/dag-json/) +(and prettyprint it), we should get this result: + +[testmark]:# (jwe-recipients/datamodel.dag-json.pretty) +```json +{ + "ciphertext": "bf3EjktGprd7pUQ8mzU4uke8_4KDwN6Z-PHBX65W0JlNqJG9", + "iv": "8_PJJGfBkbKjP3A-3HK_CfZTg5IWBzd0", + "protected": "eyJlbmMiOiJYQzIwUCJ9", + "recipients": [ + { + "encrypted_key": "DFzYEgHrY697Lc7MTynzvOZvAPw5ZGCF5cmw1prkFNo", + "header": { + "alg": "ECDH-ES+XC20PKW", + "epk": { + "crv": "X25519", + "kty": "OKP", + "x": "EprUVZBKzdKWWfdJd7rJg--_8ZhkANe5nzhle0VpAyg" + }, + "iv": "AgLyYatlJnGqXo9FaY5asyNMjiKfMdtZ", + "tag": "6_u25OtpUBWVXzvvPdppSA" + } + }, + { + "encrypted_key": "mH1JZHXvXnWRU8NV0kk-22pwB7LooYigECb6Zjb3e-A", + "header": { + "alg": "ECDH-ES+XC20PKW", + "epk": { + "crv": "X25519", + "kty": "OKP", + "x": "zBduD4YWph7-OSIp3FgFF2VVAzUD7xhGfy-j2pa4zQA" + }, + "iv": "lrDbW8Elc8S0gT7i_yJRhLNQkWlAQjJ3", + "tag": "K3K9kY31PQ7GmPb_GAIovQ" + } + } + ], + "tag": "gD5LXz4_h1GP4Xdv9S_u-Q" +} +``` diff --git a/src/lib.rs b/src/lib.rs index b9b26978..17e74cf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub use libipld_cbor as cbor; #[cfg(all(feature = "dag-cbor", feature = "derive"))] pub use libipld_cbor_derive::DagCbor; pub use libipld_core::*; +#[cfg(feature = "dag-jose")] +pub use libipld_jose as jose; #[cfg(feature = "dag-json")] pub use libipld_json as json; pub use libipld_macro::*;