cat_gateway/service/common/auth/rbac/
token.rs1use std::{
6 fmt::{Display, Formatter},
7 sync::LazyLock,
8 time::Duration,
9};
10
11use anyhow::{Context, Result, anyhow};
12use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
13use cardano_chain_follower::Network;
14use catalyst_types::catalyst_id::{CatalystId, key_rotation::KeyRotation, role_index::RoleId};
15use chrono::{TimeDelta, Utc};
16use ed25519_dalek::{Signature, VerifyingKey};
17use rbac_registration::registration::cardano::RegistrationChain;
18use regex::Regex;
19
20use crate::{rbac::latest_rbac_chain, settings::Settings};
21
22#[allow(clippy::unwrap_used)]
25static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/\d+$").unwrap());
26
27#[derive(Debug, Clone)]
33pub(crate) struct CatalystRBACTokenV1 {
34 catalyst_id: CatalystId,
36 network: Network,
41 signature: Signature,
43 raw: Vec<u8>,
45 reg_chain: Option<RegistrationChain>,
48}
49
50#[derive(thiserror::Error, Debug)]
51pub(crate) enum VerificationError {
52 #[error("Not a valid Admin RBAC token")]
54 NotAdmin,
55 #[error("Registration not found for the auth token.")]
57 RegistrationNotFound,
58 #[error("Unable to get the latest signing key.")]
60 LatestSigningKey,
61 #[error("Invalid RBAC Token signature.")]
63 InvalidSignature,
64}
65
66impl CatalystRBACTokenV1 {
67 const AUTH_TOKEN_PREFIX: &str = "catid.";
69
70 #[cfg(test)]
72 pub(crate) fn new(
73 network: &str,
74 subnet: Option<&str>,
75 role0_pk: VerifyingKey,
76 sk: &ed25519_dalek::SigningKey,
77 ) -> Result<Self> {
78 use ed25519_dalek::ed25519::signature::Signer;
79
80 let catalyst_id = CatalystId::new(network, subnet, role0_pk)
81 .with_nonce()
82 .as_id();
83 let network = convert_network(&catalyst_id.network())?;
84 let raw = as_raw_bytes(&catalyst_id.to_string());
85 let signature = sk.sign(&raw);
86
87 Ok(Self {
88 catalyst_id,
89 network,
90 signature,
91 raw,
92 reg_chain: None,
93 })
94 }
95
96 pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
110 let token = token
111 .strip_prefix(Self::AUTH_TOKEN_PREFIX)
112 .ok_or_else(|| anyhow!("Missing token prefix"))?;
113 let (token, signature) = token
114 .rsplit_once('.')
115 .ok_or_else(|| anyhow!("Missing token signature"))?;
116 let signature = BASE64_URL_SAFE_NO_PAD
117 .decode(signature.as_bytes())
118 .context("Invalid token signature encoding")?
119 .try_into()
120 .map(|b| Signature::from_bytes(&b))
121 .map_err(|_| anyhow!("Invalid token signature length"))?;
122 let raw = as_raw_bytes(token);
123
124 let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
125 if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
126 return Err(anyhow!("Catalyst ID must not contain username"));
127 }
128 if catalyst_id.is_uri() {
129 return Err(anyhow!("Catalyst ID cannot be in URI format"));
130 }
131 if catalyst_id.nonce().is_none() {
132 return Err(anyhow!("Catalyst ID must have nonce"));
133 }
134
135 if REGEX.is_match(token) {
136 return Err(anyhow!(
137 "Catalyst ID mustn't have role or rotation specified"
138 ));
139 }
140 let network = convert_network(&catalyst_id.network())?;
141
142 Ok(Self {
143 catalyst_id,
144 network,
145 signature,
146 raw,
147 reg_chain: None,
148 })
149 }
150
151 pub(crate) async fn get_latest_signing_public_key_for_role(
154 &mut self,
155 role: RoleId,
156 ) -> Result<(VerifyingKey, KeyRotation)> {
157 let res = if self.catalyst_id.is_admin() {
158 Settings::admin_cfg()
159 .get_admin_key(&self.catalyst_id, role)
160 .ok_or(VerificationError::NotAdmin)?
161 } else {
162 let reg_chain = self
163 .reg_chain()
164 .await?
165 .ok_or(VerificationError::RegistrationNotFound)?;
166 reg_chain
167 .get_latest_signing_public_key_for_role(role)
168 .ok_or_else(|| {
169 tracing::debug!(
170 "Unable to get last signing key for {} Catalyst ID",
171 self.catalyst_id
172 );
173 VerificationError::LatestSigningKey
174 })?
175 };
176
177 Ok(res)
178 }
179
180 pub(crate) async fn verify(&mut self) -> Result<()> {
182 let public_key = self
183 .get_latest_signing_public_key_for_role(RoleId::Role0)
184 .await?
185 .0;
186
187 Ok(public_key
188 .verify_strict(&self.raw, &self.signature)
189 .map_err(|_| VerificationError::InvalidSignature)?)
190 }
191
192 pub(crate) fn is_young(
196 &self,
197 max_age: Duration,
198 max_skew: Duration,
199 ) -> bool {
200 let Some(token_age) = self.catalyst_id.nonce() else {
201 return false;
202 };
203
204 let now = Utc::now();
205
206 let Ok(max_age) = TimeDelta::from_std(max_age) else {
210 return false;
211 };
212 let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
213 return false;
214 };
215 let Some(min_time) = now.checked_sub_signed(max_age) else {
216 return false;
217 };
218 let Some(max_time) = now.checked_add_signed(max_skew) else {
219 return false;
220 };
221 (min_time < token_age) && (max_time > token_age)
222 }
223
224 pub(crate) fn catalyst_id(&self) -> &CatalystId {
226 &self.catalyst_id
227 }
228
229 #[allow(dead_code)]
231 pub(crate) fn network(&self) -> &Network {
232 &self.network
233 }
234
235 pub(crate) async fn reg_chain(&mut self) -> Result<Option<RegistrationChain>> {
238 if self.reg_chain.is_none() {
239 self.reg_chain = latest_rbac_chain(&self.catalyst_id).await?.map(|i| i.chain);
240 }
241 Ok(self.reg_chain.clone())
242 }
243}
244
245impl Display for CatalystRBACTokenV1 {
246 fn fmt(
247 &self,
248 f: &mut Formatter<'_>,
249 ) -> std::fmt::Result {
250 write!(
251 f,
252 "{}{}.{}",
253 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
254 self.catalyst_id,
255 BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
256 )
257 }
258}
259
260fn as_raw_bytes(token: &str) -> Vec<u8> {
262 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
264 .bytes()
265 .chain(token.bytes())
266 .chain(".".bytes())
267 .collect()
268}
269
270fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
272 if network != "cardano" {
273 return Err(anyhow!("Unsupported network: {network}"));
274 }
275
276 match subnet.as_deref() {
277 None => Ok(Network::Mainnet),
278 Some("preprod") => Ok(Network::Preprod),
279 Some("preview") => Ok(Network::Preview),
280 Some(subnet) => Err(anyhow!("Unsupported host: {subnet}.{network}",)),
281 }
282}
283
284#[cfg(test)]
285mod tests {
286
287 use ed25519_dalek::SigningKey;
288 use rand::rngs::OsRng;
289 use test_case::test_case;
290
291 use super::*;
292
293 #[test_case("cardano", None ; "mainnet cardano network")]
294 #[test_case("cardano", Some("preprod") ; "preprod.cardano network")]
295 #[test_case("cardano", Some("preview") ; "preview.cardano network")]
296 fn roundtrip(
297 network: &'static str,
298 subnet: Option<&'static str>,
299 ) {
300 let mut seed = OsRng;
301 let signing_key: SigningKey = SigningKey::generate(&mut seed);
302 let verifying_key = signing_key.verifying_key();
303 let token = CatalystRBACTokenV1::new(network, subnet, verifying_key, &signing_key).unwrap();
304 assert_eq!(token.catalyst_id().username(), None);
305 assert!(token.catalyst_id().nonce().is_some());
306 assert_eq!(
307 token.catalyst_id().network(),
308 (network.to_string(), subnet.map(ToString::to_string))
309 );
310 assert!(!token.catalyst_id().is_encryption_key());
311 assert!(token.catalyst_id().is_signature_key());
312
313 let token_str = token.to_string();
314 let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
315 assert_eq!(token.signature, parsed.signature);
316 assert_eq!(token.raw, parsed.raw);
317 assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
318 assert!(parsed.catalyst_id().nonce().is_some());
319 assert_eq!(
320 parsed.catalyst_id().network(),
321 (network.to_string(), subnet.map(ToString::to_string))
322 );
323 assert!(!token.catalyst_id().is_encryption_key());
324 assert!(token.catalyst_id().is_signature_key());
325
326 let parsed_str = parsed.to_string();
327 assert_eq!(token_str, parsed_str);
328 }
329
330 #[test]
331 fn is_young() {
332 let mut seed = OsRng;
333 let signing_key: SigningKey = SigningKey::generate(&mut seed);
334 let verifying_key = signing_key.verifying_key();
335 let mut token =
336 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
337 .unwrap();
338
339 let now = Utc::now();
341 token.catalyst_id = token
342 .catalyst_id
343 .with_specific_nonce(now - Duration::from_secs(2));
344
345 let max_age = Duration::from_secs(1);
347 let max_skew = Duration::from_secs(1);
348 assert!(!token.is_young(max_age, max_skew));
349
350 let max_age = Duration::from_secs(3);
352 assert!(token.is_young(max_age, max_skew));
353
354 token.catalyst_id = token
356 .catalyst_id
357 .with_specific_nonce(now + Duration::from_secs(2));
358
359 let max_skew = Duration::from_secs(1);
361 assert!(!token.is_young(max_age, max_skew));
362
363 let max_skew = Duration::from_secs(3);
365 assert!(token.is_young(max_age, max_skew));
366 }
367}