Skip to main content

ratify_protocol/
types.rs

1//! Ratify Protocol v1 types.
2//!
3//! Every public key and every signature is a hybrid pair: one Ed25519
4//! component and one ML-DSA-65 (FIPS 204) component. Both must verify.
5
6use serde::ser::{SerializeMap, Serializer};
7use serde::{Deserialize, Serialize};
8
9pub const PROTOCOL_VERSION: i32 = 1;
10pub const MAX_DELEGATION_CHAIN_DEPTH: usize = 3;
11pub const CHALLENGE_WINDOW_SECONDS: i64 = 300;
12
13pub const ED25519_PUBLIC_KEY_SIZE: usize = 32;
14pub const ED25519_SIGNATURE_SIZE: usize = 64;
15pub const MLDSA65_PUBLIC_KEY_SIZE: usize = 1952;
16pub const MLDSA65_SIGNATURE_SIZE: usize = 3309;
17
18/// Ed25519 + ML-DSA-65 public key pair.
19///
20/// Canonical JSON form (keys in lex order):
21/// `{"ed25519":"<base64-32-bytes>","ml_dsa_65":"<base64-1952-bytes>"}`
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct HybridPublicKey {
24    #[serde(with = "crate::canonical::base64_bytes")]
25    pub ed25519: Vec<u8>, // 32 bytes
26    #[serde(with = "crate::canonical::base64_bytes")]
27    pub ml_dsa_65: Vec<u8>, // 1952 bytes
28}
29
30/// Ed25519 + ML-DSA-65 signature pair over the same canonical bytes.
31///
32/// Both components MUST verify for the signature to be accepted.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct HybridSignature {
35    #[serde(with = "crate::canonical::base64_bytes")]
36    pub ed25519: Vec<u8>, // 64 bytes
37    #[serde(with = "crate::canonical::base64_bytes")]
38    pub ml_dsa_65: Vec<u8>, // 3309 bytes
39}
40
41/// Both component private keys. Never serialized to the wire.
42#[derive(Debug, Clone)]
43pub struct HybridPrivateKey {
44    pub ed25519: Vec<u8>,   // 32-byte seed
45    pub ml_dsa_65: Vec<u8>, // ML-DSA-65 secret key bytes
46}
47
48/// Optional external binding for higher-assurance identity.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Anchor {
51    #[serde(rename = "type")]
52    pub anchor_type: String,
53    pub provider: String,
54    pub reference: String,
55    pub verified_at: i64,
56}
57
58/// Master identity for a human (or tenant admin).
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct HumanRoot {
61    pub id: String,
62    pub public_key: HybridPublicKey,
63    pub created_at: i64,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub anchors: Option<Vec<Anchor>>,
66}
67
68/// An AI agent's identity.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct AgentIdentity {
71    pub id: String,
72    pub public_key: HybridPublicKey,
73    pub name: String,
74    pub agent_type: String,
75    pub created_at: i64,
76}
77
78/// Signed authorization from a principal to an agent.
79///
80/// `scope` answers *what* the agent may do. `constraints` answer *where /
81/// when / how much* — first-class bounds evaluated at verify time against a
82/// caller-supplied VerifierContext.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct DelegationCert {
85    pub cert_id: String,
86    pub version: i32,
87    pub issuer_id: String,
88    pub issuer_pub_key: HybridPublicKey,
89    pub subject_id: String,
90    pub subject_pub_key: HybridPublicKey,
91    pub scope: Vec<String>,
92    /// Always present in canonical JSON (`[]` when empty) so canonical bytes
93    /// are deterministic across issuers.
94    #[serde(default)]
95    pub constraints: Vec<Constraint>,
96    pub issued_at: i64,
97    pub expires_at: i64,
98    pub signature: HybridSignature,
99}
100
101/// First-class bound on when/where/how much an agent may exercise its scopes.
102///
103/// Wire format is a tagged JSON object. `type` discriminates the kind;
104/// remaining fields are kind-specific. Unknown `type` values MUST be
105/// rejected by conformant verifiers (fail-closed).
106///
107// Fields are declared in alphabetical JSON-key order so serde's default
108// struct serialization order produces canonical bytes that match the Go
109// reference and the other SDKs' lex-sorted output (SPEC §6.2). Do not
110// reorder — cross-SDK byte identicality depends on this.
111//
112// Serialization is custom (see impl Serialize below) to emit the
113// canonical per-kind shape rather than the default "skip if zero"
114// behavior. This closes the v1 zero-as-absence ambiguity: a geo_circle at
115// lat=0, lon=0 now emits lat:0, lon:0 explicitly instead of omitting them.
116#[derive(Debug, Clone, Default, Deserialize)]
117pub struct Constraint {
118    #[serde(default)]
119    pub count: i64,
120    #[serde(default)]
121    pub currency: String,
122    #[serde(default)]
123    pub end: String,
124    #[serde(default)]
125    pub lat: f64,
126    #[serde(default)]
127    pub lon: f64,
128    #[serde(default)]
129    pub max_alt_m: f64,
130    #[serde(default)]
131    pub max_amount: f64,
132    #[serde(default)]
133    pub max_lat: f64,
134    #[serde(default)]
135    pub max_lon: f64,
136    #[serde(default)]
137    pub max_mps: f64,
138    #[serde(default)]
139    pub min_alt_m: f64,
140    #[serde(default)]
141    pub min_lat: f64,
142    #[serde(default)]
143    pub min_lon: f64,
144    #[serde(default)]
145    pub points: Vec<[f64; 2]>,
146    #[serde(default)]
147    pub radius_m: f64,
148    #[serde(default)]
149    pub start: String,
150    #[serde(default)]
151    pub tz: String,
152    #[serde(rename = "type")]
153    pub kind: String,
154    #[serde(default)]
155    pub window_s: i64,
156}
157
158// Custom Serialize for Constraint — emits the canonical per-kind shape.
159// Mirrors Go's Constraint.MarshalJSON and TS canonicalConstraintDict.
160// Keys are emitted in alphabetical order, matching the other SDKs.
161impl Serialize for Constraint {
162    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163        // Count fields up front so serde's map writer knows the length.
164        // Doing this the verbose way rather than with serialize_struct
165        // because the per-kind shape is dynamic, not a fixed struct.
166        let entries: Vec<(&'static str, FieldValue)> = match self.kind.as_str() {
167            "geo_circle" => vec![
168                ("lat", FieldValue::F64(self.lat)),
169                ("lon", FieldValue::F64(self.lon)),
170                ("radius_m", FieldValue::F64(self.radius_m)),
171                ("type", FieldValue::Str(self.kind.clone())),
172            ],
173            "geo_polygon" => vec![
174                ("points", FieldValue::Points(self.points.clone())),
175                ("type", FieldValue::Str(self.kind.clone())),
176            ],
177            "geo_bbox" => {
178                let mut v = vec![
179                    ("max_lat", FieldValue::F64(self.max_lat)),
180                    ("max_lon", FieldValue::F64(self.max_lon)),
181                    ("min_lat", FieldValue::F64(self.min_lat)),
182                    ("min_lon", FieldValue::F64(self.min_lon)),
183                ];
184                if self.min_alt_m != 0.0 || self.max_alt_m != 0.0 {
185                    // Insert altitude pair alphabetically: max_alt_m < max_lat.
186                    v.insert(0, ("max_alt_m", FieldValue::F64(self.max_alt_m)));
187                    // min_alt_m < min_lat → insert after max_lon (index 2).
188                    v.insert(3, ("min_alt_m", FieldValue::F64(self.min_alt_m)));
189                }
190                v.push(("type", FieldValue::Str(self.kind.clone())));
191                v
192            }
193            "time_window" => vec![
194                ("end", FieldValue::Str(self.end.clone())),
195                ("start", FieldValue::Str(self.start.clone())),
196                ("type", FieldValue::Str(self.kind.clone())),
197                ("tz", FieldValue::Str(self.tz.clone())),
198            ],
199            "max_speed_mps" => vec![
200                ("max_mps", FieldValue::F64(self.max_mps)),
201                ("type", FieldValue::Str(self.kind.clone())),
202            ],
203            "max_amount" => vec![
204                ("currency", FieldValue::Str(self.currency.clone())),
205                ("max_amount", FieldValue::F64(self.max_amount)),
206                ("type", FieldValue::Str(self.kind.clone())),
207            ],
208            "max_rate" => vec![
209                ("count", FieldValue::I64(self.count)),
210                ("type", FieldValue::Str(self.kind.clone())),
211                ("window_s", FieldValue::I64(self.window_s)),
212            ],
213            // Unknown kind: emit only the tag. Verifier returns constraint_unknown.
214            _ => vec![("type", FieldValue::Str(self.kind.clone()))],
215        };
216        let mut m = serializer.serialize_map(Some(entries.len()))?;
217        for (k, v) in entries {
218            match v {
219                FieldValue::F64(x) => m.serialize_entry(k, &x)?,
220                FieldValue::I64(x) => m.serialize_entry(k, &x)?,
221                FieldValue::Str(x) => m.serialize_entry(k, &x)?,
222                FieldValue::Points(x) => m.serialize_entry(k, &x)?,
223            }
224        }
225        m.end()
226    }
227}
228
229// Small sum type so the serialize impl can carry mixed-type values in one
230// vector. Kept private to this module.
231enum FieldValue {
232    F64(f64),
233    I64(i64),
234    Str(String),
235    Points(Vec<[f64; 2]>),
236}
237
238/// Application-supplied inputs for evaluating first-class constraints.
239/// A cert bearing a constraint whose required context field is absent will
240/// be rejected with `constraint_unverifiable` (fail-closed).
241#[derive(Default)]
242pub struct VerifierContext<'a> {
243    pub current_lat: Option<f64>,
244    pub current_lon: Option<f64>,
245    pub current_alt_m: Option<f64>,
246    pub current_speed_mps: Option<f64>,
247    pub requested_amount: Option<f64>,
248    pub requested_currency: Option<String>,
249    /// (cert_id, window_s) -> invocation count
250    pub invocations_in_window: Option<Box<dyn Fn(&str, i64) -> i64 + 'a>>,
251}
252
253/// Proof an agent presents to a verifier.
254///
255/// v1.1 optional stream binding: when `stream_id` and `stream_seq` are set,
256/// the bundle is "stream-bound" — it belongs to an ordered sequence of
257/// interactions sharing a stream_id. Both are signed into the challenge bytes
258/// (SPEC §6.4.2) so replay, reorder, or omission within the stream invalidate
259/// the signature.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ProofBundle {
262    pub agent_id: String,
263    pub agent_pub_key: HybridPublicKey,
264    pub delegations: Vec<DelegationCert>,
265    #[serde(with = "crate::canonical::base64_bytes")]
266    pub challenge: Vec<u8>,
267    pub challenge_at: i64,
268    pub challenge_sig: HybridSignature,
269    #[serde(
270        default,
271        skip_serializing_if = "Vec::is_empty",
272        with = "crate::canonical::base64_bytes"
273    )]
274    pub session_context: Vec<u8>,
275    #[serde(
276        default,
277        skip_serializing_if = "Vec::is_empty",
278        with = "crate::canonical::base64_bytes"
279    )]
280    pub stream_id: Vec<u8>,
281    #[serde(default, skip_serializing_if = "is_zero_i64")]
282    pub stream_seq: i64,
283}
284
285fn is_zero_i64(v: &i64) -> bool {
286    *v == 0
287}
288
289/// Identity status values in a VerifyResult (SPEC §5.9). Granular failure
290/// statuses (scope_denied, constraint_denied, etc) let callers route on the
291/// enum directly — they do not have to parse error_reason text.
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum IdentityStatus {
295    VerifiedHuman,
296    AuthorizedAgent,
297    Expired,
298    Revoked,
299    ScopeDenied,
300    ConstraintDenied,
301    ConstraintUnverifiable,
302    ConstraintUnknown,
303    DelegationNotAuthorized,
304    Invalid,
305    Unauthorized,
306}
307
308impl IdentityStatus {
309    pub fn as_str(&self) -> &'static str {
310        match self {
311            Self::VerifiedHuman => "verified_human",
312            Self::AuthorizedAgent => "authorized_agent",
313            Self::Expired => "expired",
314            Self::Revoked => "revoked",
315            Self::ScopeDenied => "scope_denied",
316            Self::ConstraintDenied => "constraint_denied",
317            Self::ConstraintUnverifiable => "constraint_unverifiable",
318            Self::ConstraintUnknown => "constraint_unknown",
319            Self::DelegationNotAuthorized => "delegation_not_authorized",
320            Self::Invalid => "invalid",
321            Self::Unauthorized => "unauthorized",
322        }
323    }
324
325    /// Parse the snake_case wire form back into the enum. Returns None if
326    /// the input is not a known status; callers should fail-closed.
327    pub fn from_wire(s: &str) -> Option<Self> {
328        Some(match s {
329            "verified_human" => Self::VerifiedHuman,
330            "authorized_agent" => Self::AuthorizedAgent,
331            "expired" => Self::Expired,
332            "revoked" => Self::Revoked,
333            "scope_denied" => Self::ScopeDenied,
334            "constraint_denied" => Self::ConstraintDenied,
335            "constraint_unverifiable" => Self::ConstraintUnverifiable,
336            "constraint_unknown" => Self::ConstraintUnknown,
337            "delegation_not_authorized" => Self::DelegationNotAuthorized,
338            "invalid" => Self::Invalid,
339            "unauthorized" => Self::Unauthorized,
340            _ => return None,
341        })
342    }
343}
344
345/// Deterministic output of `verify_bundle`. Always check `valid` first.
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct VerifyResult {
348    pub valid: bool,
349    pub identity_status: IdentityStatus,
350    #[serde(skip_serializing_if = "String::is_empty", default)]
351    pub human_id: String,
352    #[serde(skip_serializing_if = "String::is_empty", default)]
353    pub agent_id: String,
354    #[serde(skip_serializing_if = "String::is_empty", default)]
355    pub agent_name: String,
356    #[serde(skip_serializing_if = "String::is_empty", default)]
357    pub agent_type: String,
358    #[serde(skip_serializing_if = "Vec::is_empty", default)]
359    pub granted_scope: Vec<String>,
360    #[serde(skip_serializing_if = "String::is_empty", default)]
361    pub error_reason: String,
362}
363
364/// Signed list of revoked cert IDs, served by the issuer.
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct RevocationList {
367    pub issuer_id: String,
368    pub updated_at: i64,
369    pub revoked_certs: Vec<String>,
370    pub signature: HybridSignature,
371}
372
373/// v1.1 signed push notification of newly revoked cert IDs.
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct RevocationPush {
376    pub issuer_id: String,
377    pub seq_no: i64,
378    pub entries: Vec<String>,
379    pub pushed_at: i64,
380    pub signature: HybridSignature,
381}
382
383/// v1.1 element in a hash-chain append-only witness log.
384#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct WitnessEntry {
386    #[serde(with = "crate::canonical::base64_bytes")]
387    pub prev_hash: Vec<u8>,
388    #[serde(with = "crate::canonical::base64_bytes")]
389    pub entry_data: Vec<u8>,
390    pub timestamp: i64,
391    pub witness_id: String,
392    pub signature: HybridSignature,
393}
394
395/// v1.1 verifier-issued credential that caches a verified chain. MAC =
396/// HMAC-SHA256(session_secret, session_token_sign_bytes(token)). The session
397/// secret is private to the verifier and never leaves its trust boundary.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct SessionToken {
400    pub version: i32,
401    pub session_id: String,
402    pub agent_id: String,
403    pub agent_pub_key: HybridPublicKey,
404    pub human_id: String,
405    pub granted_scope: Vec<String>,
406    pub issued_at: i64,
407    pub valid_until: i64,
408    #[serde(with = "crate::canonical::base64_bytes")]
409    pub chain_hash: Vec<u8>,
410    #[serde(with = "crate::canonical::base64_bytes")]
411    pub mac: Vec<u8>,
412}
413
414/// v1.1 canonical envelope for a multi-party, atomic transaction.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct TransactionReceipt {
417    pub version: i32,
418    pub transaction_id: String,
419    pub created_at: i64,
420    pub terms_schema_uri: String,
421    #[serde(with = "crate::canonical::base64_bytes")]
422    pub terms_canonical_json: Vec<u8>,
423    pub parties: Vec<ReceiptParty>,
424    pub party_signatures: Vec<ReceiptPartySignature>,
425}
426
427/// One party to a TransactionReceipt.
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct ReceiptParty {
430    pub party_id: String,
431    pub role: String,
432    pub agent_id: String,
433    pub agent_pub_key: HybridPublicKey,
434    pub proof_bundle: ProofBundle,
435}
436
437/// Hybrid signature by a party over the canonical receipt signable.
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct ReceiptPartySignature {
440    pub party_id: String,
441    pub signature: HybridSignature,
442}
443
444/// Outcome of verify_transaction_receipt.
445pub struct TransactionReceiptResult {
446    pub valid: bool,
447    pub error_reason: String,
448    pub party_results: Vec<VerifyResult>,
449}
450
451/// Signed continuity statement from an old root key to a new root key.
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct KeyRotationStatement {
454    pub version: i32,
455    pub old_id: String,
456    pub old_pub_key: HybridPublicKey,
457    pub new_id: String,
458    pub new_pub_key: HybridPublicKey,
459    pub rotated_at: i64,
460    pub reason: String,
461    pub signature_old: HybridSignature,
462    pub signature_new: HybridSignature,
463}
464
465/// Verifier state tracked per stream_id for v1.1 stream-bound bundles.
466///
467/// `last_seen_seq` is the highest sequence number the verifier has already
468/// accepted for `stream_id`; zero means no turns accepted yet, so the first
469/// valid bundle must carry `stream_seq == 1`.
470#[derive(Debug, Clone, Default)]
471pub struct StreamContext {
472    pub stream_id: Vec<u8>,
473    pub last_seen_seq: i64,
474}
475
476/// Options passed to `verify_bundle`.
477pub struct VerifyOptions<'a> {
478    /// Required scope; empty string skips scope checking.
479    pub required_scope: String,
480    /// Revocation callback; None disables revocation checking.
481    pub is_revoked: Option<Box<dyn Fn(&str) -> bool + 'a>>,
482    /// Force a fresh revocation check for high-stakes endpoints. The SDK
483    /// cannot fetch revocation state itself; callers must provide is_revoked
484    /// when this is true.
485    pub force_revocation_check: bool,
486    /// Override current time (unix seconds); None = SystemTime::now().
487    pub now: Option<i64>,
488    /// Optional verifier-reconstructed 32-byte v1.1 session context.
489    pub session_context: Vec<u8>,
490    /// Optional verifier-tracked v1.1 stream context.
491    pub stream: Option<StreamContext>,
492    /// Application inputs for evaluating first-class constraints. Default is
493    /// empty; constraint-bearing certs fail closed if required context is
494    /// absent.
495    pub context: VerifierContext<'a>,
496}
497
498impl<'a> Default for VerifyOptions<'a> {
499    fn default() -> Self {
500        Self {
501            required_scope: String::new(),
502            is_revoked: None,
503            force_revocation_check: false,
504            now: None,
505            session_context: Vec::new(),
506            stream: None,
507            context: VerifierContext::default(),
508        }
509    }
510}