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::{Context, Result, anyhow};
12use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
13use cardano_chain_follower::Network;
14use catalyst_types::catalyst_id::{CatalystId, key_rotation::KeyRotation, role_index::RoleId};
15use chrono::{TimeDelta, Utc};
16use ed25519_dalek::{Signature, VerifyingKey};
17use rbac_registration::registration::cardano::RegistrationChain;
18use regex::Regex;
19
20use crate::{rbac::latest_rbac_chain, settings::Settings};
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
50#[derive(thiserror::Error, Debug)]
51pub(crate) enum VerificationError {
52    /// Not a Admin RBAC token
53    #[error("Not a valid Admin RBAC token")]
54    NotAdmin,
55    /// Registration chain cannot be found.
56    #[error("Registration not found for the auth token.")]
57    RegistrationNotFound,
58    /// Latest signing key cannot be found.
59    #[error("Unable to get the latest signing key.")]
60    LatestSigningKey,
61    /// Invalid RBAC token signature
62    #[error("Invalid RBAC Token signature.")]
63    InvalidSignature,
64}
65
66impl CatalystRBACTokenV1 {
67    /// Bearer Token prefix for this token.
68    const AUTH_TOKEN_PREFIX: &str = "catid.";
69
70    /// Creates a new token instance.
71    #[cfg(test)]
72    pub(crate) fn new(
73        network: &str,
74        subnet: Option<&str>,
75        role0_pk: VerifyingKey,
76        sk: &ed25519_dalek::SigningKey,
77    ) -> Result<Self> {
78        use ed25519_dalek::ed25519::signature::Signer;
79
80        let catalyst_id = CatalystId::new(network, subnet, role0_pk)
81            .with_nonce()
82            .as_id();
83        let network = convert_network(&catalyst_id.network())?;
84        let raw = as_raw_bytes(&catalyst_id.to_string());
85        let signature = sk.sign(&raw);
86
87        Ok(Self {
88            catalyst_id,
89            network,
90            signature,
91            raw,
92            reg_chain: None,
93        })
94    }
95
96    /// Parses a token from the given string.
97    ///
98    /// The token consists of the following parts:
99    /// - "catid" prefix.
100    /// - Nonce.
101    /// - Network.
102    /// - Role 0 public key.
103    /// - Signature.
104    ///
105    /// For example:
106    /// ```
107    /// catid.:173710179@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE.<signature>
108    /// ```
109    pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
110        let token = token
111            .strip_prefix(Self::AUTH_TOKEN_PREFIX)
112            .ok_or_else(|| anyhow!("Missing token prefix"))?;
113        let (token, signature) = token
114            .rsplit_once('.')
115            .ok_or_else(|| anyhow!("Missing token signature"))?;
116        let signature = BASE64_URL_SAFE_NO_PAD
117            .decode(signature.as_bytes())
118            .context("Invalid token signature encoding")?
119            .try_into()
120            .map(|b| Signature::from_bytes(&b))
121            .map_err(|_| anyhow!("Invalid token signature length"))?;
122        let raw = as_raw_bytes(token);
123
124        let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
125        if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
126            return Err(anyhow!("Catalyst ID must not contain username"));
127        }
128        if catalyst_id.is_uri() {
129            return Err(anyhow!("Catalyst ID cannot be in URI format"));
130        }
131        if catalyst_id.nonce().is_none() {
132            return Err(anyhow!("Catalyst ID must have nonce"));
133        }
134
135        if REGEX.is_match(token) {
136            return Err(anyhow!(
137                "Catalyst ID mustn't have role or rotation specified"
138            ));
139        }
140        let network = convert_network(&catalyst_id.network())?;
141
142        Ok(Self {
143            catalyst_id,
144            network,
145            signature,
146            raw,
147            reg_chain: None,
148        })
149    }
150
151    /// Return the latest signing public key for the provided role.
152    /// If the its an admin RBAC token, returns associated Admin public key.
153    pub(crate) async fn get_latest_signing_public_key_for_role(
154        &mut self,
155        role: RoleId,
156    ) -> Result<(VerifyingKey, KeyRotation)> {
157        let res = if self.catalyst_id.is_admin() {
158            Settings::admin_cfg()
159                .get_admin_key(&self.catalyst_id, role)
160                .ok_or(VerificationError::NotAdmin)?
161        } else {
162            let reg_chain = self
163                .reg_chain()
164                .await?
165                .ok_or(VerificationError::RegistrationNotFound)?;
166            reg_chain
167                .get_latest_signing_public_key_for_role(role)
168                .ok_or_else(|| {
169                    tracing::debug!(
170                        "Unable to get last signing key for {} Catalyst ID",
171                        self.catalyst_id
172                    );
173                    VerificationError::LatestSigningKey
174                })?
175        };
176
177        Ok(res)
178    }
179
180    /// Given the `PublicKey`, verifies the token was correctly signed.
181    pub(crate) async fn verify(&mut self) -> Result<()> {
182        let public_key = self
183            .get_latest_signing_public_key_for_role(RoleId::Role0)
184            .await?
185            .0;
186
187        Ok(public_key
188            .verify_strict(&self.raw, &self.signature)
189            .map_err(|_| VerificationError::InvalidSignature)?)
190    }
191
192    /// Checks that the token timestamp is valid.
193    ///
194    /// The timestamp is valid if it isn't too old or too skewed.
195    pub(crate) fn is_young(
196        &self,
197        max_age: Duration,
198        max_skew: Duration,
199    ) -> bool {
200        let Some(token_age) = self.catalyst_id.nonce() else {
201            return false;
202        };
203
204        let now = Utc::now();
205
206        // The token is considered old if it was issued more than max_age ago.
207        // And newer than an allowed clock skew value
208        // This is a safety measure to avoid replay attacks.
209        let Ok(max_age) = TimeDelta::from_std(max_age) else {
210            return false;
211        };
212        let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
213            return false;
214        };
215        let Some(min_time) = now.checked_sub_signed(max_age) else {
216            return false;
217        };
218        let Some(max_time) = now.checked_add_signed(max_skew) else {
219            return false;
220        };
221        (min_time < token_age) && (max_time > token_age)
222    }
223
224    /// Returns a Catalyst ID from the token.
225    pub(crate) fn catalyst_id(&self) -> &CatalystId {
226        &self.catalyst_id
227    }
228
229    /// Returns a network.
230    #[allow(dead_code)]
231    pub(crate) fn network(&self) -> &Network {
232        &self.network
233    }
234
235    /// Returns a corresponded registration chain if any registrations present.
236    /// If it is a first call, fetch all data from the database and initialize it.
237    pub(crate) async fn reg_chain(&mut self) -> Result<Option<RegistrationChain>> {
238        if self.reg_chain.is_none() {
239            self.reg_chain = latest_rbac_chain(&self.catalyst_id).await?.map(|i| i.chain);
240        }
241        Ok(self.reg_chain.clone())
242    }
243}
244
245impl Display for CatalystRBACTokenV1 {
246    fn fmt(
247        &self,
248        f: &mut Formatter<'_>,
249    ) -> std::fmt::Result {
250        write!(
251            f,
252            "{}{}.{}",
253            CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
254            self.catalyst_id,
255            BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
256        )
257    }
258}
259
260/// Converts the given token string to raw bytes.
261fn as_raw_bytes(token: &str) -> Vec<u8> {
262    // The signature is calculated over all bytes in the token including the final '.'.
263    CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
264        .bytes()
265        .chain(token.bytes())
266        .chain(".".bytes())
267        .collect()
268}
269
270/// Checks if the given network is supported.
271fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
272    if network != "cardano" {
273        return Err(anyhow!("Unsupported network: {network}"));
274    }
275
276    match subnet.as_deref() {
277        None => Ok(Network::Mainnet),
278        Some("preprod") => Ok(Network::Preprod),
279        Some("preview") => Ok(Network::Preview),
280        Some(subnet) => Err(anyhow!("Unsupported host: {subnet}.{network}",)),
281    }
282}
283
284#[cfg(test)]
285mod tests {
286
287    use ed25519_dalek::SigningKey;
288    use rand::rngs::OsRng;
289    use test_case::test_case;
290
291    use super::*;
292
293    #[test_case("cardano", None ; "mainnet cardano network")]
294    #[test_case("cardano", Some("preprod") ; "preprod.cardano network")]
295    #[test_case("cardano", Some("preview") ; "preview.cardano network")]
296    fn roundtrip(
297        network: &'static str,
298        subnet: Option<&'static str>,
299    ) {
300        let mut seed = OsRng;
301        let signing_key: SigningKey = SigningKey::generate(&mut seed);
302        let verifying_key = signing_key.verifying_key();
303        let token = CatalystRBACTokenV1::new(network, subnet, verifying_key, &signing_key).unwrap();
304        assert_eq!(token.catalyst_id().username(), None);
305        assert!(token.catalyst_id().nonce().is_some());
306        assert_eq!(
307            token.catalyst_id().network(),
308            (network.to_string(), subnet.map(ToString::to_string))
309        );
310        assert!(!token.catalyst_id().is_encryption_key());
311        assert!(token.catalyst_id().is_signature_key());
312
313        let token_str = token.to_string();
314        let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
315        assert_eq!(token.signature, parsed.signature);
316        assert_eq!(token.raw, parsed.raw);
317        assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
318        assert!(parsed.catalyst_id().nonce().is_some());
319        assert_eq!(
320            parsed.catalyst_id().network(),
321            (network.to_string(), subnet.map(ToString::to_string))
322        );
323        assert!(!token.catalyst_id().is_encryption_key());
324        assert!(token.catalyst_id().is_signature_key());
325
326        let parsed_str = parsed.to_string();
327        assert_eq!(token_str, parsed_str);
328    }
329
330    #[test]
331    fn is_young() {
332        let mut seed = OsRng;
333        let signing_key: SigningKey = SigningKey::generate(&mut seed);
334        let verifying_key = signing_key.verifying_key();
335        let mut token =
336            CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
337                .unwrap();
338
339        // Update the token timestamp to be two seconds in the past.
340        let now = Utc::now();
341        token.catalyst_id = token
342            .catalyst_id
343            .with_specific_nonce(now - Duration::from_secs(2));
344
345        // Check that the token ISN'T young if max_age is one second.
346        let max_age = Duration::from_secs(1);
347        let max_skew = Duration::from_secs(1);
348        assert!(!token.is_young(max_age, max_skew));
349
350        // Check that the token IS young if max_age is three seconds.
351        let max_age = Duration::from_secs(3);
352        assert!(token.is_young(max_age, max_skew));
353
354        // Update the token timestamp to be two seconds in the future.
355        token.catalyst_id = token
356            .catalyst_id
357            .with_specific_nonce(now + Duration::from_secs(2));
358
359        // Check that the token IS too new if max_skew is one seconds.
360        let max_skew = Duration::from_secs(1);
361        assert!(!token.is_young(max_age, max_skew));
362
363        // Check that the token ISN'T too new if max_skew is three seconds.
364        let max_skew = Duration::from_secs(3);
365        assert!(token.is_young(max_age, max_skew));
366    }
367}