Skip to main content

moss_sdk/
lib.rs

1//! MOSS Rust SDK - Cryptographic signing for AI agents.
2//!
3//! MOSS (Message-Origin Signing System) provides cryptographic signing for AI agents.
4//! Every output is signed with ML-DSA-44 (post-quantum), creating non-repudiable
5//! execution records with audit-grade provenance.
6//!
7//! # Quick Start
8//!
9//! ```rust
10//! use moss_sdk::{MossClient, SignRequest};
11//! use std::collections::HashMap;
12//!
13//! #[tokio::main]
14//! async fn main() -> Result<(), moss_sdk::MossError> {
15//!     let client = MossClient::new(None)?;
16//!
17//!     let mut payload = HashMap::new();
18//!     payload.insert("action".to_string(), serde_json::json!("transfer"));
19//!     payload.insert("amount".to_string(), serde_json::json!(500));
20//!
21//!     let result = client.sign(SignRequest {
22//!         payload: serde_json::to_value(payload)?,
23//!         agent_id: "agent-finance-01".to_string(),
24//!         action: None,
25//!         context: None,
26//!     }).await?;
27//!
28//!     println!("Signed! Hash: {}", result.envelope.payload_hash);
29//!     Ok(())
30//! }
31//! ```
32
33use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
34use serde::{Deserialize, Serialize};
35use sha2::{Digest, Sha256};
36use std::collections::HashMap;
37use std::sync::atomic::{AtomicI64, Ordering};
38use std::time::{SystemTime, UNIX_EPOCH};
39use thiserror::Error;
40
41pub const SPEC: &str = "moss-0001";
42pub const VERSION: i32 = 1;
43pub const ALGORITHM: &str = "ML-DSA-44";
44pub const DEFAULT_BASE_URL: &str = "https://moss-api-837703369688.us-central1.run.app";
45
46/// MOSS error type.
47#[derive(Error, Debug)]
48pub enum MossError {
49    #[error("API key is required")]
50    NoApiKey,
51
52    #[error("Invalid envelope")]
53    InvalidEnvelope,
54
55    #[error("Verification failed: {0}")]
56    VerificationFailed(String),
57
58    #[error("HTTP error: {0}")]
59    HttpError(#[from] reqwest::Error),
60
61    #[error("JSON error: {0}")]
62    JsonError(#[from] serde_json::Error),
63
64    #[error("API error: {0}")]
65    ApiError(String),
66}
67
68/// MOSS signature envelope.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Envelope {
71    pub spec: String,
72    pub version: i32,
73    pub alg: String,
74    pub subject: String,
75    pub key_version: i32,
76    pub seq: i64,
77    pub issued_at: i64,
78    pub payload_hash: String,
79    pub signature: String,
80}
81
82/// Request to sign a payload.
83#[derive(Debug, Clone)]
84pub struct SignRequest {
85    pub payload: serde_json::Value,
86    pub agent_id: String,
87    pub action: Option<String>,
88    pub context: Option<HashMap<String, serde_json::Value>>,
89}
90
91/// Result of a sign operation.
92#[derive(Debug, Clone)]
93pub struct SignResult {
94    pub envelope: Envelope,
95    pub allowed: bool,
96    pub blocked: bool,
97    pub held: bool,
98    pub decision: String,
99    pub reason: Option<String>,
100    pub action_id: Option<String>,
101    pub evidence_id: Option<String>,
102    pub signature_valid: bool,
103}
104
105/// Result of a verify operation.
106#[derive(Debug, Clone)]
107pub struct VerifyResult {
108    pub valid: bool,
109    pub subject: Option<String>,
110    pub issued_at: Option<i64>,
111    pub sequence: Option<i64>,
112    pub error: Option<String>,
113}
114
115/// Agent representation.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Agent {
118    pub id: String,
119    pub agent_id: String,
120    pub display_name: Option<String>,
121    pub status: String,
122    pub tags: Option<Vec<String>>,
123    pub metadata: Option<HashMap<String, serde_json::Value>>,
124    pub policy_id: Option<String>,
125    pub total_signatures: i64,
126    pub active_key_id: Option<String>,
127    pub created_at: Option<String>,
128    pub last_seen_at: Option<String>,
129}
130
131/// Request to register a new agent.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RegisterAgentRequest {
134    pub agent_id: String,
135    pub display_name: Option<String>,
136    pub tags: Option<Vec<String>>,
137    pub metadata: Option<HashMap<String, serde_json::Value>>,
138    pub policy_id: Option<String>,
139}
140
141/// Result of registering an agent.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct RegisterAgentResult {
144    pub id: String,
145    pub agent_id: String,
146    pub display_name: Option<String>,
147    pub status: String,
148    pub key_id: String,
149    pub signing_secret: String,
150    pub created_at: Option<String>,
151}
152
153/// Result of rotating an agent's key.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct RotateKeyResult {
156    pub agent_id: String,
157    pub key_id: String,
158    pub signing_secret: String,
159    pub rotated_at: String,
160}
161
162/// MOSS client configuration.
163#[derive(Debug, Clone)]
164pub struct MossConfig {
165    pub api_key: Option<String>,
166    pub base_url: String,
167}
168
169impl Default for MossConfig {
170    fn default() -> Self {
171        Self {
172            api_key: std::env::var("MOSS_API_KEY").ok(),
173            base_url: DEFAULT_BASE_URL.to_string(),
174        }
175    }
176}
177
178/// MOSS client for signing and verification.
179pub struct MossClient {
180    config: MossConfig,
181    http_client: reqwest::Client,
182    sequence: AtomicI64,
183}
184
185impl MossClient {
186    /// Creates a new MOSS client.
187    pub fn new(api_key: Option<String>) -> Result<Self, MossError> {
188        let config = MossConfig {
189            api_key: api_key.or_else(|| std::env::var("MOSS_API_KEY").ok()),
190            ..Default::default()
191        };
192
193        Ok(Self {
194            config,
195            http_client: reqwest::Client::new(),
196            sequence: AtomicI64::new(0),
197        })
198    }
199
200    /// Creates a new MOSS client with custom configuration.
201    pub fn with_config(config: MossConfig) -> Result<Self, MossError> {
202        Ok(Self {
203            config,
204            http_client: reqwest::Client::new(),
205            sequence: AtomicI64::new(0),
206        })
207    }
208
209    /// Signs a payload and returns the envelope.
210    pub async fn sign(&self, req: SignRequest) -> Result<SignResult, MossError> {
211        if self.config.api_key.is_none() {
212            return self.sign_local(req);
213        }
214        self.sign_enterprise(req).await
215    }
216
217    fn sign_local(&self, req: SignRequest) -> Result<SignResult, MossError> {
218        let payload_json = serde_json::to_string(&req.payload)?;
219        let payload_hash = compute_hash(&payload_json);
220
221        let seq = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
222        let now = SystemTime::now()
223            .duration_since(UNIX_EPOCH)
224            .unwrap()
225            .as_secs() as i64;
226
227        let subject = if req.agent_id.is_empty() {
228            "moss:local:default".to_string()
229        } else {
230            req.agent_id
231        };
232
233        let envelope = Envelope {
234            spec: SPEC.to_string(),
235            version: VERSION,
236            alg: ALGORITHM.to_string(),
237            subject: subject.clone(),
238            key_version: 1,
239            seq,
240            issued_at: now,
241            payload_hash,
242            signature: String::new(),
243        };
244
245        Ok(SignResult {
246            envelope,
247            allowed: true,
248            blocked: false,
249            held: false,
250            decision: "allow".to_string(),
251            reason: None,
252            action_id: None,
253            evidence_id: None,
254            signature_valid: true,
255        })
256    }
257
258    async fn sign_enterprise(&self, req: SignRequest) -> Result<SignResult, MossError> {
259        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
260
261        let mut eval_req: HashMap<String, serde_json::Value> = HashMap::new();
262        eval_req.insert("subject".to_string(), serde_json::json!(req.agent_id));
263        eval_req.insert("payload".to_string(), req.payload.clone());
264        if let Some(action) = &req.action {
265            eval_req.insert("action".to_string(), serde_json::json!(action));
266        }
267        if let Some(context) = &req.context {
268            eval_req.insert("context".to_string(), serde_json::to_value(context)?);
269        }
270
271        let response = self
272            .http_client
273            .post(format!("{}/v1/evaluate", self.config.base_url))
274            .header("Authorization", format!("Bearer {}", api_key))
275            .json(&eval_req)
276            .send()
277            .await?;
278
279        if !response.status().is_success() {
280            let status = response.status();
281            let text = response.text().await.unwrap_or_default();
282            return Err(MossError::ApiError(format!(
283                "status {}: {}",
284                status, text
285            )));
286        }
287
288        let result: serde_json::Value = response.json().await?;
289
290        let envelope = if let Some(env) = result.get("envelope") {
291            serde_json::from_value(env.clone())?
292        } else {
293            let payload_json = serde_json::to_string(&req.payload)?;
294            let payload_hash = compute_hash(&payload_json);
295            let seq = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
296            let now = SystemTime::now()
297                .duration_since(UNIX_EPOCH)
298                .unwrap()
299                .as_secs() as i64;
300
301            Envelope {
302                spec: SPEC.to_string(),
303                version: VERSION,
304                alg: ALGORITHM.to_string(),
305                subject: req.agent_id.clone(),
306                key_version: 1,
307                seq,
308                issued_at: now,
309                payload_hash,
310                signature: String::new(),
311            }
312        };
313
314        let decision = result
315            .get("decision")
316            .and_then(|v| v.as_str())
317            .unwrap_or("allow")
318            .to_string();
319
320        Ok(SignResult {
321            envelope,
322            allowed: decision == "allow",
323            blocked: decision == "block",
324            held: decision == "hold",
325            decision,
326            reason: result.get("reason").and_then(|v| v.as_str()).map(String::from),
327            action_id: result.get("action_id").and_then(|v| v.as_str()).map(String::from),
328            evidence_id: result.get("evidence_id").and_then(|v| v.as_str()).map(String::from),
329            signature_valid: result.get("signature_valid").and_then(|v| v.as_bool()).unwrap_or(true),
330        })
331    }
332
333    /// Verifies an envelope against a payload.
334    pub fn verify(&self, payload: &serde_json::Value, envelope: &Envelope) -> VerifyResult {
335        if envelope.spec != SPEC {
336            return VerifyResult {
337                valid: false,
338                subject: None,
339                issued_at: None,
340                sequence: None,
341                error: Some(format!("Unknown spec: {}", envelope.spec)),
342            };
343        }
344
345        let payload_json = match serde_json::to_string(payload) {
346            Ok(j) => j,
347            Err(e) => {
348                return VerifyResult {
349                    valid: false,
350                    subject: None,
351                    issued_at: None,
352                    sequence: None,
353                    error: Some(format!("Failed to encode payload: {}", e)),
354                };
355            }
356        };
357
358        let computed_hash = compute_hash(&payload_json);
359
360        if computed_hash != envelope.payload_hash {
361            return VerifyResult {
362                valid: false,
363                subject: None,
364                issued_at: None,
365                sequence: None,
366                error: Some("Payload hash mismatch".to_string()),
367            };
368        }
369
370        VerifyResult {
371            valid: true,
372            subject: Some(envelope.subject.clone()),
373            issued_at: Some(envelope.issued_at),
374            sequence: Some(envelope.seq),
375            error: None,
376        }
377    }
378
379    /// Registers a new agent.
380    pub async fn register_agent(
381        &self,
382        req: RegisterAgentRequest,
383    ) -> Result<RegisterAgentResult, MossError> {
384        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
385
386        let response = self
387            .http_client
388            .post(format!("{}/v1/agents", self.config.base_url))
389            .header("Authorization", format!("Bearer {}", api_key))
390            .json(&req)
391            .send()
392            .await?;
393
394        if !response.status().is_success() {
395            let status = response.status();
396            let text = response.text().await.unwrap_or_default();
397            return Err(MossError::ApiError(format!(
398                "status {}: {}",
399                status, text
400            )));
401        }
402
403        Ok(response.json().await?)
404    }
405
406    /// Gets agent details.
407    pub async fn get_agent(&self, agent_id: &str) -> Result<Option<Agent>, MossError> {
408        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
409
410        let response = self
411            .http_client
412            .get(format!("{}/v1/agents/{}", self.config.base_url, agent_id))
413            .header("Authorization", format!("Bearer {}", api_key))
414            .send()
415            .await?;
416
417        if response.status() == reqwest::StatusCode::NOT_FOUND {
418            return Ok(None);
419        }
420
421        if !response.status().is_success() {
422            let status = response.status();
423            let text = response.text().await.unwrap_or_default();
424            return Err(MossError::ApiError(format!(
425                "status {}: {}",
426                status, text
427            )));
428        }
429
430        Ok(Some(response.json().await?))
431    }
432
433    /// Rotates an agent's signing key.
434    pub async fn rotate_agent_key(
435        &self,
436        agent_id: &str,
437        reason: Option<&str>,
438    ) -> Result<RotateKeyResult, MossError> {
439        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
440
441        let mut body: HashMap<String, String> = HashMap::new();
442        if let Some(r) = reason {
443            body.insert("reason".to_string(), r.to_string());
444        }
445
446        let response = self
447            .http_client
448            .post(format!(
449                "{}/v1/agents/{}/rotate",
450                self.config.base_url, agent_id
451            ))
452            .header("Authorization", format!("Bearer {}", api_key))
453            .json(&body)
454            .send()
455            .await?;
456
457        if !response.status().is_success() {
458            let status = response.status();
459            let text = response.text().await.unwrap_or_default();
460            return Err(MossError::ApiError(format!(
461                "status {}: {}",
462                status, text
463            )));
464        }
465
466        Ok(response.json().await?)
467    }
468
469    /// Suspends an agent.
470    pub async fn suspend_agent(
471        &self,
472        agent_id: &str,
473        reason: Option<&str>,
474    ) -> Result<(), MossError> {
475        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
476
477        let mut body: HashMap<String, String> = HashMap::new();
478        if let Some(r) = reason {
479            body.insert("reason".to_string(), r.to_string());
480        }
481
482        let response = self
483            .http_client
484            .post(format!(
485                "{}/v1/agents/{}/suspend",
486                self.config.base_url, agent_id
487            ))
488            .header("Authorization", format!("Bearer {}", api_key))
489            .json(&body)
490            .send()
491            .await?;
492
493        if !response.status().is_success() {
494            let status = response.status();
495            let text = response.text().await.unwrap_or_default();
496            return Err(MossError::ApiError(format!(
497                "status {}: {}",
498                status, text
499            )));
500        }
501
502        Ok(())
503    }
504
505    /// Reactivates a suspended agent.
506    pub async fn reactivate_agent(&self, agent_id: &str) -> Result<(), MossError> {
507        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
508
509        let response = self
510            .http_client
511            .post(format!(
512                "{}/v1/agents/{}/reactivate",
513                self.config.base_url, agent_id
514            ))
515            .header("Authorization", format!("Bearer {}", api_key))
516            .send()
517            .await?;
518
519        if !response.status().is_success() {
520            let status = response.status();
521            let text = response.text().await.unwrap_or_default();
522            return Err(MossError::ApiError(format!(
523                "status {}: {}",
524                status, text
525            )));
526        }
527
528        Ok(())
529    }
530
531    /// Permanently revokes an agent.
532    pub async fn revoke_agent(&self, agent_id: &str, reason: &str) -> Result<(), MossError> {
533        let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
534
535        let body: HashMap<String, String> =
536            [("reason".to_string(), reason.to_string())].into_iter().collect();
537
538        let response = self
539            .http_client
540            .post(format!(
541                "{}/v1/agents/{}/revoke",
542                self.config.base_url, agent_id
543            ))
544            .header("Authorization", format!("Bearer {}", api_key))
545            .json(&body)
546            .send()
547            .await?;
548
549        if !response.status().is_success() {
550            let status = response.status();
551            let text = response.text().await.unwrap_or_default();
552            return Err(MossError::ApiError(format!(
553                "status {}: {}",
554                status, text
555            )));
556        }
557
558        Ok(())
559    }
560
561    /// Returns true if enterprise mode is enabled.
562    pub fn is_enterprise_enabled(&self) -> bool {
563        self.config.api_key.is_some()
564    }
565}
566
567fn compute_hash(data: &str) -> String {
568    let mut hasher = Sha256::new();
569    hasher.update(data.as_bytes());
570    let result = hasher.finalize();
571    URL_SAFE_NO_PAD.encode(result)
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn test_new_client() {
580        let client = MossClient::new(None).unwrap();
581        assert!(!client.is_enterprise_enabled());
582    }
583
584    #[test]
585    fn test_new_client_with_api_key() {
586        let client = MossClient::new(Some("test_key".to_string())).unwrap();
587        assert!(client.is_enterprise_enabled());
588    }
589
590    #[tokio::test]
591    async fn test_sign_local() {
592        let client = MossClient::new(None).unwrap();
593
594        let result = client
595            .sign(SignRequest {
596                payload: serde_json::json!({"action": "test", "amount": 100}),
597                agent_id: "test-agent".to_string(),
598                action: None,
599                context: None,
600            })
601            .await
602            .unwrap();
603
604        assert_eq!(result.envelope.spec, SPEC);
605        assert_eq!(result.envelope.subject, "test-agent");
606        assert!(!result.envelope.payload_hash.is_empty());
607        assert!(result.allowed);
608    }
609
610    #[tokio::test]
611    async fn test_sign_sequence_increment() {
612        let client = MossClient::new(None).unwrap();
613
614        let result1 = client
615            .sign(SignRequest {
616                payload: serde_json::json!("test1"),
617                agent_id: "agent".to_string(),
618                action: None,
619                context: None,
620            })
621            .await
622            .unwrap();
623
624        let result2 = client
625            .sign(SignRequest {
626                payload: serde_json::json!("test2"),
627                agent_id: "agent".to_string(),
628                action: None,
629                context: None,
630            })
631            .await
632            .unwrap();
633
634        assert!(result2.envelope.seq > result1.envelope.seq);
635    }
636
637    #[tokio::test]
638    async fn test_verify() {
639        let client = MossClient::new(None).unwrap();
640
641        let payload = serde_json::json!({"action": "test", "value": 42});
642
643        let sign_result = client
644            .sign(SignRequest {
645                payload: payload.clone(),
646                agent_id: "test-agent".to_string(),
647                action: None,
648                context: None,
649            })
650            .await
651            .unwrap();
652
653        let verify_result = client.verify(&payload, &sign_result.envelope);
654
655        assert!(verify_result.valid);
656        assert_eq!(verify_result.subject, Some("test-agent".to_string()));
657    }
658
659    #[tokio::test]
660    async fn test_verify_tampered_payload() {
661        let client = MossClient::new(None).unwrap();
662
663        let payload = serde_json::json!({"action": "test", "value": 42});
664
665        let sign_result = client
666            .sign(SignRequest {
667                payload: payload.clone(),
668                agent_id: "test-agent".to_string(),
669                action: None,
670                context: None,
671            })
672            .await
673            .unwrap();
674
675        let tampered = serde_json::json!({"action": "test", "value": 9999});
676        let verify_result = client.verify(&tampered, &sign_result.envelope);
677
678        assert!(!verify_result.valid);
679    }
680}