1#![cfg_attr(not(feature = "std"), no_std)]
2
3extern crate alloc;
14
15use alloc::format;
16use alloc::string::String;
17use alloc::string::ToString;
18use alloc::vec::Vec;
19use core::fmt;
20use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug)]
25pub enum SdkError {
26 SerializationError(String),
28 SignatureError(String),
30 KeyError(String),
32 ValidationError(String),
34}
35
36impl fmt::Display for SdkError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 SdkError::SerializationError(e) => write!(f, "Serialization failed: {}", e),
40 SdkError::SignatureError(e) => write!(f, "Invalid signature: {}", e),
41 SdkError::KeyError(e) => write!(f, "Key format error: {}", e),
42 SdkError::ValidationError(e) => write!(f, "Validation error: {}", e),
43 }
44 }
45}
46
47#[cfg(feature = "std")]
48impl std::error::Error for SdkError {}
49
50impl From<serde_json::Error> for SdkError {
51 fn from(e: serde_json::Error) -> Self {
52 SdkError::SerializationError(e.to_string())
53 }
54}
55
56impl From<ed25519_dalek::SignatureError> for SdkError {
57 fn from(e: ed25519_dalek::SignatureError) -> Self {
58 SdkError::SignatureError(e.to_string())
59 }
60}
61
62pub type Result<T> = core::result::Result<T, SdkError>;
63
64#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
68pub struct Claim {
69 pub data: String,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub metadata: Option<String>,
74 pub timestamp: u64,
76}
77
78#[derive(Debug, Serialize, Deserialize, Clone)]
80pub struct SignedClaim {
81 pub claim: Claim,
83 pub public_key: String,
85 pub signature: String,
87}
88
89impl Claim {
90 #[cfg(feature = "std")]
92 pub fn new(data: String) -> Self {
93 Self {
94 data,
95 timestamp: std::time::SystemTime::now()
96 .duration_since(std::time::UNIX_EPOCH)
97 .unwrap_or_default()
98 .as_secs(),
99 metadata: None,
100 }
101 }
102
103 pub fn new_with_timestamp(data: String, timestamp: u64) -> Result<Self> {
105 if data.trim().is_empty() {
106 return Err(SdkError::ValidationError(
107 "Error: Data field cannot be empty.".to_string(),
108 ));
109 }
110
111 if !(1..=32503680000).contains(×tamp) {
113 return Err(SdkError::ValidationError(
114 "Error: Timestamp out of bounds.".to_string(),
115 ));
116 }
117
118 Ok(Self {
119 data,
120 timestamp,
121 metadata: None,
122 })
123 }
124
125 pub fn to_signable_bytes(&self) -> Result<Vec<u8>> {
128 let json = serde_json::to_string(self)?;
130 let bytes = json.into_bytes();
131
132 const MAX_PAYLOAD_SIZE: usize = 2048;
134 if bytes.len() > MAX_PAYLOAD_SIZE {
135 return Err(SdkError::ValidationError(
136 "Error: Payload too large. Tip: For large datasets, hash the file locally and anchor the hash instead of the raw data.".to_string()
137 ));
138 }
139
140 Ok(bytes)
141 }
142}
143
144pub fn compute_hash(data: &[u8]) -> String {
146 use sha2::{Digest, Sha256};
147 let mut hasher = Sha256::new();
148 hasher.update(data);
149 hex::encode(hasher.finalize())
150}
151
152#[cfg(any(feature = "std", feature = "getrandom"))]
162pub fn generate_keypair() -> SigningKey {
163 use rand::rngs::OsRng;
164 SigningKey::generate(&mut OsRng)
165}
166
167pub fn sign_claim(claim: &Claim, key: &SigningKey) -> Result<SignedClaim> {
177 let bytes = claim.to_signable_bytes()?;
178 let signature = key.sign(&bytes);
179
180 Ok(SignedClaim {
181 claim: claim.clone(),
182 public_key: hex::encode(key.verifying_key().as_bytes()),
183 signature: hex::encode(signature.to_bytes()),
184 })
185}
186
187pub fn verify_claim(signed_claim: &SignedClaim) -> Result<bool> {
198 let pk_bytes = hex::decode(&signed_claim.public_key)
200 .map_err(|e| SdkError::KeyError(format!("Invalid Hex Public Key: {}", e)))?;
201 let pk = VerifyingKey::from_bytes(
202 pk_bytes
203 .as_slice()
204 .try_into()
205 .map_err(|_| SdkError::KeyError("Invalid Key Length".into()))?,
206 )?;
207
208 let sig_bytes = hex::decode(&signed_claim.signature)
210 .map_err(|e| SdkError::KeyError(format!("Invalid Hex Signature: {}", e)))?;
211
212 if sig_bytes.len() != 64 {
213 return Err(SdkError::KeyError(format!(
214 "Invalid Signature Length: expected 64, got {}",
215 sig_bytes.len()
216 )));
217 }
218
219 let sig = Signature::from_bytes(
220 sig_bytes
221 .as_slice()
222 .try_into()
223 .map_err(|_| SdkError::KeyError("Invalid Signature Length".into()))?,
224 );
225
226 let msg_bytes = signed_claim.claim.to_signable_bytes()?;
228
229 let computed_signature = pk.verify_strict(&msg_bytes, &sig);
231 computed_signature
232 .map_err(|e| SdkError::SignatureError(format!("Invalid signature: {}", e)))?;
233
234 Ok(true)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_sign_verify_flow() {
243 let key = SigningKey::from_bytes(&[1u8; 32]);
244 let claim = Claim::new_with_timestamp("Hello World".to_string(), 123456789).unwrap();
245
246 let signed = sign_claim(&claim, &key).expect("Sign failed");
247
248 let valid = verify_claim(&signed).expect("Verify failed");
250 assert!(valid);
251 }
252
253 #[test]
254 fn test_canonical_json_order() {
255 let claim = Claim {
256 data: "test".to_string(),
257 metadata: Some("meta".to_string()),
258 timestamp: 123,
259 };
260 let json = serde_json::to_string(&claim).unwrap();
261 assert_eq!(json, r#"{"data":"test","metadata":"meta","timestamp":123}"#);
263 }
264
265 #[test]
266 fn test_tamper_detection() {
267 let key = SigningKey::from_bytes(&[1u8; 32]);
268 let claim = Claim::new_with_timestamp("Sensitive Data".to_string(), 123456789).unwrap();
269
270 let mut signed = sign_claim(&claim, &key).expect("Sign failed");
271
272 signed.claim.data = "Tampered Data".to_string();
274
275 let result = verify_claim(&signed);
276 assert!(result.is_err());
277 }
278
279 #[test]
280 fn test_payload_limit() {
281 let mut claim = Claim::new_with_timestamp("Test Data".to_string(), 123456789).unwrap();
282 let large_metadata = "a".repeat(2049);
284 claim.metadata = Some(large_metadata);
285
286 let result = claim.to_signable_bytes();
287 match result {
288 Err(SdkError::ValidationError(msg)) => {
289 assert_eq!(msg, "Error: Payload too large. Tip: For large datasets, hash the file locally and anchor the hash instead of the raw data.");
290 }
291 _ => panic!("Expected ValidationError for payload > 2KB"),
292 }
293 }
294}