cat_gateway/service/common/auth/rbac/
token.rs

1//! Catalyst RBAC Token utility functions.
2
3// cspell: words rsplit Fftx
4
5use 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/// Captures just the digits after last slash
23/// This Regex should not fail
24#[allow(clippy::unwrap_used)]
25static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/\d+$").unwrap());
26
27/// A Catalyst RBAC Authorization Token.
28///
29/// See [this document] for more details.
30///
31/// [this document]: https://github.com/input-output-hk/catalyst-voices/blob/main/docs/src/catalyst-standards/permissionless-auth/auth-header.md
32#[derive(Debug, Clone)]
33pub(crate) struct CatalystRBACTokenV1 {
34    /// A Catalyst identifier.
35    catalyst_id: CatalystId,
36    /// A network value.
37    ///
38    /// The network value is contained in the Catalyst ID and can be accessed from it, but
39    /// it is a string, so we convert it to this enum during the validation.
40    network: Network,
41    /// Ed25519 Signature of the Token
42    signature: Signature,
43    /// Raw bytes of the token without the signature.
44    raw: Vec<u8>,
45    /// A corresponded RBAC chain, constructed from the most recent data from the
46    /// database. Lazy initialized
47    reg_chain: Option<RegistrationChain>,
48}
49
50impl CatalystRBACTokenV1 {
51    /// Bearer Token prefix for this token.
52    const AUTH_TOKEN_PREFIX: &str = "catid.";
53
54    /// Creates a new token instance.
55    // TODO: Remove the attribute when the function is used.
56    #[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    /// Parses a token from the given string.
80    ///
81    /// The token consists of the following parts:
82    /// - "catid" prefix.
83    /// - Nonce.
84    /// - Network.
85    /// - Role 0 public key.
86    /// - Signature.
87    ///
88    /// For example:
89    /// ```
90    /// catid.:173710179@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE.<signature>
91    /// ```
92    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    /// Given the `PublicKey`, verifies the token was correctly signed.
135    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    /// Checks that the token timestamp is valid.
145    ///
146    /// The timestamp is valid if it isn't too old or too skewed.
147    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        // The token is considered old if it was issued more than max_age ago.
159        // And newer than an allowed clock skew value
160        // This is a safety measure to avoid replay attacks.
161        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    /// Returns a Catalyst ID from the token.
177    pub(crate) fn catalyst_id(&self) -> &CatalystId {
178        &self.catalyst_id
179    }
180
181    /// Returns a network.
182    #[allow(dead_code)]
183    pub(crate) fn network(&self) -> Network {
184        self.network
185    }
186
187    /// Returns a corresponded registration chain if any registrations present.
188    /// If it is a first call, fetch all data from the database and initialize it.
189    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
212/// Converts the given token string to raw bytes.
213fn as_raw_bytes(token: &str) -> Vec<u8> {
214    // The signature is calculated over all bytes in the token including the final '.'.
215    CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
216        .bytes()
217        .chain(token.bytes())
218        .chain(".".bytes())
219        .collect()
220}
221
222/// Checks if the given network is supported.
223fn 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        // Update the token timestamp to be two seconds in the past.
292        let now = Utc::now();
293        token.catalyst_id = token
294            .catalyst_id
295            .with_specific_nonce(now - Duration::from_secs(2));
296
297        // Check that the token ISN'T young if max_age is one second.
298        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        // Check that the token IS young if max_age is three seconds.
303        let max_age = Duration::from_secs(3);
304        assert!(token.is_young(max_age, max_skew));
305
306        // Update the token timestamp to be two seconds in the future.
307        token.catalyst_id = token
308            .catalyst_id
309            .with_specific_nonce(now + Duration::from_secs(2));
310
311        // Check that the token IS too new if max_skew is one seconds.
312        let max_skew = Duration::from_secs(1);
313        assert!(!token.is_young(max_age, max_skew));
314
315        // Check that the token ISN'T too new if max_skew is three seconds.
316        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}