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