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, subnet: Option<&str>, role0_pk: VerifyingKey, sk: &SigningKey,
59 ) -> Result<Self> {
60 let catalyst_id = CatalystId::new(network, subnet, role0_pk)
61 .with_nonce()
62 .as_id();
63 let network = convert_network(&catalyst_id.network())?;
64 let raw = as_raw_bytes(&catalyst_id.to_string());
65 let signature = sk.sign(&raw);
66
67 Ok(Self {
68 catalyst_id,
69 network,
70 signature,
71 raw,
72 reg_chain: None,
73 })
74 }
75
76 pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
90 let token = token
91 .strip_prefix(Self::AUTH_TOKEN_PREFIX)
92 .ok_or_else(|| anyhow!("Missing token prefix"))?;
93 let (token, signature) = token
94 .rsplit_once('.')
95 .ok_or_else(|| anyhow!("Missing token signature"))?;
96 let signature = BASE64_URL_SAFE_NO_PAD
97 .decode(signature.as_bytes())
98 .context("Invalid token signature encoding")?
99 .try_into()
100 .map(|b| Signature::from_bytes(&b))
101 .map_err(|_| anyhow!("Invalid token signature length"))?;
102 let raw = as_raw_bytes(token);
103
104 let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
105 if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
106 return Err(anyhow!("Catalyst ID must not contain username"));
107 }
108 if !catalyst_id.clone().is_id() {
109 return Err(anyhow!("Catalyst ID must be in an ID format"));
110 }
111 if catalyst_id.nonce().is_none() {
112 return Err(anyhow!("Catalyst ID must have nonce"));
113 }
114
115 if REGEX.is_match(token) {
116 return Err(anyhow!(
117 "Catalyst ID mustn't have role or rotation specified"
118 ));
119 }
120 let network = convert_network(&catalyst_id.network())?;
121
122 Ok(Self {
123 catalyst_id,
124 network,
125 signature,
126 raw,
127 reg_chain: None,
128 })
129 }
130
131 pub(crate) fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
133 public_key
134 .verify_strict(&self.raw, &self.signature)
135 .context("Token signature verification failed")
136 }
137
138 pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
142 let Some(token_age) = self.catalyst_id.nonce() else {
143 return false;
144 };
145
146 let now = Utc::now();
147
148 let Ok(max_age) = TimeDelta::from_std(max_age) else {
152 return false;
153 };
154 let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
155 return false;
156 };
157 let Some(min_time) = now.checked_sub_signed(max_age) else {
158 return false;
159 };
160 let Some(max_time) = now.checked_add_signed(max_skew) else {
161 return false;
162 };
163 (min_time < token_age) && (max_time > token_age)
164 }
165
166 pub(crate) fn catalyst_id(&self) -> &CatalystId {
168 &self.catalyst_id
169 }
170
171 #[allow(dead_code)]
173 pub(crate) fn network(&self) -> Network {
174 self.network
175 }
176
177 pub(crate) async fn reg_chain(&mut self) -> Result<Option<RegistrationChain>> {
180 if self.reg_chain.is_none() {
181 self.reg_chain = latest_rbac_chain(&self.catalyst_id).await?.map(|i| i.chain);
182 }
183 Ok(self.reg_chain.clone())
184 }
185}
186
187impl Display for CatalystRBACTokenV1 {
188 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
189 write!(
190 f,
191 "{}{}.{}",
192 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
193 self.catalyst_id,
194 BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
195 )
196 }
197}
198
199fn as_raw_bytes(token: &str) -> Vec<u8> {
201 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
203 .bytes()
204 .chain(token.bytes())
205 .chain(".".bytes())
206 .collect()
207}
208
209fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
211 if network != "cardano" {
212 return Err(anyhow!("Unsupported network: {network}"));
213 }
214
215 match subnet.as_deref() {
216 None => Ok(Network::Mainnet),
217 Some("preprod") => Ok(Network::Preprod),
218 Some("preview") => Ok(Network::Preview),
219 Some(subnet) => Err(anyhow!("Unsupported host: {subnet}.{network}",)),
220 }
221}
222
223#[cfg(test)]
224mod tests {
225
226 use ed25519_dalek::SigningKey;
227 use rand::rngs::OsRng;
228 use test_case::test_case;
229
230 use super::*;
231
232 #[test_case("cardano", None ; "mainnet cardano network")]
233 #[test_case("cardano", Some("preprod") ; "preprod.cardano network")]
234 #[test_case("cardano", Some("preview") ; "preview.cardano network")]
235 fn roundtrip(network: &'static str, subnet: Option<&'static str>) {
236 let mut seed = OsRng;
237 let signing_key: SigningKey = SigningKey::generate(&mut seed);
238 let verifying_key = signing_key.verifying_key();
239 let token = CatalystRBACTokenV1::new(network, subnet, verifying_key, &signing_key).unwrap();
240 assert_eq!(token.catalyst_id().username(), None);
241 assert!(token.catalyst_id().nonce().is_some());
242 assert_eq!(
243 token.catalyst_id().network(),
244 (network.to_string(), subnet.map(ToString::to_string))
245 );
246 assert!(!token.catalyst_id().is_encryption_key());
247 assert!(token.catalyst_id().is_signature_key());
248
249 let token_str = token.to_string();
250 let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
251 assert_eq!(token.signature, parsed.signature);
252 assert_eq!(token.raw, parsed.raw);
253 assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
254 assert!(parsed.catalyst_id().nonce().is_some());
255 assert_eq!(
256 parsed.catalyst_id().network(),
257 (network.to_string(), subnet.map(ToString::to_string))
258 );
259 assert!(!token.catalyst_id().is_encryption_key());
260 assert!(token.catalyst_id().is_signature_key());
261
262 let parsed_str = parsed.to_string();
263 assert_eq!(token_str, parsed_str);
264 }
265
266 #[test]
267 fn is_young() {
268 let mut seed = OsRng;
269 let signing_key: SigningKey = SigningKey::generate(&mut seed);
270 let verifying_key = signing_key.verifying_key();
271 let mut token =
272 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
273 .unwrap();
274
275 let now = Utc::now();
277 token.catalyst_id = token
278 .catalyst_id
279 .with_specific_nonce(now - Duration::from_secs(2));
280
281 let max_age = Duration::from_secs(1);
283 let max_skew = Duration::from_secs(1);
284 assert!(!token.is_young(max_age, max_skew));
285
286 let max_age = Duration::from_secs(3);
288 assert!(token.is_young(max_age, max_skew));
289
290 token.catalyst_id = token
292 .catalyst_id
293 .with_specific_nonce(now + Duration::from_secs(2));
294
295 let max_skew = Duration::from_secs(1);
297 assert!(!token.is_young(max_age, max_skew));
298
299 let max_skew = Duration::from_secs(3);
301 assert!(token.is_young(max_age, max_skew));
302 }
303
304 #[test]
305 fn verify() {
306 let mut seed = OsRng;
307 let signing_key: SigningKey = SigningKey::generate(&mut seed);
308 let verifying_key = signing_key.verifying_key();
309 let token =
310 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
311 .unwrap();
312 token.verify(&verifying_key).unwrap();
313 }
314}