dag_jose/
lib.rs

1//! DAG-JOSE codec.
2//!
3//! Structures are provided for encoding and decoding JSON Web Signatures and Encryption values.
4//!
5//! ```
6//! use dag_jose::{DagJoseCodec, Jose};
7//! use ipld_core::codec::Codec;
8//!
9//!     let data = hex::decode("
10//! a2677061796c6f616458240171122089556551c3926679cc52c72e182a5619056a4727409ee93a26
11//! d05ad727ca11f46a7369676e61747572657381a26970726f7465637465644f7b22616c67223a2245
12//! 64445341227d697369676e61747572655840fbff49e4e65c979955b9196023534913373416a11beb
13//! fdb256c9146903ddb9c450e287be379ca70a5e7bc039b848fb66d4bd5b96dae986941e04e7968d55
14//! b505".chars().filter(|c| !c.is_whitespace()).collect::<String>()).unwrap();
15//!
16//!     // Decode binary data into an JOSE value.
17//!     let jose: Jose = DagJoseCodec::decode_from_slice(&data).unwrap();
18//!
19//!     // Encode an JOSE value into bytes
20//!     let bytes = DagJoseCodec::encode_to_vec(&jose).unwrap();
21//!
22//!     assert_eq!(data, bytes);
23//! ```
24//!
25#![cfg_attr(
26    feature = "dag-json",
27    doc = "
28 With the feature `dag-json` the JOSE values may also be encoded to DAG-JSON.
29
30 ```
31 use dag_jose::{DagJoseCodec, Jose};
32 use ipld_core::codec::Codec;
33 use serde_ipld_dagjson::codec::DagJsonCodec;
34
35     let data = hex::decode(\"
36 a2677061796c6f616458240171122089556551c3926679cc52c72e182a5619056a4727409ee93a26
37 d05ad727ca11f46a7369676e61747572657381a26970726f7465637465644f7b22616c67223a2245
38 64445341227d697369676e61747572655840fbff49e4e65c979955b9196023534913373416a11beb
39 fdb256c9146903ddb9c450e287be379ca70a5e7bc039b848fb66d4bd5b96dae986941e04e7968d55
40 b505\".chars().filter(|c| !c.is_whitespace()).collect::<String>()).unwrap();
41
42     // Decode binary data into an JOSE value.
43     let jose: Jose = DagJoseCodec::decode_from_slice(&data).unwrap();
44
45     // Encode an JOSE value into DAG-JSON bytes
46     let bytes = DagJsonCodec::encode_to_vec(&jose).unwrap();
47
48     assert_eq!(String::from_utf8(bytes).unwrap(), r#\"{
49         \"link\":{\"/\":\"bafyreiejkvsvdq4smz44yuwhfymcuvqzavveoj2at3utujwqlllspsqr6q\"},
50         \"payload\":\"AXESIIlVZVHDkmZ5zFLHLhgqVhkFakcnQJ7pOibQWtcnyhH0\",
51         \"signatures\":[{
52             \"protected\":\"eyJhbGciOiJFZERTQSJ9\",
53             \"signature\":\"-_9J5OZcl5lVuRlgI1NJEzc0FqEb6_2yVskUaQPducRQ4oe-N5ynCl57wDm4SPtm1L1bltrphpQeBOeWjVW1BQ\"
54         }]}\"#.chars().filter(|c| !c.is_whitespace()).collect::<String>());
55 ```
56 "
57)]
58#![cfg_attr(
59    not(feature = "dag-json"),
60    doc = "Enable the feature 'dag-json' to be able to encode/decode Jose values using DAG-JSON."
61)]
62#![deny(missing_docs)]
63
64mod bytes;
65mod codec;
66mod error;
67
68use std::collections::BTreeMap;
69
70use ipld_core::{
71    cid::Cid,
72    codec::{Codec, Links},
73    ipld,
74    ipld::Ipld,
75};
76use serde_derive::Serialize;
77use serde_ipld_dagcbor::codec::DagCborCodec;
78#[cfg(feature = "dag-json")]
79use serde_ipld_dagjson::codec::DagJsonCodec;
80
81use codec::Encoded;
82
83/// DAG-JOSE codec
84#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
85pub struct DagJoseCodec;
86
87impl Links for DagJoseCodec {
88    type LinksError = error::Error;
89
90    fn links(bytes: &[u8]) -> Result<impl Iterator<Item = Cid>, Self::LinksError> {
91        Ok(DagCborCodec::links(bytes)?)
92    }
93}
94impl Codec<Ipld> for DagJoseCodec {
95    const CODE: u64 = 0x85;
96
97    type Error = error::Error;
98
99    fn decode<R: std::io::BufRead>(reader: R) -> Result<Ipld, Self::Error> {
100        Ok(serde_ipld_dagcbor::from_reader(reader)?)
101    }
102
103    fn encode<W: std::io::Write>(writer: W, data: &Ipld) -> Result<(), Self::Error> {
104        Ok(serde_ipld_dagcbor::to_writer(writer, data)?)
105    }
106}
107
108/// A JSON Object Signing and Encryption value as defined in RFC7165.
109#[derive(Clone, Debug, PartialEq)]
110pub enum Jose {
111    /// JSON Web Signature value
112    Signature(JsonWebSignature),
113    /// JSON Web Encryption value
114    Encryption(JsonWebEncryption),
115}
116
117impl Codec<Jose> for DagJoseCodec {
118    const CODE: u64 = 0x85;
119
120    type Error = error::Error;
121
122    fn decode<R: std::io::BufRead>(reader: R) -> Result<Jose, Self::Error> {
123        let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?;
124        encoded.try_into()
125    }
126
127    fn encode<W: std::io::Write>(writer: W, data: &Jose) -> Result<(), Self::Error> {
128        let encoded: Encoded = data.try_into()?;
129        Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?)
130    }
131}
132
133#[cfg(feature = "dag-json")]
134impl Codec<Jose> for DagJsonCodec {
135    const CODE: u64 = 0x0129;
136    type Error = error::Error;
137
138    fn decode<R: std::io::BufRead>(reader: R) -> Result<Jose, Self::Error> {
139        let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?;
140        encoded.try_into()
141    }
142
143    fn encode<W: std::io::Write>(writer: W, data: &Jose) -> Result<(), Self::Error> {
144        match data {
145            Jose::Signature(jws) => DagJsonCodec::encode(writer, jws),
146            Jose::Encryption(jwe) => DagJsonCodec::encode(writer, jwe),
147        }
148    }
149}
150
151/// A JSON Web Signature object as defined in RFC7515.
152#[derive(Clone, Debug, PartialEq, Serialize)]
153pub struct JsonWebSignature {
154    /// CID link from the payload.
155    pub link: Cid,
156
157    /// The payload base64 url encoded.
158    pub payload: String,
159
160    /// The set of signatures.
161    pub signatures: Vec<Signature>,
162}
163
164impl<'a> From<&'a JsonWebSignature> for Ipld {
165    fn from(value: &'a JsonWebSignature) -> Self {
166        ipld!({
167            "payload": value.payload.to_owned(),
168            "signatures": value.signatures.iter().map(Ipld::from).collect::<Vec<Ipld>>(),
169            "link": value.link,
170        })
171    }
172}
173
174impl Codec<JsonWebSignature> for DagJoseCodec {
175    const CODE: u64 = 0x85;
176
177    type Error = error::Error;
178
179    fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebSignature, Self::Error> {
180        let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?;
181        encoded.try_into()
182    }
183
184    fn encode<W: std::io::Write>(writer: W, data: &JsonWebSignature) -> Result<(), Self::Error> {
185        let encoded: Encoded = data.try_into()?;
186        Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?)
187    }
188}
189
190#[cfg(feature = "dag-json")]
191impl Codec<JsonWebSignature> for DagJsonCodec {
192    const CODE: u64 = 0x0129;
193
194    type Error = error::Error;
195
196    fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebSignature, Self::Error> {
197        let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?;
198        encoded.try_into()
199    }
200
201    fn encode<W: std::io::Write>(writer: W, data: &JsonWebSignature) -> Result<(), Self::Error> {
202        // Here we directly encode the JsonWebSignature type without using the Encoded type.
203        // This is because when encoding to DAG-JSON we do not want to encode the payload etc at
204        // raw bytes but instead encode them as base64url encoded strings.
205        Ok(serde_ipld_dagjson::to_writer(writer, &data)?)
206    }
207}
208
209/// A signature part of a JSON Web Signature.
210#[derive(Clone, Debug, PartialEq, Serialize)]
211pub struct Signature {
212    /// The optional unprotected header.
213    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
214    pub header: BTreeMap<String, Ipld>,
215    /// The protected header as a JSON object base64 url encoded.
216    pub protected: Option<String>,
217    /// The web signature base64 url encoded.
218    pub signature: String,
219}
220
221impl<'a> From<&'a Signature> for Ipld {
222    fn from(value: &'a Signature) -> Self {
223        let mut fields: BTreeMap<String, Ipld> = BTreeMap::new();
224        if !value.header.is_empty() {
225            fields.insert("header".to_string(), value.header.to_owned().into());
226        }
227        if let Some(protected) = value.protected.to_owned() {
228            fields.insert("protected".to_string(), protected.into());
229        };
230        fields.insert("signature".to_string(), value.signature.to_owned().into());
231        Ipld::Map(fields)
232    }
233}
234
235/// A JSON Web Encryption object as defined in RFC7516.
236#[derive(Clone, Debug, PartialEq, Serialize)]
237pub struct JsonWebEncryption {
238    /// The optional additional authenticated data.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub aad: Option<String>,
241
242    /// The ciphertext value resulting from authenticated encryption of the
243    /// plaintext with additional authenticated data.
244    pub ciphertext: String,
245
246    /// Initialization Vector value used when encrypting the plaintext base64 url encoded.
247    pub iv: String,
248
249    /// The protected header as a JSON object base64 url encoded.
250    pub protected: String,
251
252    /// The set of recipients.
253    #[serde(skip_serializing_if = "Vec::is_empty")]
254    pub recipients: Vec<Recipient>,
255
256    /// The authentication tag value resulting from authenticated encryption.
257    pub tag: String,
258
259    /// The optional unprotected header.
260    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
261    pub unprotected: BTreeMap<String, Ipld>,
262}
263
264impl<'a> From<&'a JsonWebEncryption> for Ipld {
265    fn from(value: &'a JsonWebEncryption) -> Self {
266        let mut fields: BTreeMap<String, Ipld> = BTreeMap::new();
267        if let Some(aad) = value.aad.to_owned() {
268            fields.insert("aad".to_string(), aad.into());
269        }
270        fields.insert("ciphertext".to_string(), value.ciphertext.to_owned().into());
271        fields.insert("iv".to_string(), value.iv.to_owned().into());
272        fields.insert("protected".to_string(), value.protected.to_owned().into());
273        if !value.recipients.is_empty() {
274            fields.insert(
275                "recipients".to_string(),
276                value
277                    .recipients
278                    .iter()
279                    .map(Ipld::from)
280                    .collect::<Vec<Ipld>>()
281                    .into(),
282            );
283        }
284
285        fields.insert("tag".to_string(), value.tag.to_owned().into());
286        if !value.unprotected.is_empty() {
287            fields.insert(
288                "unprotected".to_string(),
289                value.unprotected.to_owned().into(),
290            );
291        }
292        Ipld::Map(fields)
293    }
294}
295
296impl Codec<JsonWebEncryption> for DagJoseCodec {
297    const CODE: u64 = 0x85;
298
299    type Error = error::Error;
300
301    fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebEncryption, Self::Error> {
302        let encoded: Encoded = serde_ipld_dagcbor::from_reader(reader)?;
303        encoded.try_into()
304    }
305
306    fn encode<W: std::io::Write>(writer: W, data: &JsonWebEncryption) -> Result<(), Self::Error> {
307        let encoded: Encoded = data.try_into()?;
308        Ok(serde_ipld_dagcbor::to_writer(writer, &encoded)?)
309    }
310}
311
312#[cfg(feature = "dag-json")]
313impl Codec<JsonWebEncryption> for DagJsonCodec {
314    const CODE: u64 = 0x0129;
315
316    type Error = error::Error;
317
318    fn decode<R: std::io::BufRead>(reader: R) -> Result<JsonWebEncryption, Self::Error> {
319        let encoded: Encoded = serde_ipld_dagjson::from_reader(reader)?;
320        encoded.try_into()
321    }
322
323    fn encode<W: std::io::Write>(writer: W, data: &JsonWebEncryption) -> Result<(), Self::Error> {
324        // Here we directly encode the JsonWebEncryption type without using the Encoded type.
325        // This is because when encoding to DAG-JSON we do not want to encode the protected field etc as
326        // raw bytes but instead encode them as base64url encoded strings.
327        Ok(serde_ipld_dagjson::to_writer(writer, &data)?)
328    }
329}
330
331/// A recipient of a JSON Web Encryption message.
332#[derive(Clone, Debug, PartialEq, Serialize)]
333pub struct Recipient {
334    /// The encrypted content encryption key value.
335    pub encrypted_key: Option<String>,
336
337    /// The optional unprotected header.
338    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
339    pub header: BTreeMap<String, Ipld>,
340}
341
342impl<'a> From<&'a Recipient> for Ipld {
343    fn from(value: &'a Recipient) -> Self {
344        let mut fields: BTreeMap<String, Ipld> = BTreeMap::new();
345        if let Some(encrypted_key) = value.encrypted_key.to_owned() {
346            fields.insert("encrypted_key".to_string(), encrypted_key.into());
347        }
348        if !value.header.is_empty() {
349            fields.insert("header".to_string(), value.header.to_owned().into());
350        }
351        Ipld::Map(fields)
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use std::collections::BTreeMap;
358
359    use super::*;
360
361    struct JwsFixture {
362        payload: Box<[u8]>,
363        protected: Box<[u8]>,
364        signature: Box<[u8]>,
365    }
366    fn fixture_jws() -> JwsFixture {
367        let payload =
368            base64_url::decode("AXESIIlVZVHDkmZ5zFLHLhgqVhkFakcnQJ7pOibQWtcnyhH0").unwrap();
369        let protected = base64_url::decode("eyJhbGciOiJFZERTQSJ9").unwrap();
370        let signature =  base64_url::decode("-_9J5OZcl5lVuRlgI1NJEzc0FqEb6_2yVskUaQPducRQ4oe-N5ynCl57wDm4SPtm1L1bltrphpQeBOeWjVW1BQ").unwrap();
371        JwsFixture {
372            payload: payload.into_boxed_slice(),
373            protected: protected.into_boxed_slice(),
374            signature: signature.into_boxed_slice(),
375        }
376    }
377    fn fixture_jws_base64(
378        payload: &[u8],
379        protected: &[u8],
380        signature: &[u8],
381    ) -> (String, String, String) {
382        (
383            base64_url::encode(payload),
384            base64_url::encode(protected),
385            base64_url::encode(signature),
386        )
387    }
388    struct JweFixture {
389        ciphertext: Box<[u8]>,
390        iv: Box<[u8]>,
391        protected: Box<[u8]>,
392        tag: Box<[u8]>,
393    }
394    fn fixture_jwe() -> JweFixture {
395        let ciphertext = base64_url::decode("3XqLW28NHP-raqW8vMfIHOzko4N3IRaR").unwrap();
396        let iv = base64_url::decode("PSWIuAyO8CpevzCL").unwrap();
397        let protected = base64_url::decode("eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0").unwrap();
398        let tag = base64_url::decode("WZAMBblhzDCsQWOAKdlkSA").unwrap();
399        JweFixture {
400            ciphertext: ciphertext.into_boxed_slice(),
401            iv: iv.into_boxed_slice(),
402            protected: protected.into_boxed_slice(),
403            tag: tag.into_boxed_slice(),
404        }
405    }
406    fn fixture_jwe_base64(
407        ciphertext: &[u8],
408        iv: &[u8],
409        protected: &[u8],
410        tag: &[u8],
411    ) -> (String, String, String, String) {
412        (
413            base64_url::encode(ciphertext),
414            base64_url::encode(iv),
415            base64_url::encode(protected),
416            base64_url::encode(tag),
417        )
418    }
419    #[test]
420    fn roundtrip_jws() {
421        let JwsFixture {
422            payload,
423            protected,
424            signature,
425        } = fixture_jws();
426        let (payload_b64, protected_b64, signature_b64) =
427            fixture_jws_base64(&payload, &protected, &signature);
428        let link = Cid::try_from(base64_url::decode(&payload_b64).unwrap()).unwrap();
429        assert_roundtrip(
430            DagJoseCodec,
431            &JsonWebSignature {
432                payload: payload_b64,
433                signatures: vec![Signature {
434                    header: BTreeMap::from([
435                        ("k0".to_string(), Ipld::from("v0")),
436                        ("k1".to_string(), Ipld::from(1)),
437                    ]),
438                    protected: Some(protected_b64),
439                    signature: signature_b64,
440                }],
441                link,
442            },
443            &ipld!({
444                "payload": payload,
445                "signatures": [{
446                    "header": {
447                        "k0": "v0",
448                        "k1": 1
449                    },
450                    "protected": protected,
451                    "signature": signature,
452                }],
453            }),
454        );
455    }
456    #[test]
457    fn roundtrip_jwe() {
458        let JweFixture {
459            ciphertext,
460            iv,
461            protected,
462            tag,
463        } = fixture_jwe();
464        let (ciphertext_b64, iv_b64, protected_b64, tag_b64) =
465            fixture_jwe_base64(&ciphertext, &iv, &protected, &tag);
466        assert_roundtrip(
467            DagJoseCodec,
468            &JsonWebEncryption {
469                aad: None,
470                ciphertext: ciphertext_b64,
471                iv: iv_b64,
472                protected: protected_b64,
473                recipients: vec![],
474                tag: tag_b64,
475                unprotected: BTreeMap::new(),
476            },
477            &ipld!({
478                "ciphertext": ciphertext,
479                "iv": iv,
480                "protected": protected,
481                "tag": tag,
482            }),
483        );
484    }
485
486    // Utility for testing codecs.
487    //
488    // Encodes the `data` using the codec `c` and checks that it matches the `ipld`.
489    fn assert_roundtrip<C, T>(_c: C, data: &T, ipld: &Ipld)
490    where
491        C: Codec<T>,
492        C: Codec<Ipld>,
493        <C as Codec<T>>::Error: std::fmt::Debug,
494        <C as Codec<Ipld>>::Error: std::fmt::Debug,
495        T: std::cmp::PartialEq + std::fmt::Debug,
496    {
497        let bytes = C::encode_to_vec(data).unwrap();
498        let bytes2 = C::encode_to_vec(ipld).unwrap();
499        if bytes != bytes2 {
500            panic!(
501                r#"assertion failed: `(left == right)`
502        left: `{}`,
503       right: `{}`"#,
504                hex::encode(&bytes),
505                hex::encode(&bytes2)
506            );
507        }
508        let ipld2: Ipld = C::decode_from_slice(&bytes).unwrap();
509        assert_eq!(&ipld2, ipld);
510        let data2: T = C::decode_from_slice(&bytes).unwrap();
511        assert_eq!(&data2, data);
512    }
513}