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 catalyst_types::id_uri::IdUri;
13use chrono::{TimeDelta, Utc};
14use ed25519_dalek::{ed25519::signature::Signer, Signature, SigningKey, VerifyingKey};
15
16#[derive(Debug, Clone)]
22pub(crate) struct CatalystRBACTokenV1 {
23 catalyst_id: IdUri,
25 signature: Signature,
27 raw: Vec<u8>,
29}
30
31impl CatalystRBACTokenV1 {
32 const AUTH_TOKEN_PREFIX: &str = "catid.";
34
35 #[allow(dead_code)]
38 pub(crate) fn new(
39 network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, sk: &SigningKey,
40 ) -> Self {
41 let catalyst_id = IdUri::new(network, subnet, role0_pk).with_nonce().as_id();
42 let raw = as_raw_bytes(&catalyst_id.to_string());
43 let signature = sk.sign(&raw);
44
45 Self {
46 catalyst_id,
47 signature,
48 raw,
49 }
50 }
51
52 pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
66 let token = token
67 .strip_prefix(Self::AUTH_TOKEN_PREFIX)
68 .ok_or_else(|| anyhow!("Missing token prefix"))?;
69 let (token, signature) = token
70 .rsplit_once('.')
71 .ok_or_else(|| anyhow!("Missing token signature"))?;
72 let signature = BASE64_URL_SAFE_NO_PAD
73 .decode(signature.as_bytes())
74 .context("Invalid token signature encoding")?
75 .try_into()
76 .map(|b| Signature::from_bytes(&b))
77 .map_err(|_| anyhow!("Invalid token signature length"))?;
78 let raw = as_raw_bytes(token);
79
80 let catalyst_id: IdUri = token.parse().context("Invalid Catalyst ID")?;
81 if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
82 return Err(anyhow!("Catalyst ID must not contain username"));
83 }
84 if !catalyst_id.clone().is_id() {
85 return Err(anyhow!("Catalyst ID must be in an ID format"));
86 }
87 if catalyst_id.nonce().is_none() {
88 return Err(anyhow!("Catalyst ID must have nonce"));
89 }
90 is_network_supported(&catalyst_id.network())?;
91
92 Ok(Self {
93 catalyst_id,
94 signature,
95 raw,
96 })
97 }
98
99 pub(crate) fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
101 public_key
102 .verify_strict(&self.raw, &self.signature)
103 .context("Token signature verification failed")
104 }
105
106 pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
110 let Some(token_age) = self.catalyst_id.nonce() else {
111 return false;
112 };
113
114 let now = Utc::now();
115
116 let Ok(max_age) = TimeDelta::from_std(max_age) else {
120 return false;
121 };
122 let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
123 return false;
124 };
125 let Some(min_time) = now.checked_sub_signed(max_age) else {
126 return false;
127 };
128 let Some(max_time) = now.checked_add_signed(max_skew) else {
129 return false;
130 };
131 (min_time < token_age) && (max_time > token_age)
132 }
133
134 pub(crate) fn catalyst_id(&self) -> &IdUri {
136 &self.catalyst_id
137 }
138}
139
140impl Display for CatalystRBACTokenV1 {
141 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142 write!(
143 f,
144 "{}{}.{}",
145 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
146 self.catalyst_id,
147 BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
148 )
149 }
150}
151
152fn as_raw_bytes(token: &str) -> Vec<u8> {
154 CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
156 .bytes()
157 .chain(token.bytes())
158 .chain(".".bytes())
159 .collect()
160}
161
162fn is_network_supported((network, subnet): &(String, Option<String>)) -> Result<()> {
164 if network != "cardano" {
165 return Err(anyhow!("Unsupported network: {network}"));
166 }
167
168 let subnet = subnet.as_deref();
169 if subnet != Some("mainnet") && subnet != Some("preprod") {
170 return Err(anyhow!("Unsupported subnet: {subnet:?}"));
171 }
172
173 Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178
179 use ed25519_dalek::SigningKey;
180 use rand::rngs::OsRng;
181
182 use super::*;
183
184 #[test]
185 fn roundtrip() {
186 let mut seed = OsRng;
187 let signing_key: SigningKey = SigningKey::generate(&mut seed);
188 let verifying_key = signing_key.verifying_key();
189 let token =
190 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key);
191 assert_eq!(token.catalyst_id().username(), None);
192 assert!(token.catalyst_id().nonce().is_some());
193 assert_eq!(
194 token.catalyst_id().network(),
195 ("cardano".into(), Some("preprod".into()))
196 );
197 assert!(!token.catalyst_id().is_encryption_key());
198 assert!(token.catalyst_id().is_signature_key());
199
200 let token_str = token.to_string();
201 let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
202 assert_eq!(token.signature, parsed.signature);
203 assert_eq!(token.raw, parsed.raw);
204 assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
205 assert!(parsed.catalyst_id().nonce().is_some());
206 assert_eq!(
207 parsed.catalyst_id().network(),
208 ("cardano".into(), Some("preprod".into()))
209 );
210 assert!(!token.catalyst_id().is_encryption_key());
211 assert!(token.catalyst_id().is_signature_key());
212
213 let parsed_str = parsed.to_string();
214 assert_eq!(token_str, parsed_str);
215 }
216
217 #[test]
218 fn is_young() {
219 let mut seed = OsRng;
220 let signing_key: SigningKey = SigningKey::generate(&mut seed);
221 let verifying_key = signing_key.verifying_key();
222 let mut token =
223 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key);
224
225 let now = Utc::now();
227 token.catalyst_id = token
228 .catalyst_id
229 .with_specific_nonce(now - Duration::from_secs(2));
230
231 let max_age = Duration::from_secs(1);
233 let max_skew = Duration::from_secs(1);
234 assert!(!token.is_young(max_age, max_skew));
235
236 let max_age = Duration::from_secs(3);
238 assert!(token.is_young(max_age, max_skew));
239
240 token.catalyst_id = token
242 .catalyst_id
243 .with_specific_nonce(now + Duration::from_secs(2));
244
245 let max_skew = Duration::from_secs(1);
247 assert!(!token.is_young(max_age, max_skew));
248
249 let max_skew = Duration::from_secs(3);
251 assert!(token.is_young(max_age, max_skew));
252 }
253
254 #[test]
255 fn verify() {
256 let mut seed = OsRng;
257 let signing_key: SigningKey = SigningKey::generate(&mut seed);
258 let verifying_key = signing_key.verifying_key();
259 let token =
260 CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key);
261 token.verify(&verifying_key).unwrap();
262 }
263}