Skip to main content

mid_types/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![forbid(unsafe_code)]
3//! # `mid-types` — the mID wire format, `no_std`
4//!
5//! The types every mID party agrees on — the JWT payload, the embedded
6//! genesis roster and roster chain, the verification methods — and the
7//! canonical bytes their signatures cover. `mid-issuer` builds them,
8//! `mid-verify` checks them, and a device that signs its own genesis roster
9//! and self-issued token (the Janus ESP32 family) carries only this crate
10//! and a verifier. `no_std` + `alloc`; `std` (default) only forwards to
11//! serde and serde_json.
12//!
13//! Every field name here MUST exactly match the JSON serialization the
14//! verifier and the JS SDK expect (ADR 0005 Decision 1). Changing a field
15//! name is a wire-format break.
16
17extern crate alloc;
18
19pub mod canonical;
20
21use alloc::collections::BTreeMap;
22use alloc::string::String;
23use alloc::vec::Vec;
24
25use serde::{Deserialize, Serialize};
26
27// ─── Request side ──────────────────────────────────────────────────────────
28
29/// The relying party's sign-in request, posted into the wallet by the
30/// `@mata/mid` SDK on the RP page.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RpRequest {
33    /// The RP's bare origin, e.g. `"https://acme.com"`. The wallet verifies
34    /// this matches the page's actual origin before issuing.
35    pub rp_origin: String,
36    /// RP-issued single-use nonce. The wallet echoes it in the JWT for
37    /// replay defense.
38    pub nonce: String,
39    /// Per-claim categorization (required/optional/custom).
40    pub claims: ClaimRequest,
41}
42
43/// Categorized claim request from the RP.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct ClaimRequest {
46    /// Claims that block sign-in if denied. Almost always just `["did"]`.
47    pub required: Vec<String>,
48    /// Claims the user can include or skip.
49    #[serde(default)]
50    pub optional: Vec<String>,
51    /// Arbitrary keys outside the standard catalog — looked up in the
52    /// wallet's local `profile_kv`.
53    #[serde(default)]
54    pub custom: BTreeMap<String, CustomClaimRequest>,
55}
56
57/// Per-key custom-claim metadata.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct CustomClaimRequest {
60    /// Always true in v1 — custom claims cannot be required (the wallet
61    /// doesn't know what they mean to the RP).
62    pub optional: bool,
63    /// Optional human-readable description the consent UI surfaces.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub description: Option<String>,
66}
67
68// ─── Identity snapshot ─────────────────────────────────────────────────────
69
70/// The wallet's identity state at the moment of sign-in, assembled by the
71/// caller from `mata-identity` plus the `mid_user_consents` Y-CRDT lookup.
72///
73/// This crate consumes the snapshot read-only — it never mutates wallet
74/// state.
75#[derive(Debug, Clone)]
76pub struct IdentitySnapshot {
77    /// The user's DID, e.g. `"did:mata:p3k7q...8x2h"`.
78    pub did: String,
79    /// The genesis roster envelope, self-signed at signup by the genesis
80    /// device's key. Held in wallet storage forever.
81    pub genesis_roster: EmbeddedGenesisRoster,
82    /// The wallet-signed roster mutations from genesis to current. May be
83    /// empty if the user has only ever had the genesis device.
84    pub roster_chain: Vec<EmbeddedRosterChainEntry>,
85    /// The verification method for the device that's about to sign this
86    /// mID JWT. MUST appear in either the genesis roster (if `roster_chain`
87    /// is empty) or in the head roster chain entry.
88    pub current_verification_method: EmbeddedVerificationMethod,
89    /// Pre-built claim values keyed by claim name. The caller looks each
90    /// claim up against the wallet's local stores (profile, achievements,
91    /// computed level ratings) and populates this map BEFORE calling
92    /// [`crate::build_mid_jwt`].
93    ///
94    /// Claims the user denied at the consent screen MUST be absent from
95    /// this map — the issuer trusts the caller's filtering.
96    pub approved_claims: BTreeMap<String, ClaimValue>,
97}
98
99// ─── JWT payload ───────────────────────────────────────────────────────────
100
101/// The mID JWT payload — the JSON object that's base64url-encoded as the
102/// second segment of the JWS compact-form token.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct MidJwtPayload {
105    /// Issuer — the user's DID.
106    pub iss: String,
107    /// Subject — same as `iss` for self-issued tokens.
108    pub sub: String,
109    /// Audience — the RP's bare origin from the request.
110    pub aud: String,
111    /// Issued-at, Unix seconds.
112    pub iat: u64,
113    /// Expiry, Unix seconds. v1 ships short-lived (~1 hour) tokens.
114    pub exp: u64,
115    /// RP-supplied nonce, echoed for replay defense.
116    pub nonce: String,
117    /// Claim values approved by the user, keyed by claim name.
118    pub claims: BTreeMap<String, ClaimValue>,
119    /// Self-signed genesis roster (signed by the key encoded in the DID).
120    pub embedded_genesis_roster: EmbeddedGenesisRoster,
121    /// Chain of wallet-signed roster mutations from genesis to current.
122    pub embedded_roster_chain: Vec<EmbeddedRosterChainEntry>,
123    /// The device key signing this JWT — must appear in the head roster.
124    pub embedded_verification_method: EmbeddedVerificationMethod,
125}
126
127/// A single claim value with provenance metadata.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ClaimValue {
130    /// The claim's value. Free-form per RFC 7519; common shapes are
131    /// strings, numbers, booleans.
132    pub value: serde_json::Value,
133    /// Provenance — `self` in v1; v2 may add `third_party_vc`.
134    pub attested_by: AttestedBy,
135    /// Only set on the `email` claim if the user completed signup OTP.
136    /// Historical fact, not a MATA signature.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub verified_at_signup: Option<bool>,
139    /// Only set on derived claims (level ratings) — the wall-clock the
140    /// wallet computed the score at.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub computed_at: Option<u64>,
143    /// Only set on derived claims — pins the formula generation. RPs can
144    /// reject older versions.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub formula_version: Option<String>,
147}
148
149/// Attestation provenance for a claim.
150///
151/// v1 ships only `self`. The enum is open for future provenance categories
152/// without breaking the wire format.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154pub enum AttestedBy {
155    /// User-supplied, signed by the user's M5 device key as part of the
156    /// outer JWT signature. MATA never witnessed this value.
157    #[serde(rename = "self")]
158    SelfAttested,
159}
160
161// ─── Embedded roster + verification method ─────────────────────────────────
162
163/// The genesis roster envelope — version 1, self-signed by the genesis
164/// device's signing key (the key whose public form is encoded in the DID
165/// identifier).
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct EmbeddedGenesisRoster {
168    /// Always 1 for the genesis envelope.
169    pub version: u64,
170    /// The DID this roster belongs to.
171    pub did: String,
172    /// Verification methods present in the genesis roster. v1 wallets ship
173    /// a one-entry genesis roster (just the genesis device); future
174    /// versions could initialize multi-device genesis.
175    pub verification_methods: Vec<VerificationMethod>,
176    /// When the genesis roster was signed, Unix seconds.
177    pub signed_at: u64,
178    /// Signature by the genesis key over the canonical envelope bytes.
179    /// Verifiable against the public key recovered from the DID.
180    /// base64url-encoded `r || s` (64 bytes total).
181    pub self_signed_by_genesis_key: String,
182}
183
184/// One entry in the wallet-signed roster chain. Each entry is signed by a
185/// verification method present in the prior entry's roster.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct EmbeddedRosterChainEntry {
188    /// Monotonically increasing across the chain. genesis is version 1; the
189    /// first chain entry is version 2.
190    pub version: u64,
191    /// The DID this roster belongs to.
192    pub did: String,
193    /// Verification methods after this mutation.
194    pub verification_methods: Vec<VerificationMethod>,
195    /// When this mutation was signed, Unix seconds.
196    pub signed_at: u64,
197    /// Signature by a verification method present in the PRIOR roster.
198    /// base64url-encoded `r || s` (64 bytes total).
199    pub signed_by_signer_in_prior_roster: String,
200    /// The `kid` of the verification method in the prior roster that
201    /// produced this signature. Lets the verifier skip a scan.
202    pub signer_kid: String,
203}
204
205/// A verification method = a public key bound to a DID. Same shape as the
206/// W3C DID document `verificationMethod[]` entry, minimum-fields form.
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct VerificationMethod {
209    /// Fully-qualified ID, e.g. `"did:mata:abc...#dev-laptop"`.
210    pub id: String,
211    /// Always `"EcdsaSecp256r1VerificationKey2019"` in v1.
212    #[serde(rename = "type")]
213    pub vm_type: String,
214    /// The DID that controls this verification method.
215    pub controller: String,
216    /// Multibase-encoded public key. v1 wallets use `z6Mk...` base58btc
217    /// per the multibase spec.
218    pub public_key_multibase: String,
219}
220
221/// The current device's verification method — the key that's about to sign
222/// the JWT. Slim form (just the key + identity), no extra DID-document
223/// metadata. Must appear in the head roster of the chain.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct EmbeddedVerificationMethod {
226    /// Fully-qualified ID, e.g. `"did:mata:abc...#dev-laptop"`.
227    pub id: String,
228    /// Always `"EcdsaSecp256r1VerificationKey2019"` in v1.
229    #[serde(rename = "type")]
230    pub vm_type: String,
231    /// The DID that controls this verification method.
232    pub controller: String,
233    /// Multibase-encoded public key.
234    pub public_key_multibase: String,
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn attested_by_serializes_as_lowercase_self() {
243        let cv = ClaimValue {
244            value: serde_json::Value::String("tim@example.com".into()),
245            attested_by: AttestedBy::SelfAttested,
246            verified_at_signup: Some(true),
247            computed_at: None,
248            formula_version: None,
249        };
250        let json = serde_json::to_string(&cv).unwrap();
251        assert!(
252            json.contains(r#""attested_by":"self""#),
253            "attested_by must serialize as the lowercase string 'self' per ADR 0005 Decision 1, got: {json}"
254        );
255        assert!(
256            json.contains(r#""verified_at_signup":true"#),
257            "verified_at_signup must be present when Some, got: {json}"
258        );
259    }
260
261    #[test]
262    fn claim_value_omits_optional_fields_when_none() {
263        let cv = ClaimValue {
264            value: serde_json::Value::String("Tim".into()),
265            attested_by: AttestedBy::SelfAttested,
266            verified_at_signup: None,
267            computed_at: None,
268            formula_version: None,
269        };
270        let json = serde_json::to_string(&cv).unwrap();
271        assert!(!json.contains("verified_at_signup"), "absent when None");
272        assert!(!json.contains("computed_at"), "absent when None");
273        assert!(!json.contains("formula_version"), "absent when None");
274    }
275
276    #[test]
277    fn verification_method_type_field_serializes_as_type() {
278        let vm = VerificationMethod {
279            id: "did:mata:abc#dev".into(),
280            vm_type: "EcdsaSecp256r1VerificationKey2019".into(),
281            controller: "did:mata:abc".into(),
282            public_key_multibase: "z6Mk...".into(),
283        };
284        let json = serde_json::to_string(&vm).unwrap();
285        assert!(
286            json.contains(r#""type":"EcdsaSecp256r1VerificationKey2019""#),
287            "vm_type must serialize as JSON field 'type' per the DID document standard, got: {json}"
288        );
289    }
290}