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_blockchain_types::Network;
14use catalyst_types::catalyst_id::CatalystId;
15use chrono::{TimeDelta, Utc};
16use ed25519_dalek::{ed25519::signature::Signer, Signature, SigningKey, VerifyingKey};
17use futures::future::try_join;
18use rbac_registration::registration::cardano::RegistrationChain;
19use regex::Regex;
20
21use crate::db::index::{
22    queries::rbac::get_rbac_registrations::{build_reg_chain, indexed_registrations},
23    session::{CassandraSession, CassandraSessionError},
24};
25
26/// Captures just the digits after last slash
27/// This Regex should not fail
28#[allow(clippy::unwrap_used)]
29static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"/\d+$").unwrap());
30
31/// A Catalyst RBAC Authorization Token.
32///
33/// See [this document] for more details.
34///
35/// [this document]: https://github.com/input-output-hk/catalyst-voices/blob/main/docs/src/catalyst-standards/permissionless-auth/auth-header.md
36#[derive(Debug, Clone)]
37pub(crate) struct CatalystRBACTokenV1 {
38    /// A Catalyst identifier.
39    catalyst_id: CatalystId,
40    /// A network value.
41    ///
42    /// The network value is contained in the Catalyst ID and can be accessed from it, but
43    /// it is a string, so we convert it to this enum during the validation.
44    network: Network,
45    /// Ed25519 Signature of the Token
46    signature: Signature,
47    /// Raw bytes of the token without the signature.
48    raw: Vec<u8>,
49    /// A corresponded RBAC chain, constructed from the most recent data from the
50    /// database. Lazy initialized
51    reg_chain: Option<RegistrationChain>,
52}
53
54impl CatalystRBACTokenV1 {
55    /// Bearer Token prefix for this token.
56    const AUTH_TOKEN_PREFIX: &str = "catid.";
57
58    /// Creates a new token instance.
59    // TODO: Remove the attribute when the function is used.
60    #[allow(dead_code)]
61    pub(crate) fn new(
62        network: &str, subnet: Option<&str>, role0_pk: VerifyingKey, sk: &SigningKey,
63    ) -> Result<Self> {
64        let catalyst_id = CatalystId::new(network, subnet, role0_pk)
65            .with_nonce()
66            .as_id();
67        let network = convert_network(&catalyst_id.network())?;
68        let raw = as_raw_bytes(&catalyst_id.to_string());
69        let signature = sk.sign(&raw);
70
71        Ok(Self {
72            catalyst_id,
73            network,
74            signature,
75            raw,
76            reg_chain: None,
77        })
78    }
79
80    /// Parses a token from the given string.
81    ///
82    /// The token consists of the following parts:
83    /// - "catid" prefix.
84    /// - Nonce.
85    /// - Network.
86    /// - Role 0 public key.
87    /// - Signature.
88    ///
89    /// For example:
90    /// ```
91    /// catid.:173710179@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE.<signature>
92    /// ```
93    pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
94        let token = token
95            .strip_prefix(Self::AUTH_TOKEN_PREFIX)
96            .ok_or_else(|| anyhow!("Missing token prefix"))?;
97        let (token, signature) = token
98            .rsplit_once('.')
99            .ok_or_else(|| anyhow!("Missing token signature"))?;
100        let signature = BASE64_URL_SAFE_NO_PAD
101            .decode(signature.as_bytes())
102            .context("Invalid token signature encoding")?
103            .try_into()
104            .map(|b| Signature::from_bytes(&b))
105            .map_err(|_| anyhow!("Invalid token signature length"))?;
106        let raw = as_raw_bytes(token);
107
108        let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
109        if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
110            return Err(anyhow!("Catalyst ID must not contain username"));
111        }
112        if !catalyst_id.clone().is_id() {
113            return Err(anyhow!("Catalyst ID must be in an ID format"));
114        }
115        if catalyst_id.nonce().is_none() {
116            return Err(anyhow!("Catalyst ID must have nonce"));
117        }
118
119        if REGEX.is_match(token) {
120            return Err(anyhow!(
121                "Catalyst ID mustn't have role or rotation specified"
122            ));
123        }
124        let network = convert_network(&catalyst_id.network())?;
125
126        Ok(Self {
127            catalyst_id,
128            network,
129            signature,
130            raw,
131            reg_chain: None,
132        })
133    }
134
135    /// Given the `PublicKey`, verifies the token was correctly signed.
136    pub(crate) fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
137        public_key
138            .verify_strict(&self.raw, &self.signature)
139            .context("Token signature verification failed")
140    }
141
142    /// Checks that the token timestamp is valid.
143    ///
144    /// The timestamp is valid if it isn't too old or too skewed.
145    pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
146        let Some(token_age) = self.catalyst_id.nonce() else {
147            return false;
148        };
149
150        let now = Utc::now();
151
152        // The token is considered old if it was issued more than max_age ago.
153        // And newer than an allowed clock skew value
154        // This is a safety measure to avoid replay attacks.
155        let Ok(max_age) = TimeDelta::from_std(max_age) else {
156            return false;
157        };
158        let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
159            return false;
160        };
161        let Some(min_time) = now.checked_sub_signed(max_age) else {
162            return false;
163        };
164        let Some(max_time) = now.checked_add_signed(max_skew) else {
165            return false;
166        };
167        (min_time < token_age) && (max_time > token_age)
168    }
169
170    /// Returns a Catalyst ID from the token.
171    pub(crate) fn catalyst_id(&self) -> &CatalystId {
172        &self.catalyst_id
173    }
174
175    /// Returns a network.
176    pub(crate) fn network(&self) -> Network {
177        self.network
178    }
179
180    /// Returns a corresponded registration chain if any registrations present.
181    /// If it is a first call, fetch all data from the database and initialize it.
182    pub(crate) async fn reg_chain(&mut self) -> anyhow::Result<Option<RegistrationChain>> {
183        if self.reg_chain.is_none() {
184            let persistent_session =
185                CassandraSession::get(true).ok_or(CassandraSessionError::FailedAcquiringSession)?;
186            let volatile_session = CassandraSession::get(false)
187                .ok_or(CassandraSessionError::FailedAcquiringSession)?;
188            let (persistent_regs, volatile_regs) = try_join(
189                indexed_registrations(&persistent_session, self.catalyst_id()),
190                indexed_registrations(&volatile_session, self.catalyst_id()),
191            )
192            .await?;
193            // Combine persistent and volatile registrations.
194            let combined_regs = persistent_regs
195                .into_iter()
196                .map(|r| (true, r))
197                .chain(volatile_regs.into_iter().map(|r| (false, r)));
198            self.reg_chain = build_reg_chain(combined_regs, self.network(), |_, _, _| {}).await?;
199        }
200        Ok(self.reg_chain.clone())
201    }
202}
203
204impl Display for CatalystRBACTokenV1 {
205    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
206        write!(
207            f,
208            "{}{}.{}",
209            CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
210            self.catalyst_id,
211            BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
212        )
213    }
214}
215
216/// Converts the given token string to raw bytes.
217fn as_raw_bytes(token: &str) -> Vec<u8> {
218    // The signature is calculated over all bytes in the token including the final '.'.
219    CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
220        .bytes()
221        .chain(token.bytes())
222        .chain(".".bytes())
223        .collect()
224}
225
226/// Checks if the given network is supported.
227fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
228    if network != "cardano" {
229        return Err(anyhow!("Unsupported network: {network}"));
230    }
231
232    match subnet.as_deref() {
233        Some("mainnet") => Ok(Network::Mainnet),
234        Some("preprod") => Ok(Network::Preprod),
235        Some("preview") => Ok(Network::Preview),
236        Some(other) => Err(anyhow!("Unsupported subnet: {other}")),
237        None => Err(anyhow!("Missing subnet")),
238    }
239}
240
241#[cfg(test)]
242mod tests {
243
244    use ed25519_dalek::SigningKey;
245    use rand::rngs::OsRng;
246
247    use super::*;
248
249    #[test]
250    fn roundtrip() {
251        let mut seed = OsRng;
252        let signing_key: SigningKey = SigningKey::generate(&mut seed);
253        let verifying_key = signing_key.verifying_key();
254        let token =
255            CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
256                .unwrap();
257        assert_eq!(token.catalyst_id().username(), None);
258        assert!(token.catalyst_id().nonce().is_some());
259        assert_eq!(
260            token.catalyst_id().network(),
261            ("cardano".into(), Some("preprod".into()))
262        );
263        assert!(!token.catalyst_id().is_encryption_key());
264        assert!(token.catalyst_id().is_signature_key());
265
266        let token_str = token.to_string();
267        let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
268        assert_eq!(token.signature, parsed.signature);
269        assert_eq!(token.raw, parsed.raw);
270        assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
271        assert!(parsed.catalyst_id().nonce().is_some());
272        assert_eq!(
273            parsed.catalyst_id().network(),
274            ("cardano".into(), Some("preprod".into()))
275        );
276        assert!(!token.catalyst_id().is_encryption_key());
277        assert!(token.catalyst_id().is_signature_key());
278
279        let parsed_str = parsed.to_string();
280        assert_eq!(token_str, parsed_str);
281    }
282
283    #[test]
284    fn is_young() {
285        let mut seed = OsRng;
286        let signing_key: SigningKey = SigningKey::generate(&mut seed);
287        let verifying_key = signing_key.verifying_key();
288        let mut token =
289            CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
290                .unwrap();
291
292        // Update the token timestamp to be two seconds in the past.
293        let now = Utc::now();
294        token.catalyst_id = token
295            .catalyst_id
296            .with_specific_nonce(now - Duration::from_secs(2));
297
298        // Check that the token ISN'T young if max_age is one second.
299        let max_age = Duration::from_secs(1);
300        let max_skew = Duration::from_secs(1);
301        assert!(!token.is_young(max_age, max_skew));
302
303        // Check that the token IS young if max_age is three seconds.
304        let max_age = Duration::from_secs(3);
305        assert!(token.is_young(max_age, max_skew));
306
307        // Update the token timestamp to be two seconds in the future.
308        token.catalyst_id = token
309            .catalyst_id
310            .with_specific_nonce(now + Duration::from_secs(2));
311
312        // Check that the token IS too new if max_skew is one seconds.
313        let max_skew = Duration::from_secs(1);
314        assert!(!token.is_young(max_age, max_skew));
315
316        // Check that the token ISN'T too new if max_skew is three seconds.
317        let max_skew = Duration::from_secs(3);
318        assert!(token.is_young(max_age, max_skew));
319    }
320
321    #[test]
322    fn verify() {
323        let mut seed = OsRng;
324        let signing_key: SigningKey = SigningKey::generate(&mut seed);
325        let verifying_key = signing_key.verifying_key();
326        let token =
327            CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
328                .unwrap();
329        token.verify(&verifying_key).unwrap();
330    }
331}