cat_gateway/service/common/auth/rbac/
token.rs1use std::{
6 fmt::{Display, Formatter},
7 time::Duration,
8};
9
10use anyhow::{anyhow, Context, Result};
11use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
12use cardano_blockchain_types::Network;
13use catalyst_types::id_uri::{key_rotation::KeyRotation, role_index::RoleIndex, IdUri};
14use chrono::{TimeDelta, Utc};
15use ed25519_dalek::{ed25519::signature::Signer, Signature, SigningKey, VerifyingKey};
16
17#[derive(Debug, Clone)]
23pub(crate) struct CatalystRBACTokenV1 {
24 catalyst_id: IdUri,
26 network: Network,
31 signature: Signature,
33 raw: Vec<u8>,
35}
36
37impl CatalystRBACTokenV1 {
38 const AUTH_TOKEN_PREFIX: &str = "catid.";
40
41 #[allow(dead_code)]
44 pub(crate) fn new(
45 network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, sk: &SigningKey,
46 ) -> Result<Self> {
47 let catalyst_id = IdUri::new(network, subnet, role0_pk).with_nonce().as_id();
48 let network = convert_network(&catalyst_id.network())?;
49 let raw = as_raw_bytes(&catalyst_id.to_string());
50 let signature = sk.sign(&raw);
51
52 Ok(Self {
53 catalyst_id,
54 network,
55 signature,
56 raw,
57 })
58 }
59
60 pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
74 let token = token
75 .strip_prefix(Self::AUTH_TOKEN_PREFIX)
76 .ok_or_else(|| anyhow!("Missing token prefix"))?;
77 let (token, signature) = token
78 .rsplit_once('.')
79 .ok_or_else(|| anyhow!("Missing token signature"))?;
80 let signature = BASE64_URL_SAFE_NO_PAD
81 .decode(signature.as_bytes())
82 .context("Invalid token signature encoding")?
83 .try_into()
84 .map(|b| Signature::from_bytes(&b))
85 .map_err(|_| anyhow!("Invalid token signature length"))?;
86 let raw = as_raw_bytes(token);
87
88 let catalyst_id: IdUri = token.parse().context("Invalid Catalyst ID")?;
89 if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
90 return Err(anyhow!("Catalyst ID must not contain username"));
91 }
92 if !catalyst_id.clone().is_id() {
93 return Err(anyhow!("Catalyst ID must be in an ID format"));
94 }
95 if catalyst_id.nonce().is_none() {
96 return Err(anyhow!("Catalyst ID must have nonce"));
97 }
98 let (role, rotation) = catalyst_id.role_and_rotation();
99 if role != RoleIndex::DEFAULT {
100 return Err(anyhow!("Catalyst ID mustn't have role specified"));
101 }
102 if rotation != KeyRotation::DEFAULT {
103 return Err(anyhow!("Catalyst ID mustn't have rotation specified"));
104 }
105 let network = convert_network(&catalyst_id.network())?;
106
107 Ok(Self {
108 catalyst_id,
109 network,
110 signature,
111 raw,
112 })
113 }
114
115 pub(crate) fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
117 public_key
118 .verify_strict(&self.raw, &self.signature)
119 .context("Token signature verification failed")
120 }
121
122 pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
126 let Some(token_age) = self.catalyst_id.nonce() else {
127 return false;
128 };
129
130 let now = Utc::now();
131
132 let Ok(max_age) = TimeDelta::from_std(max_age) else {
136 return false;
137 };
138 let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
139 return false;
140 };
141 let Some(min_time) = now.checked_sub_signed(max_age) else {
142 return false;
143 };
144 let Some(max_time) = now.checked_add_signed(max_skew) else {
145 return false;
146 };
147 (min_time < token_age) && (max_time > token_age)
148 }
149
150 pub(crate) fn catalyst_id(&self) -> &IdUri {
152 &self.catalyst_id
153 }
154
155 pub(crate) fn network(&self) -> Network {
157 self.network
158 }
159}
160
161impl From<CatalystRBACTokenV1> for IdUri {
162 fn from(value: CatalystRBACTokenV1) -> Self {
163 value.catalyst_id
164 }
165}
166
167impl Display for CatalystRBACTokenV1 {
168 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169 write!(
170 f,
171 "{}{}.{}",
172 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
173 self.catalyst_id,
174 BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
175 )
176 }
177}
178
179fn as_raw_bytes(token: &str) -> Vec<u8> {
181 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
183 .bytes()
184 .chain(token.bytes())
185 .chain(".".bytes())
186 .collect()
187}
188
189fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
191 if network != "cardano" {
192 return Err(anyhow!("Unsupported network: {network}"));
193 }
194
195 match subnet.as_deref() {
196 Some("mainnet") => Ok(Network::Mainnet),
197 Some("preprod") => Ok(Network::Preprod),
198 Some("preview") => Ok(Network::Preview),
199 _ => Err(anyhow!("Unsupported network: {network}")),
200 }
201}
202
203#[cfg(test)]
204mod tests {
205
206 use ed25519_dalek::SigningKey;
207 use rand::rngs::OsRng;
208
209 use super::*;
210
211 #[test]
212 fn roundtrip() {
213 let mut seed = OsRng;
214 let signing_key: SigningKey = SigningKey::generate(&mut seed);
215 let verifying_key = signing_key.verifying_key();
216 let token =
217 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
218 .unwrap();
219 assert_eq!(token.catalyst_id().username(), None);
220 assert!(token.catalyst_id().nonce().is_some());
221 assert_eq!(
222 token.catalyst_id().network(),
223 ("cardano".into(), Some("preprod".into()))
224 );
225 assert!(!token.catalyst_id().is_encryption_key());
226 assert!(token.catalyst_id().is_signature_key());
227
228 let token_str = token.to_string();
229 let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
230 assert_eq!(token.signature, parsed.signature);
231 assert_eq!(token.raw, parsed.raw);
232 assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
233 assert!(parsed.catalyst_id().nonce().is_some());
234 assert_eq!(
235 parsed.catalyst_id().network(),
236 ("cardano".into(), Some("preprod".into()))
237 );
238 assert!(!token.catalyst_id().is_encryption_key());
239 assert!(token.catalyst_id().is_signature_key());
240
241 let parsed_str = parsed.to_string();
242 assert_eq!(token_str, parsed_str);
243 }
244
245 #[test]
246 fn is_young() {
247 let mut seed = OsRng;
248 let signing_key: SigningKey = SigningKey::generate(&mut seed);
249 let verifying_key = signing_key.verifying_key();
250 let mut token =
251 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
252 .unwrap();
253
254 let now = Utc::now();
256 token.catalyst_id = token
257 .catalyst_id
258 .with_specific_nonce(now - Duration::from_secs(2));
259
260 let max_age = Duration::from_secs(1);
262 let max_skew = Duration::from_secs(1);
263 assert!(!token.is_young(max_age, max_skew));
264
265 let max_age = Duration::from_secs(3);
267 assert!(token.is_young(max_age, max_skew));
268
269 token.catalyst_id = token
271 .catalyst_id
272 .with_specific_nonce(now + Duration::from_secs(2));
273
274 let max_skew = Duration::from_secs(1);
276 assert!(!token.is_young(max_age, max_skew));
277
278 let max_skew = Duration::from_secs(3);
280 assert!(token.is_young(max_age, max_skew));
281 }
282
283 #[test]
284 fn verify() {
285 let mut seed = OsRng;
286 let signing_key: SigningKey = SigningKey::generate(&mut seed);
287 let verifying_key = signing_key.verifying_key();
288 let token =
289 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
290 .unwrap();
291 token.verify(&verifying_key).unwrap();
292 }
293}