cat_gateway/service/common/auth/rbac/
token.rs1use std::{
6 fmt::{Display, Formatter},
7 sync::LazyLock,
8 time::Duration,
9};
10
11use anyhow::{anyhow, Context, Result};
12use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
13use cardano_chain_follower::Network;
14use catalyst_types::catalyst_id::CatalystId;
15use chrono::{TimeDelta, Utc};
16use ed25519_dalek::{ed25519::signature::Signer, Signature, SigningKey, VerifyingKey};
17use rbac_registration::registration::cardano::RegistrationChain;
18use regex::Regex;
19
20use crate::rbac::latest_rbac_chain;
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
50impl CatalystRBACTokenV1 {
51 const AUTH_TOKEN_PREFIX: &str = "catid.";
53
54 #[allow(dead_code)]
57 pub(crate) fn new(
58 network: &str,
59 subnet: Option<&str>,
60 role0_pk: VerifyingKey,
61 sk: &SigningKey,
62 ) -> Result<Self> {
63 let catalyst_id = CatalystId::new(network, subnet, role0_pk)
64 .with_nonce()
65 .as_id();
66 let network = convert_network(&catalyst_id.network())?;
67 let raw = as_raw_bytes(&catalyst_id.to_string());
68 let signature = sk.sign(&raw);
69
70 Ok(Self {
71 catalyst_id,
72 network,
73 signature,
74 raw,
75 reg_chain: None,
76 })
77 }
78
79 pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
93 let token = token
94 .strip_prefix(Self::AUTH_TOKEN_PREFIX)
95 .ok_or_else(|| anyhow!("Missing token prefix"))?;
96 let (token, signature) = token
97 .rsplit_once('.')
98 .ok_or_else(|| anyhow!("Missing token signature"))?;
99 let signature = BASE64_URL_SAFE_NO_PAD
100 .decode(signature.as_bytes())
101 .context("Invalid token signature encoding")?
102 .try_into()
103 .map(|b| Signature::from_bytes(&b))
104 .map_err(|_| anyhow!("Invalid token signature length"))?;
105 let raw = as_raw_bytes(token);
106
107 let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
108 if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
109 return Err(anyhow!("Catalyst ID must not contain username"));
110 }
111 if !catalyst_id.clone().is_id() {
112 return Err(anyhow!("Catalyst ID must be in an ID format"));
113 }
114 if catalyst_id.nonce().is_none() {
115 return Err(anyhow!("Catalyst ID must have nonce"));
116 }
117
118 if REGEX.is_match(token) {
119 return Err(anyhow!(
120 "Catalyst ID mustn't have role or rotation specified"
121 ));
122 }
123 let network = convert_network(&catalyst_id.network())?;
124
125 Ok(Self {
126 catalyst_id,
127 network,
128 signature,
129 raw,
130 reg_chain: None,
131 })
132 }
133
134 pub(crate) fn verify(
136 &self,
137 public_key: &VerifyingKey,
138 ) -> Result<()> {
139 public_key
140 .verify_strict(&self.raw, &self.signature)
141 .context("Token signature verification failed")
142 }
143
144 pub(crate) fn is_young(
148 &self,
149 max_age: Duration,
150 max_skew: Duration,
151 ) -> bool {
152 let Some(token_age) = self.catalyst_id.nonce() else {
153 return false;
154 };
155
156 let now = Utc::now();
157
158 let Ok(max_age) = TimeDelta::from_std(max_age) else {
162 return false;
163 };
164 let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
165 return false;
166 };
167 let Some(min_time) = now.checked_sub_signed(max_age) else {
168 return false;
169 };
170 let Some(max_time) = now.checked_add_signed(max_skew) else {
171 return false;
172 };
173 (min_time < token_age) && (max_time > token_age)
174 }
175
176 pub(crate) fn catalyst_id(&self) -> &CatalystId {
178 &self.catalyst_id
179 }
180
181 #[allow(dead_code)]
183 pub(crate) fn network(&self) -> Network {
184 self.network
185 }
186
187 pub(crate) async fn reg_chain(&mut self) -> Result<Option<RegistrationChain>> {
190 if self.reg_chain.is_none() {
191 self.reg_chain = latest_rbac_chain(&self.catalyst_id).await?.map(|i| i.chain);
192 }
193 Ok(self.reg_chain.clone())
194 }
195}
196
197impl Display for CatalystRBACTokenV1 {
198 fn fmt(
199 &self,
200 f: &mut Formatter<'_>,
201 ) -> std::fmt::Result {
202 write!(
203 f,
204 "{}{}.{}",
205 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
206 self.catalyst_id,
207 BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
208 )
209 }
210}
211
212fn as_raw_bytes(token: &str) -> Vec<u8> {
214 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
216 .bytes()
217 .chain(token.bytes())
218 .chain(".".bytes())
219 .collect()
220}
221
222fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
224 if network != "cardano" {
225 return Err(anyhow!("Unsupported network: {network}"));
226 }
227
228 match subnet.as_deref() {
229 None => Ok(Network::Mainnet),
230 Some("preprod") => Ok(Network::Preprod),
231 Some("preview") => Ok(Network::Preview),
232 Some(subnet) => Err(anyhow!("Unsupported host: {subnet}.{network}",)),
233 }
234}
235
236#[cfg(test)]
237mod tests {
238
239 use ed25519_dalek::SigningKey;
240 use rand::rngs::OsRng;
241 use test_case::test_case;
242
243 use super::*;
244
245 #[test_case("cardano", None ; "mainnet cardano network")]
246 #[test_case("cardano", Some("preprod") ; "preprod.cardano network")]
247 #[test_case("cardano", Some("preview") ; "preview.cardano network")]
248 fn roundtrip(
249 network: &'static str,
250 subnet: Option<&'static str>,
251 ) {
252 let mut seed = OsRng;
253 let signing_key: SigningKey = SigningKey::generate(&mut seed);
254 let verifying_key = signing_key.verifying_key();
255 let token = CatalystRBACTokenV1::new(network, subnet, verifying_key, &signing_key).unwrap();
256 assert_eq!(token.catalyst_id().username(), None);
257 assert!(token.catalyst_id().nonce().is_some());
258 assert_eq!(
259 token.catalyst_id().network(),
260 (network.to_string(), subnet.map(ToString::to_string))
261 );
262 assert!(!token.catalyst_id().is_encryption_key());
263 assert!(token.catalyst_id().is_signature_key());
264
265 let token_str = token.to_string();
266 let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
267 assert_eq!(token.signature, parsed.signature);
268 assert_eq!(token.raw, parsed.raw);
269 assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
270 assert!(parsed.catalyst_id().nonce().is_some());
271 assert_eq!(
272 parsed.catalyst_id().network(),
273 (network.to_string(), subnet.map(ToString::to_string))
274 );
275 assert!(!token.catalyst_id().is_encryption_key());
276 assert!(token.catalyst_id().is_signature_key());
277
278 let parsed_str = parsed.to_string();
279 assert_eq!(token_str, parsed_str);
280 }
281
282 #[test]
283 fn is_young() {
284 let mut seed = OsRng;
285 let signing_key: SigningKey = SigningKey::generate(&mut seed);
286 let verifying_key = signing_key.verifying_key();
287 let mut token =
288 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
289 .unwrap();
290
291 let now = Utc::now();
293 token.catalyst_id = token
294 .catalyst_id
295 .with_specific_nonce(now - Duration::from_secs(2));
296
297 let max_age = Duration::from_secs(1);
299 let max_skew = Duration::from_secs(1);
300 assert!(!token.is_young(max_age, max_skew));
301
302 let max_age = Duration::from_secs(3);
304 assert!(token.is_young(max_age, max_skew));
305
306 token.catalyst_id = token
308 .catalyst_id
309 .with_specific_nonce(now + Duration::from_secs(2));
310
311 let max_skew = Duration::from_secs(1);
313 assert!(!token.is_young(max_age, max_skew));
314
315 let max_skew = Duration::from_secs(3);
317 assert!(token.is_young(max_age, max_skew));
318 }
319
320 #[test]
321 fn verify() {
322 let mut seed = OsRng;
323 let signing_key: SigningKey = SigningKey::generate(&mut seed);
324 let verifying_key = signing_key.verifying_key();
325 let token =
326 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
327 .unwrap();
328 token.verify(&verifying_key).unwrap();
329 }
330}