Skip to main content

provn_sdk/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! # Provncloud SDK
4//!
5//! A high-performance, universal cryptographic engine for signing and verifying data claims.
6//!
7//! This crate provides a lightweight, `no_std` compatible implementation for generating
8//! verifiable audit trails. It is designed to be compatible with resource-constrained
9//! environments like Solana programs and Arweave AO processes, while maintaining
10//! strict [JCS (RFC 8785)](https://rfc-editor.org/rfc/rfc8785) compliance for
11//! cross-platform interoperability.
12
13extern 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/// Errors encountered during SDK operations.
24#[derive(Debug)]
25pub enum SdkError {
26    /// Error occurred during JSON serialization or deserialization.
27    SerializationError(String),
28    /// Error occurred during cryptographic signature generation or verification.
29    SignatureError(String),
30    /// Error occurred due to invalid key format or length.
31    KeyError(String),
32    /// Error occurred when validation rules (like size limits) are violated.
33    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/// A Claim representing a statement of truth to be anchored.
65/// Fields are ordered alphabetically to ensure "Canonical JSON" (JCS - RFC 8785)
66/// compliance when using deterministic serialization.
67#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
68pub struct Claim {
69    /// The actual data being claimed (e.g., "AI Model v1.0 Accuracy: 98%")
70    pub data: String,
71    /// Optional metadata or context
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub metadata: Option<String>,
74    /// Timestamp of the claim (UTC seconds)
75    pub timestamp: u64,
76}
77
78/// A SignedClaim wraps the claim with its signature and public key.
79#[derive(Debug, Serialize, Deserialize, Clone)]
80pub struct SignedClaim {
81    /// The original claim
82    pub claim: Claim,
83    /// The public key of the signer (Hex encoded)
84    pub public_key: String,
85    /// The signature of the serialized claim (Hex encoded)
86    pub signature: String,
87}
88
89impl Claim {
90    /// Create a new claim with the current system time (requires "std")
91    #[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    /// Create a new claim with a provided timestamp (useful for no-std)
104    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        // Basic bounds checking for timestamp sanity (e.g., > 1970 and < 3000)
112        if !(1..=32503680000).contains(&timestamp) {
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    /// Canonical serialization for signing (Sorted keys, no whitespace)
126    /// This follows JCS (RFC 8785) logic by relying on struct field ordering.
127    pub fn to_signable_bytes(&self) -> Result<Vec<u8>> {
128        // Enforce canonical JSON (no whitespace, sorted keys via struct order)
129        let json = serde_json::to_string(self)?;
130        let bytes = json.into_bytes();
131
132        // Validation: Enforce 2KB Limit on Total Signable Payload across all SDKs
133        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
144/// Compute a SHA-256 hash of a byte slice for "Hash-Only" forensics.
145pub 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/// Generate a new random keypair
153///
154/// Requires either "std" or "getrandom" feature for entropy source.
155///
156/// # Example
157/// ```
158/// use provn_sdk::generate_keypair;
159/// let key = generate_keypair();
160/// ```
161#[cfg(any(feature = "std", feature = "getrandom"))]
162pub fn generate_keypair() -> SigningKey {
163    use rand::rngs::OsRng;
164    SigningKey::generate(&mut OsRng)
165}
166
167/// Sign a claim with a private key
168///
169/// # Example
170/// ```
171/// use provn_sdk::{Claim, sign_claim, generate_keypair};
172/// let key = generate_keypair();
173/// let claim = Claim::new("Test Claim".to_string());
174/// let signed = sign_claim(&claim, &key).unwrap();
175/// ```
176pub 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
187/// Verify a signed claim
188///
189/// # Example
190/// ```
191/// use provn_sdk::{Claim, sign_claim, verify_claim, generate_keypair};
192/// let key = generate_keypair();
193/// let claim = Claim::new("Test Claim".to_string());
194/// let signed = sign_claim(&claim, &key).unwrap();
195/// assert!(verify_claim(&signed).unwrap());
196/// ```
197pub fn verify_claim(signed_claim: &SignedClaim) -> Result<bool> {
198    // 1. Decode Public Key
199    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    // Decode signature
209    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    // 3. Reconstruct Signable Bytes
227    let msg_bytes = signed_claim.claim.to_signable_bytes()?;
228
229    // 4. Verify in constant time to prevent timing attacks
230    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        // Verify locally
249        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        // data comes before metadata comes before timestamp
262        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        // Tamper with data
273        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        // Create metadata slightly larger than 2048 bytes
283        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}