oxirs_did/signed_graph/
mod.rs

1//! Signed RDF Graphs module
2
3pub mod canonicalization;
4pub mod signature;
5
6use crate::{Did, DidResult, Proof, ProofPurpose};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// A signed RDF graph
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct SignedGraph {
14    /// Graph URI (named graph identifier)
15    pub graph_uri: String,
16
17    /// RDF triples in the graph
18    pub triples: Vec<RdfTriple>,
19
20    /// Issuer DID
21    pub issuer: Did,
22
23    /// Issuance timestamp
24    pub issued_at: DateTime<Utc>,
25
26    /// Graph signature
27    pub proof: Proof,
28
29    /// Optional expiration
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub expires_at: Option<DateTime<Utc>>,
32}
33
34/// RDF Triple representation
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub struct RdfTriple {
37    pub subject: RdfTerm,
38    pub predicate: String,
39    pub object: RdfTerm,
40}
41
42/// RDF Term (subject or object)
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(tag = "type", content = "value")]
45pub enum RdfTerm {
46    /// IRI reference
47    Iri(String),
48    /// Blank node
49    BlankNode(String),
50    /// Literal value
51    Literal {
52        value: String,
53        #[serde(skip_serializing_if = "Option::is_none")]
54        datatype: Option<String>,
55        #[serde(skip_serializing_if = "Option::is_none")]
56        language: Option<String>,
57    },
58}
59
60impl RdfTriple {
61    pub fn new(subject: RdfTerm, predicate: &str, object: RdfTerm) -> Self {
62        Self {
63            subject,
64            predicate: predicate.to_string(),
65            object,
66        }
67    }
68
69    /// Create triple with IRI subject and object
70    pub fn iri(subject: &str, predicate: &str, object: &str) -> Self {
71        Self {
72            subject: RdfTerm::Iri(subject.to_string()),
73            predicate: predicate.to_string(),
74            object: RdfTerm::Iri(object.to_string()),
75        }
76    }
77
78    /// Create triple with IRI subject and literal object
79    pub fn literal(subject: &str, predicate: &str, value: &str, datatype: Option<&str>) -> Self {
80        Self {
81            subject: RdfTerm::Iri(subject.to_string()),
82            predicate: predicate.to_string(),
83            object: RdfTerm::Literal {
84                value: value.to_string(),
85                datatype: datatype.map(String::from),
86                language: None,
87            },
88        }
89    }
90}
91
92impl SignedGraph {
93    /// Create a new signed graph (without signing yet)
94    pub fn new(graph_uri: &str, triples: Vec<RdfTriple>, issuer: Did) -> Self {
95        Self {
96            graph_uri: graph_uri.to_string(),
97            triples,
98            issuer: issuer.clone(),
99            issued_at: Utc::now(),
100            proof: Proof::ed25519(
101                &issuer.key_id("key-1"),
102                ProofPurpose::AssertionMethod,
103                &[], // Empty signature - to be filled
104            ),
105            expires_at: None,
106        }
107    }
108
109    /// Sign the graph with Ed25519
110    pub fn sign(mut self, signer: &crate::proof::ed25519::Ed25519Signer) -> DidResult<Self> {
111        // Canonicalize the graph
112        let canonical = self.canonicalize()?;
113
114        // Hash the canonical form
115        use sha2::{Digest, Sha256};
116        let hash = Sha256::digest(canonical.as_bytes());
117
118        // Sign the hash
119        let signature = signer.sign(&hash);
120
121        // Update proof
122        self.proof = Proof::ed25519(
123            &self.issuer.key_id("key-1"),
124            ProofPurpose::AssertionMethod,
125            &signature,
126        );
127
128        Ok(self)
129    }
130
131    /// Verify the graph signature
132    pub async fn verify(
133        &self,
134        resolver: &crate::DidResolver,
135    ) -> DidResult<crate::VerificationResult> {
136        // Resolve issuer DID
137        let did_doc = resolver.resolve(&self.issuer).await?;
138
139        // Get verification key
140        let vm = did_doc.get_assertion_method().ok_or_else(|| {
141            crate::DidError::VerificationFailed("No assertion method in DID Document".to_string())
142        })?;
143
144        let public_key = vm.get_public_key_bytes()?;
145
146        // Canonicalize
147        let canonical = self.canonicalize()?;
148
149        // Hash
150        use sha2::{Digest, Sha256};
151        let hash = Sha256::digest(canonical.as_bytes());
152
153        // Verify signature
154        let signature = self.proof.get_signature_bytes()?;
155        let verifier = crate::proof::ed25519::Ed25519Verifier::from_bytes(&public_key)?;
156        let valid = verifier.verify(&hash, &signature)?;
157
158        if valid {
159            Ok(crate::VerificationResult::success(self.issuer.as_str())
160                .with_check("signature", true, None)
161                .with_check("expiration", !self.is_expired(), None))
162        } else {
163            Ok(crate::VerificationResult::failure("Invalid signature"))
164        }
165    }
166
167    /// Canonicalize the graph using RDFC-1.0 algorithm
168    fn canonicalize(&self) -> DidResult<String> {
169        canonicalization::canonicalize_graph(&self.triples)
170    }
171
172    /// Check if the graph signature is expired
173    pub fn is_expired(&self) -> bool {
174        if let Some(expires) = self.expires_at {
175            Utc::now() > expires
176        } else {
177            false
178        }
179    }
180
181    /// Set expiration
182    pub fn with_expiration(mut self, expires: DateTime<Utc>) -> Self {
183        self.expires_at = Some(expires);
184        self
185    }
186
187    /// Get the graph hash
188    pub fn hash(&self) -> DidResult<String> {
189        let canonical = self.canonicalize()?;
190        use sha2::{Digest, Sha256};
191        let hash = Sha256::digest(canonical.as_bytes());
192        Ok(hex::encode(hash))
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::proof::ed25519::Ed25519Signer;
200
201    #[test]
202    fn test_rdf_triple() {
203        let triple = RdfTriple::iri(
204            "http://example.org/subject",
205            "http://example.org/predicate",
206            "http://example.org/object",
207        );
208
209        assert!(matches!(triple.subject, RdfTerm::Iri(_)));
210        assert!(matches!(triple.object, RdfTerm::Iri(_)));
211    }
212
213    #[test]
214    fn test_signed_graph() {
215        let signer = Ed25519Signer::generate();
216        let public_key = signer.public_key_bytes();
217        let issuer = Did::new_key_ed25519(&public_key).unwrap();
218
219        let triples = vec![RdfTriple::iri(
220            "http://example.org/s",
221            "http://example.org/p",
222            "http://example.org/o",
223        )];
224
225        let graph = SignedGraph::new("http://example.org/graph", triples, issuer);
226        let signed = graph.sign(&signer).unwrap();
227
228        assert!(signed.proof.proof_value.is_some());
229    }
230}