1use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, BTreeSet};
4
5pub const KEY_REFRESH_INTERVAL_SECONDS: u64 = 24 * 60 * 60;
6pub const REQUEST_WINDOW_SECONDS: u64 = 60;
7pub const ENROLLMENT_PROOF_TTL_SECONDS: u64 = 300;
8pub const PROTOCOL: &str = "meow-host-auth-v1";
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase", deny_unknown_fields)]
13pub struct PublicKey {
14 pub x: String,
15 pub y: String,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub enum SigningKeyStatus {
22 Active,
23 VerifyOnly,
24 Disabled,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct SigningKey {
30 pub kid: String,
31 pub status: SigningKeyStatus,
32 pub n: String,
34 pub e: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40pub struct SigningKeySet {
41 pub version: u64,
42 pub keys: Vec<SigningKey>,
43}
44
45impl SigningKeySet {
46 pub fn validate_update(&self, previous: Option<&Self>) -> Result<(), &'static str> {
47 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
48 if self.version == 0 || self.version > 9_007_199_254_740_991 {
49 return Err("invalid key set version");
50 }
51 if self.keys.is_empty() || self.keys.len() > 128 {
52 return Err("invalid key set size");
53 }
54 let mut kids = BTreeSet::new();
55 let mut active = 0;
56 for key in &self.keys {
57 if key.kid.is_empty() || key.kid.len() > 128 || !kids.insert(&key.kid) {
58 return Err("invalid or duplicate signing key ID");
59 }
60 let n = URL_SAFE_NO_PAD
61 .decode(&key.n)
62 .map_err(|_| "invalid RSA modulus")?;
63 let e = URL_SAFE_NO_PAD
64 .decode(&key.e)
65 .map_err(|_| "invalid RSA exponent")?;
66 if !(256..=512).contains(&n.len())
67 || n.first().is_some_and(|v| *v < 128)
68 || e != [1, 0, 1]
69 {
70 return Err("unsupported RSA public key");
71 }
72 active += usize::from(key.status == SigningKeyStatus::Active);
73 }
74 if active != 1 {
75 return Err("exactly one active signing key is required");
76 }
77 if let Some(old) = previous {
78 if self.version < old.version {
79 return Err("key set rollback");
80 }
81 let index: BTreeMap<_, _> = old.keys.iter().map(|k| (&k.kid, k)).collect();
82 for key in &self.keys {
83 if let Some(prior) = index.get(&key.kid) {
84 if key.n != prior.n || key.e != prior.e {
85 return Err("signing key ID was reused");
86 }
87 if prior.status == SigningKeyStatus::Disabled
88 && key.status != SigningKeyStatus::Disabled
89 {
90 return Err("disabled signing key was reactivated");
91 }
92 }
93 }
94 let next: BTreeMap<_, _> = self.keys.iter().map(|k| (&k.kid, k)).collect();
95 if self.version == old.version && next != index {
96 return Err("conflicting key set version");
97 }
98 if old
100 .keys
101 .iter()
102 .any(|k| k.status == SigningKeyStatus::Disabled && !next.contains_key(&k.kid))
103 {
104 return Err("disabled signing key tombstone was removed");
105 }
106 }
107 Ok(())
108 }
109
110 pub fn permits_binding(&self, kid: &str) -> bool {
111 self.keys
112 .iter()
113 .any(|key| key.kid == kid && key.status != SigningKeyStatus::Disabled)
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
119pub enum ErrorCode {
120 HostReauthRequired,
121 HostEnrollmentRequired,
122 HostUserNotAuthorized,
123 HostContextChanged,
124 HostReplayDetected,
125 HostAuthUnavailable,
126 HostInvalidProof,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase", deny_unknown_fields)]
132pub struct AuthError {
133 pub code: ErrorCode,
134 pub host_id: String,
135 pub boot_id: String,
136 pub request_id: String,
137 pub key_set_version: u64,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct ContextRequest {
143 pub probe_id: String,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct HostContext {
149 pub protocol: String,
150 pub host_id: String,
151 pub boot_id: String,
152 pub probe_id: String,
153 pub server_time: u64,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase", deny_unknown_fields)]
158pub struct EnrollmentRequest {
159 pub proof: String,
160 pub client_signature: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase", deny_unknown_fields)]
165pub struct EnrollmentChallengeRequest {
166 pub user_id: String,
167 pub client_public_key: PublicKey,
168 pub probe_id: String,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase", deny_unknown_fields)]
173pub struct EnrollmentChallenge {
174 pub host_id: String,
175 pub boot_id: String,
176 pub user_id: String,
177 pub client_public_key: PublicKey,
178 pub probe_id: String,
179 pub challenge: String,
180 pub expires_at: u64,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct ClientBinding {
186 pub user_id: String,
187 pub client_public_key: PublicKey,
188 pub enrollment_kid: String,
189}
190
191#[derive(Clone, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase", deny_unknown_fields)]
194pub struct Grant {
195 pub sub: String,
196 pub aud: String,
197 pub jti: String,
198 pub iat: u64,
199 pub exp: u64,
200 pub host_id: String,
201 pub host_public_key: PublicKey,
202 pub client_public_key: Option<PublicKey>,
203 pub challenge: String,
204 pub boot_id: Option<String>,
205}
206#[derive(Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208pub struct LocalAuthorization {
209 pub host_id: String,
210 pub host_name: String,
211 pub host_public_key: PublicKey,
212 pub transaction_id: String,
213 pub expires_at: u64,
214}
215#[derive(Serialize, Deserialize)]
216#[serde(rename_all = "camelCase", deny_unknown_fields)]
217pub struct Possession {
218 pub proof_digest: String,
219}
220#[derive(Serialize, Deserialize)]
221#[serde(rename_all = "camelCase", deny_unknown_fields)]
222pub struct AuthorizationReceipt {
223 pub proof: String,
224 pub host_id: String,
225 pub user_id: String,
226}