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_blockchain_types::Network;
14use catalyst_types::catalyst_id::CatalystId;
15use chrono::{TimeDelta, Utc};
16use ed25519_dalek::{ed25519::signature::Signer, Signature, SigningKey, VerifyingKey};
17use futures::future::try_join;
18use rbac_registration::registration::cardano::RegistrationChain;
19use regex::Regex;
20
21use crate::db::index::{
22 queries::rbac::get_rbac_registrations::{build_reg_chain, indexed_registrations},
23 session::{CassandraSession, CassandraSessionError},
24};
25
26#[allow(clippy::unwrap_used)]
29static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/\d+$").unwrap());
30
31#[derive(Debug, Clone)]
37pub(crate) struct CatalystRBACTokenV1 {
38 catalyst_id: CatalystId,
40 network: Network,
45 signature: Signature,
47 raw: Vec<u8>,
49 reg_chain: Option<RegistrationChain>,
52}
53
54impl CatalystRBACTokenV1 {
55 const AUTH_TOKEN_PREFIX: &str = "catid.";
57
58 #[allow(dead_code)]
61 pub(crate) fn new(
62 network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, sk: &SigningKey,
63 ) -> Result<Self> {
64 let catalyst_id = CatalystId::new(network, subnet, role0_pk)
65 .with_nonce()
66 .as_id();
67 let network = convert_network(&catalyst_id.network())?;
68 let raw = as_raw_bytes(&catalyst_id.to_string());
69 let signature = sk.sign(&raw);
70
71 Ok(Self {
72 catalyst_id,
73 network,
74 signature,
75 raw,
76 reg_chain: None,
77 })
78 }
79
80 pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
94 let token = token
95 .strip_prefix(Self::AUTH_TOKEN_PREFIX)
96 .ok_or_else(|| anyhow!("Missing token prefix"))?;
97 let (token, signature) = token
98 .rsplit_once('.')
99 .ok_or_else(|| anyhow!("Missing token signature"))?;
100 let signature = BASE64_URL_SAFE_NO_PAD
101 .decode(signature.as_bytes())
102 .context("Invalid token signature encoding")?
103 .try_into()
104 .map(|b| Signature::from_bytes(&b))
105 .map_err(|_| anyhow!("Invalid token signature length"))?;
106 let raw = as_raw_bytes(token);
107
108 let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
109 if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
110 return Err(anyhow!("Catalyst ID must not contain username"));
111 }
112 if !catalyst_id.clone().is_id() {
113 return Err(anyhow!("Catalyst ID must be in an ID format"));
114 }
115 if catalyst_id.nonce().is_none() {
116 return Err(anyhow!("Catalyst ID must have nonce"));
117 }
118
119 if REGEX.is_match(token) {
120 return Err(anyhow!(
121 "Catalyst ID mustn't have role or rotation specified"
122 ));
123 }
124 let network = convert_network(&catalyst_id.network())?;
125
126 Ok(Self {
127 catalyst_id,
128 network,
129 signature,
130 raw,
131 reg_chain: None,
132 })
133 }
134
135 pub(crate) fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
137 public_key
138 .verify_strict(&self.raw, &self.signature)
139 .context("Token signature verification failed")
140 }
141
142 pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
146 let Some(token_age) = self.catalyst_id.nonce() else {
147 return false;
148 };
149
150 let now = Utc::now();
151
152 let Ok(max_age) = TimeDelta::from_std(max_age) else {
156 return false;
157 };
158 let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
159 return false;
160 };
161 let Some(min_time) = now.checked_sub_signed(max_age) else {
162 return false;
163 };
164 let Some(max_time) = now.checked_add_signed(max_skew) else {
165 return false;
166 };
167 (min_time < token_age) && (max_time > token_age)
168 }
169
170 pub(crate) fn catalyst_id(&self) -> &CatalystId {
172 &self.catalyst_id
173 }
174
175 pub(crate) fn network(&self) -> Network {
177 self.network
178 }
179
180 pub(crate) async fn reg_chain(&mut self) -> anyhow::Result<Option<RegistrationChain>> {
183 if self.reg_chain.is_none() {
184 let persistent_session =
185 CassandraSession::get(true).ok_or(CassandraSessionError::FailedAcquiringSession)?;
186 let volatile_session = CassandraSession::get(false)
187 .ok_or(CassandraSessionError::FailedAcquiringSession)?;
188 let (persistent_regs, volatile_regs) = try_join(
189 indexed_registrations(&persistent_session, self.catalyst_id()),
190 indexed_registrations(&volatile_session, self.catalyst_id()),
191 )
192 .await?;
193 let combined_regs = persistent_regs
195 .into_iter()
196 .map(|r| (true, r))
197 .chain(volatile_regs.into_iter().map(|r| (false, r)));
198 self.reg_chain = build_reg_chain(combined_regs, self.network(), |_, _, _| {}).await?;
199 }
200 Ok(self.reg_chain.clone())
201 }
202}
203
204impl Display for CatalystRBACTokenV1 {
205 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
206 write!(
207 f,
208 "{}{}.{}",
209 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
210 self.catalyst_id,
211 BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
212 )
213 }
214}
215
216fn as_raw_bytes(token: &str) -> Vec<u8> {
218 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
220 .bytes()
221 .chain(token.bytes())
222 .chain(".".bytes())
223 .collect()
224}
225
226fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
228 if network != "cardano" {
229 return Err(anyhow!("Unsupported network: {network}"));
230 }
231
232 match subnet.as_deref() {
233 Some("mainnet") => Ok(Network::Mainnet),
234 Some("preprod") => Ok(Network::Preprod),
235 Some("preview") => Ok(Network::Preview),
236 Some(other) => Err(anyhow!("Unsupported subnet: {other}")),
237 None => Err(anyhow!("Missing subnet")),
238 }
239}
240
241#[cfg(test)]
242mod tests {
243
244 use ed25519_dalek::SigningKey;
245 use rand::rngs::OsRng;
246
247 use super::*;
248
249 #[test]
250 fn roundtrip() {
251 let mut seed = OsRng;
252 let signing_key: SigningKey = SigningKey::generate(&mut seed);
253 let verifying_key = signing_key.verifying_key();
254 let token =
255 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
256 .unwrap();
257 assert_eq!(token.catalyst_id().username(), None);
258 assert!(token.catalyst_id().nonce().is_some());
259 assert_eq!(
260 token.catalyst_id().network(),
261 ("cardano".into(), Some("preprod".into()))
262 );
263 assert!(!token.catalyst_id().is_encryption_key());
264 assert!(token.catalyst_id().is_signature_key());
265
266 let token_str = token.to_string();
267 let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
268 assert_eq!(token.signature, parsed.signature);
269 assert_eq!(token.raw, parsed.raw);
270 assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
271 assert!(parsed.catalyst_id().nonce().is_some());
272 assert_eq!(
273 parsed.catalyst_id().network(),
274 ("cardano".into(), Some("preprod".into()))
275 );
276 assert!(!token.catalyst_id().is_encryption_key());
277 assert!(token.catalyst_id().is_signature_key());
278
279 let parsed_str = parsed.to_string();
280 assert_eq!(token_str, parsed_str);
281 }
282
283 #[test]
284 fn is_young() {
285 let mut seed = OsRng;
286 let signing_key: SigningKey = SigningKey::generate(&mut seed);
287 let verifying_key = signing_key.verifying_key();
288 let mut token =
289 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
290 .unwrap();
291
292 let now = Utc::now();
294 token.catalyst_id = token
295 .catalyst_id
296 .with_specific_nonce(now - Duration::from_secs(2));
297
298 let max_age = Duration::from_secs(1);
300 let max_skew = Duration::from_secs(1);
301 assert!(!token.is_young(max_age, max_skew));
302
303 let max_age = Duration::from_secs(3);
305 assert!(token.is_young(max_age, max_skew));
306
307 token.catalyst_id = token
309 .catalyst_id
310 .with_specific_nonce(now + Duration::from_secs(2));
311
312 let max_skew = Duration::from_secs(1);
314 assert!(!token.is_young(max_age, max_skew));
315
316 let max_skew = Duration::from_secs(3);
318 assert!(token.is_young(max_age, max_skew));
319 }
320
321 #[test]
322 fn verify() {
323 let mut seed = OsRng;
324 let signing_key: SigningKey = SigningKey::generate(&mut seed);
325 let verifying_key = signing_key.verifying_key();
326 let token =
327 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
328 .unwrap();
329 token.verify(&verifying_key).unwrap();
330 }
331}