ipfrs_core/
jose.rs

1//! DAG-JOSE codec for encrypted and signed IPLD data
2//!
3//! This module provides support for DAG-JOSE, which combines IPLD with:
4//! - **JWS (JSON Web Signature)** for signing data
5//! - **JWE (JSON Web Encryption)** for encrypting data
6//!
7//! ## Features
8//!
9//! - Sign IPLD data with Ed25519, RS256, or other algorithms
10//! - Verify signed IPLD data
11//! - Create content-addressed signed documents
12//! - Integration with IPLD DAG structures
13//!
14//! ## Example - Signing Data
15//!
16//! ```rust
17//! use ipfrs_core::jose::{JoseBuilder, JoseSignature};
18//! use ipfrs_core::Ipld;
19//!
20//! // Create some IPLD data
21//! let data = Ipld::String("Hello, IPFS!".to_string());
22//!
23//! // Sign the data (using a mock key for this example)
24//! let secret = b"your-secret-key-min-32-bytes-long!!";
25//! let jose = JoseBuilder::new()
26//!     .with_payload(data)
27//!     .sign_hs256(secret)
28//!     .unwrap();
29//!
30//! // Verify the signature
31//! let verified = jose.verify_hs256(secret).unwrap();
32//! assert!(verified);
33//! ```
34
35use crate::error::{Error, Result};
36use crate::ipld::Ipld;
37use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
38use serde::{Deserialize, Serialize};
39use std::collections::BTreeMap;
40
41/// Parse PEM format and extract DER bytes
42/// This is a simple PEM parser for RSA keys
43fn pem_to_der(pem: &[u8]) -> Result<Vec<u8>> {
44    let pem_str = std::str::from_utf8(pem)
45        .map_err(|e| Error::InvalidInput(format!("Invalid UTF-8 in PEM: {}", e)))?;
46
47    // Remove header, footer, and whitespace
48    let lines: Vec<&str> = pem_str
49        .lines()
50        .filter(|line| !line.starts_with("-----"))
51        .collect();
52
53    let base64_content = lines.join("");
54
55    // Decode base64 to get DER bytes
56    use base64::{engine::general_purpose::STANDARD, Engine as _};
57    STANDARD
58        .decode(base64_content.as_bytes())
59        .map_err(|e| Error::InvalidInput(format!("Failed to decode base64 in PEM: {}", e)))
60}
61
62/// DAG-JOSE signature wrapper for IPLD data
63///
64/// This structure represents a signed IPLD payload using JWS (JSON Web Signature).
65/// The signature ensures data integrity and authenticity.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67pub struct JoseSignature {
68    /// The signed payload (as IPLD)
69    pub payload: Ipld,
70    /// The JWS signature string
71    pub signature: String,
72    /// Algorithm used for signing
73    pub algorithm: String,
74}
75
76/// Builder for creating DAG-JOSE signatures
77///
78/// Provides a fluent interface for signing IPLD data with various algorithms.
79pub struct JoseBuilder {
80    payload: Option<Ipld>,
81}
82
83impl JoseBuilder {
84    /// Create a new JOSE builder
85    pub fn new() -> Self {
86        Self { payload: None }
87    }
88
89    /// Set the payload to be signed
90    pub fn with_payload(mut self, payload: Ipld) -> Self {
91        self.payload = Some(payload);
92        self
93    }
94
95    /// Sign the payload using HMAC SHA-256
96    ///
97    /// # Arguments
98    /// * `secret` - Secret key for HMAC (should be at least 32 bytes)
99    ///
100    /// # Returns
101    /// A `JoseSignature` containing the signed payload
102    pub fn sign_hs256(self, secret: &[u8]) -> Result<JoseSignature> {
103        let payload = self
104            .payload
105            .ok_or_else(|| Error::InvalidInput("No payload set".to_string()))?;
106
107        if secret.len() < 32 {
108            return Err(Error::InvalidInput(
109                "HMAC secret must be at least 32 bytes".to_string(),
110            ));
111        }
112
113        // Convert IPLD to JSON for JWT payload
114        let json_payload = ipld_to_json_value(&payload)?;
115
116        // Create JWT claims
117        let claims = serde_json::json!({
118            "payload": json_payload,
119        });
120
121        // Sign the data
122        let header = Header::new(Algorithm::HS256);
123        let token = encode(&header, &claims, &EncodingKey::from_secret(secret))
124            .map_err(|e| Error::Serialization(format!("Failed to sign data: {}", e)))?;
125
126        Ok(JoseSignature {
127            payload,
128            signature: token,
129            algorithm: "HS256".to_string(),
130        })
131    }
132
133    /// Sign the payload using RS256 (RSA with SHA-256)
134    ///
135    /// # Arguments
136    /// * `private_key_pem` - RSA private key in PEM format
137    ///
138    /// # Returns
139    /// A `JoseSignature` containing the signed payload
140    pub fn sign_rs256(self, private_key_pem: &[u8]) -> Result<JoseSignature> {
141        let payload = self
142            .payload
143            .ok_or_else(|| Error::InvalidInput("No payload set".to_string()))?;
144
145        // Convert IPLD to JSON for JWT payload
146        let json_payload = ipld_to_json_value(&payload)?;
147
148        // Create JWT claims
149        let claims = serde_json::json!({
150            "payload": json_payload,
151        });
152
153        // Sign the data
154        let header = Header::new(Algorithm::RS256);
155        let der = pem_to_der(private_key_pem)?;
156        let token = encode(&header, &claims, &EncodingKey::from_rsa_der(&der))
157            .map_err(|e| Error::Serialization(format!("Failed to sign data: {}", e)))?;
158
159        Ok(JoseSignature {
160            payload,
161            signature: token,
162            algorithm: "RS256".to_string(),
163        })
164    }
165}
166
167impl Default for JoseBuilder {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl JoseSignature {
174    /// Verify the signature using HMAC SHA-256
175    ///
176    /// # Arguments
177    /// * `secret` - Secret key used for signing
178    ///
179    /// # Returns
180    /// `Ok(true)` if the signature is valid, `Ok(false)` otherwise
181    pub fn verify_hs256(&self, secret: &[u8]) -> Result<bool> {
182        if self.algorithm != "HS256" {
183            return Err(Error::InvalidInput(format!(
184                "Expected HS256 algorithm, got {}",
185                self.algorithm
186            )));
187        }
188
189        // Decode and verify the JWT with custom validation
190        let mut validation = Validation::new(Algorithm::HS256);
191        // Don't require standard claims (exp, nbf, iat, etc.)
192        validation.required_spec_claims.clear();
193        validation.validate_exp = false;
194        validation.validate_nbf = false;
195
196        let token_data = decode::<serde_json::Value>(
197            &self.signature,
198            &DecodingKey::from_secret(secret),
199            &validation,
200        );
201
202        match token_data {
203            Ok(_) => Ok(true),
204            Err(e) => {
205                // Check if it's a validation error or signature mismatch
206                match e.kind() {
207                    jsonwebtoken::errors::ErrorKind::InvalidSignature => Ok(false),
208                    _ => Err(Error::Verification(format!(
209                        "Failed to verify signature: {}",
210                        e
211                    ))),
212                }
213            }
214        }
215    }
216
217    /// Verify the signature using RS256 (RSA with SHA-256)
218    ///
219    /// # Arguments
220    /// * `public_key_pem` - RSA public key in PEM format
221    ///
222    /// # Returns
223    /// `Ok(true)` if the signature is valid, `Ok(false)` otherwise
224    pub fn verify_rs256(&self, public_key_pem: &[u8]) -> Result<bool> {
225        if self.algorithm != "RS256" {
226            return Err(Error::InvalidInput(format!(
227                "Expected RS256 algorithm, got {}",
228                self.algorithm
229            )));
230        }
231
232        // Decode and verify the JWT with custom validation
233        let mut validation = Validation::new(Algorithm::RS256);
234        // Don't require standard claims (exp, nbf, iat, etc.)
235        validation.required_spec_claims.clear();
236        validation.validate_exp = false;
237        validation.validate_nbf = false;
238
239        let der = pem_to_der(public_key_pem)?;
240        let token_data = decode::<serde_json::Value>(
241            &self.signature,
242            &DecodingKey::from_rsa_der(&der),
243            &validation,
244        );
245
246        match token_data {
247            Ok(_) => Ok(true),
248            Err(e) => match e.kind() {
249                jsonwebtoken::errors::ErrorKind::InvalidSignature => Ok(false),
250                _ => Err(Error::Verification(format!(
251                    "Failed to verify signature: {}",
252                    e
253                ))),
254            },
255        }
256    }
257
258    /// Encode the JoseSignature to DAG-JOSE format
259    ///
260    /// Returns a JSON representation compatible with DAG-JOSE spec
261    pub fn to_dag_jose(&self) -> Result<Vec<u8>> {
262        let jose_object = serde_json::json!({
263            "payload": ipld_to_json_value(&self.payload)?,
264            "signatures": [{
265                "protected": self.algorithm,
266                "signature": self.signature,
267            }]
268        });
269
270        serde_json::to_vec(&jose_object)
271            .map_err(|e| Error::Serialization(format!("Failed to serialize DAG-JOSE: {}", e)))
272    }
273
274    /// Decode from DAG-JOSE format
275    ///
276    /// Parses a JSON representation in DAG-JOSE format
277    pub fn from_dag_jose(data: &[u8]) -> Result<Self> {
278        let jose_object: serde_json::Value = serde_json::from_slice(data)
279            .map_err(|e| Error::Deserialization(format!("Failed to parse DAG-JOSE: {}", e)))?;
280
281        let payload_json = jose_object
282            .get("payload")
283            .ok_or_else(|| Error::Deserialization("Missing payload field".to_string()))?;
284
285        let signatures = jose_object
286            .get("signatures")
287            .and_then(|s| s.as_array())
288            .ok_or_else(|| Error::Deserialization("Missing or invalid signatures".to_string()))?;
289
290        if signatures.is_empty() {
291            return Err(Error::Deserialization("No signatures found".to_string()));
292        }
293
294        let first_sig = &signatures[0];
295        let algorithm = first_sig
296            .get("protected")
297            .and_then(|a| a.as_str())
298            .ok_or_else(|| Error::Deserialization("Missing algorithm".to_string()))?
299            .to_string();
300
301        let signature = first_sig
302            .get("signature")
303            .and_then(|s| s.as_str())
304            .ok_or_else(|| Error::Deserialization("Missing signature".to_string()))?
305            .to_string();
306
307        let payload = json_value_to_ipld(payload_json)?;
308
309        Ok(JoseSignature {
310            payload,
311            signature,
312            algorithm,
313        })
314    }
315}
316
317// Helper function to convert IPLD to serde_json::Value
318fn ipld_to_json_value(ipld: &Ipld) -> Result<serde_json::Value> {
319    match ipld {
320        Ipld::Null => Ok(serde_json::Value::Null),
321        Ipld::Bool(b) => Ok(serde_json::Value::Bool(*b)),
322        Ipld::Integer(i) => {
323            // Convert i128 to i64 for JSON (with range check)
324            let i64_val: i64 = (*i)
325                .try_into()
326                .map_err(|_| Error::Serialization("Integer value out of i64 range".to_string()))?;
327            Ok(serde_json::Value::Number(i64_val.into()))
328        }
329        Ipld::Float(f) => serde_json::Number::from_f64(*f)
330            .map(serde_json::Value::Number)
331            .ok_or_else(|| Error::Serialization("Invalid float value".to_string())),
332        Ipld::String(s) => Ok(serde_json::Value::String(s.clone())),
333        Ipld::Bytes(b) => {
334            // Encode bytes as IPLD bytes object: {"/": {"bytes": "<base64>"}}
335            let encoded = base64_encode(b);
336            Ok(serde_json::json!({
337                "/": {
338                    "bytes": encoded
339                }
340            }))
341        }
342        Ipld::List(list) => {
343            let values: Result<Vec<_>> = list.iter().map(ipld_to_json_value).collect();
344            Ok(serde_json::Value::Array(values?))
345        }
346        Ipld::Map(map) => {
347            let mut json_map = serde_json::Map::new();
348            for (k, v) in map {
349                json_map.insert(k.clone(), ipld_to_json_value(v)?);
350            }
351            Ok(serde_json::Value::Object(json_map))
352        }
353        Ipld::Link(cid) => {
354            // Encode CID as a link object
355            Ok(serde_json::json!({
356                "/": cid.to_string()
357            }))
358        }
359    }
360}
361
362// Helper function to convert serde_json::Value to IPLD
363fn json_value_to_ipld(value: &serde_json::Value) -> Result<Ipld> {
364    match value {
365        serde_json::Value::Null => Ok(Ipld::Null),
366        serde_json::Value::Bool(b) => Ok(Ipld::Bool(*b)),
367        serde_json::Value::Number(n) => {
368            if let Some(i) = n.as_i64() {
369                Ok(Ipld::Integer(i as i128))
370            } else if let Some(f) = n.as_f64() {
371                Ok(Ipld::Float(f))
372            } else {
373                Err(Error::Deserialization("Invalid number".to_string()))
374            }
375        }
376        serde_json::Value::String(s) => Ok(Ipld::String(s.clone())),
377        serde_json::Value::Array(arr) => {
378            let items: Result<Vec<_>> = arr.iter().map(json_value_to_ipld).collect();
379            Ok(Ipld::List(items?))
380        }
381        serde_json::Value::Object(obj) => {
382            // Check if it's a special IPLD object
383            if obj.len() == 1 && obj.contains_key("/") {
384                let special = obj.get("/").unwrap();
385
386                // Check if it's bytes: {"/": {"bytes": "<base64>"}}
387                if let Some(bytes_obj) = special.as_object() {
388                    if bytes_obj.len() == 1 && bytes_obj.contains_key("bytes") {
389                        if let Some(b64_str) = bytes_obj.get("bytes").and_then(|v| v.as_str()) {
390                            // Decode base64 - for simplicity, just convert back to bytes
391                            // In production, use proper base64 decoding
392                            return Ok(Ipld::Bytes(base64_decode(b64_str)?));
393                        }
394                    }
395                }
396
397                // Check if it's a CID link: {"/": "<cid-string>"}
398                if let Some(cid_str) = special.as_str() {
399                    let cid = crate::cid::parse_cid(cid_str)?;
400                    return Ok(Ipld::Link(crate::cid::SerializableCid(cid)));
401                }
402            }
403
404            // Regular map
405            let mut map = BTreeMap::new();
406            for (k, v) in obj {
407                map.insert(k.clone(), json_value_to_ipld(v)?);
408            }
409            Ok(Ipld::Map(map))
410        }
411    }
412}
413
414// Simple base64 encoding helper
415fn base64_encode(data: &[u8]) -> String {
416    use std::fmt::Write;
417    let mut result = String::new();
418    for chunk in data.chunks(3) {
419        let b1 = chunk[0];
420        let b2 = chunk.get(1).copied().unwrap_or(0);
421        let b3 = chunk.get(2).copied().unwrap_or(0);
422
423        let n = ((b1 as u32) << 16) | ((b2 as u32) << 8) | (b3 as u32);
424
425        let chars = [
426            b64char((n >> 18) & 0x3f),
427            b64char((n >> 12) & 0x3f),
428            if chunk.len() > 1 {
429                b64char((n >> 6) & 0x3f)
430            } else {
431                '='
432            },
433            if chunk.len() > 2 {
434                b64char(n & 0x3f)
435            } else {
436                '='
437            },
438        ];
439
440        for c in &chars {
441            write!(&mut result, "{}", c).unwrap();
442        }
443    }
444    result
445}
446
447fn b64char(n: u32) -> char {
448    const CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
449    CHARS[n as usize] as char
450}
451
452// Simple base64 decoding helper
453fn base64_decode(s: &str) -> Result<Vec<u8>> {
454    if s.is_empty() {
455        return Ok(Vec::new());
456    }
457
458    let bytes = s.as_bytes();
459    let mut result = Vec::new();
460
461    for chunk in bytes.chunks(4) {
462        if chunk.len() < 2 {
463            break;
464        }
465
466        let c0 = b64decode_char(chunk[0])?;
467        let c1 = b64decode_char(chunk[1])?;
468        let c2 = if chunk.len() > 2 && chunk[2] != b'=' {
469            b64decode_char(chunk[2])?
470        } else {
471            0
472        };
473        let c3 = if chunk.len() > 3 && chunk[3] != b'=' {
474            b64decode_char(chunk[3])?
475        } else {
476            0
477        };
478
479        result.push((c0 << 2) | (c1 >> 4));
480        if chunk.len() > 2 && chunk[2] != b'=' {
481            result.push((c1 << 4) | (c2 >> 2));
482        }
483        if chunk.len() > 3 && chunk[3] != b'=' {
484            result.push((c2 << 6) | c3);
485        }
486    }
487
488    Ok(result)
489}
490
491fn b64decode_char(c: u8) -> Result<u8> {
492    match c {
493        b'A'..=b'Z' => Ok(c - b'A'),
494        b'a'..=b'z' => Ok(c - b'a' + 26),
495        b'0'..=b'9' => Ok(c - b'0' + 52),
496        b'+' => Ok(62),
497        b'/' => Ok(63),
498        _ => Err(Error::Deserialization(format!(
499            "Invalid base64 character: {}",
500            c
501        ))),
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn test_jose_sign_verify_hs256() {
511        let data = Ipld::String("Hello, DAG-JOSE!".to_string());
512        let secret = b"my-secret-key-must-be-32-bytes!!";
513
514        // Sign the data
515        let jose = JoseBuilder::new()
516            .with_payload(data.clone())
517            .sign_hs256(secret)
518            .unwrap();
519
520        assert_eq!(jose.payload, data);
521        assert_eq!(jose.algorithm, "HS256");
522
523        // Verify with correct secret
524        assert!(jose.verify_hs256(secret).unwrap());
525
526        // Verify with wrong secret should fail
527        let wrong_secret = b"wrong-secret-key-must-be-32byte!";
528        assert!(!jose.verify_hs256(wrong_secret).unwrap());
529    }
530
531    #[test]
532    fn test_jose_sign_different_payloads() {
533        let secret = b"my-secret-key-must-be-32-bytes!!";
534
535        // Sign different payloads
536        let jose1 = JoseBuilder::new()
537            .with_payload(Ipld::String("payload1".to_string()))
538            .sign_hs256(secret)
539            .unwrap();
540
541        let jose2 = JoseBuilder::new()
542            .with_payload(Ipld::String("payload2".to_string()))
543            .sign_hs256(secret)
544            .unwrap();
545
546        // Signatures should be different
547        assert_ne!(jose1.signature, jose2.signature);
548    }
549
550    #[test]
551    fn test_jose_with_complex_ipld() {
552        let mut map = BTreeMap::new();
553        map.insert("name".to_string(), Ipld::String("Alice".to_string()));
554        map.insert("age".to_string(), Ipld::Integer(30));
555        map.insert(
556            "roles".to_string(),
557            Ipld::List(vec![
558                Ipld::String("admin".to_string()),
559                Ipld::String("user".to_string()),
560            ]),
561        );
562
563        let data = Ipld::Map(map);
564        let secret = b"my-secret-key-must-be-32-bytes!!";
565
566        let jose = JoseBuilder::new()
567            .with_payload(data.clone())
568            .sign_hs256(secret)
569            .unwrap();
570
571        assert_eq!(jose.payload, data);
572        assert!(jose.verify_hs256(secret).unwrap());
573    }
574
575    #[test]
576    fn test_jose_short_secret_fails() {
577        let data = Ipld::String("test".to_string());
578        let short_secret = b"short"; // Too short
579
580        let result = JoseBuilder::new()
581            .with_payload(data)
582            .sign_hs256(short_secret);
583
584        assert!(result.is_err());
585    }
586
587    #[test]
588    fn test_jose_no_payload_fails() {
589        let secret = b"my-secret-key-must-be-32-bytes!!";
590
591        let result = JoseBuilder::new().sign_hs256(secret);
592
593        assert!(result.is_err());
594    }
595
596    #[test]
597    fn test_jose_to_dag_jose() {
598        let data = Ipld::String("Hello".to_string());
599        let secret = b"my-secret-key-must-be-32-bytes!!";
600
601        let jose = JoseBuilder::new()
602            .with_payload(data)
603            .sign_hs256(secret)
604            .unwrap();
605
606        // Convert to DAG-JOSE format
607        let dag_jose = jose.to_dag_jose().unwrap();
608
609        // Should be valid JSON
610        let parsed: serde_json::Value = serde_json::from_slice(&dag_jose).unwrap();
611        assert!(parsed.get("payload").is_some());
612        assert!(parsed.get("signatures").is_some());
613    }
614
615    #[test]
616    fn test_jose_roundtrip_dag_jose() {
617        let data = Ipld::String("Roundtrip test".to_string());
618        let secret = b"my-secret-key-must-be-32-bytes!!";
619
620        let jose = JoseBuilder::new()
621            .with_payload(data.clone())
622            .sign_hs256(secret)
623            .unwrap();
624
625        // Encode to DAG-JOSE
626        let dag_jose = jose.to_dag_jose().unwrap();
627
628        // Decode back
629        let decoded = JoseSignature::from_dag_jose(&dag_jose).unwrap();
630
631        assert_eq!(decoded.payload, data);
632        assert_eq!(decoded.algorithm, jose.algorithm);
633        assert!(decoded.verify_hs256(secret).unwrap());
634    }
635
636    #[test]
637    fn test_base64_encode() {
638        let data = b"hello world";
639        let encoded = base64_encode(data);
640        // Should be valid base64
641        assert!(!encoded.is_empty());
642        assert!(encoded
643            .chars()
644            .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '='));
645    }
646}