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, subnet: Option<&str>, role0_pk: VerifyingKey, sk: &SigningKey,
59    ) -> Result<Self> {
60        let catalyst_id = CatalystId::new(network, subnet, role0_pk)
61            .with_nonce()
62            .as_id();
63        let network = convert_network(&catalyst_id.network())?;
64        let raw = as_raw_bytes(&catalyst_id.to_string());
65        let signature = sk.sign(&raw);
66
67        Ok(Self {
68            catalyst_id,
69            network,
70            signature,
71            raw,
72            reg_chain: None,
73        })
74    }
75
76    /// Parses a token from the given string.
77    ///
78    /// The token consists of the following parts:
79    /// - "catid" prefix.
80    /// - Nonce.
81    /// - Network.
82    /// - Role 0 public key.
83    /// - Signature.
84    ///
85    /// For example:
86    /// ```
87    /// catid.:173710179@preprod.cardano/FftxFnOrj2qmTuB2oZG2v0YEWJfKvQ9Gg8AgNAhDsKE.<signature>
88    /// ```
89    pub(crate) fn parse(token: &str) -> Result<CatalystRBACTokenV1> {
90        let token = token
91            .strip_prefix(Self::AUTH_TOKEN_PREFIX)
92            .ok_or_else(|| anyhow!("Missing token prefix"))?;
93        let (token, signature) = token
94            .rsplit_once('.')
95            .ok_or_else(|| anyhow!("Missing token signature"))?;
96        let signature = BASE64_URL_SAFE_NO_PAD
97            .decode(signature.as_bytes())
98            .context("Invalid token signature encoding")?
99            .try_into()
100            .map(|b| Signature::from_bytes(&b))
101            .map_err(|_| anyhow!("Invalid token signature length"))?;
102        let raw = as_raw_bytes(token);
103
104        let catalyst_id: CatalystId = token.parse().context("Invalid Catalyst ID")?;
105        if catalyst_id.username().is_some_and(|n| !n.is_empty()) {
106            return Err(anyhow!("Catalyst ID must not contain username"));
107        }
108        if !catalyst_id.clone().is_id() {
109            return Err(anyhow!("Catalyst ID must be in an ID format"));
110        }
111        if catalyst_id.nonce().is_none() {
112            return Err(anyhow!("Catalyst ID must have nonce"));
113        }
114
115        if REGEX.is_match(token) {
116            return Err(anyhow!(
117                "Catalyst ID mustn't have role or rotation specified"
118            ));
119        }
120        let network = convert_network(&catalyst_id.network())?;
121
122        Ok(Self {
123            catalyst_id,
124            network,
125            signature,
126            raw,
127            reg_chain: None,
128        })
129    }
130
131    /// Given the `PublicKey`, verifies the token was correctly signed.
132    pub(crate) fn verify(&self, public_key: &VerifyingKey) -> Result<()> {
133        public_key
134            .verify_strict(&self.raw, &self.signature)
135            .context("Token signature verification failed")
136    }
137
138    /// Checks that the token timestamp is valid.
139    ///
140    /// The timestamp is valid if it isn't too old or too skewed.
141    pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
142        let Some(token_age) = self.catalyst_id.nonce() else {
143            return false;
144        };
145
146        let now = Utc::now();
147
148        // The token is considered old if it was issued more than max_age ago.
149        // And newer than an allowed clock skew value
150        // This is a safety measure to avoid replay attacks.
151        let Ok(max_age) = TimeDelta::from_std(max_age) else {
152            return false;
153        };
154        let Ok(max_skew) = TimeDelta::from_std(max_skew) else {
155            return false;
156        };
157        let Some(min_time) = now.checked_sub_signed(max_age) else {
158            return false;
159        };
160        let Some(max_time) = now.checked_add_signed(max_skew) else {
161            return false;
162        };
163        (min_time < token_age) && (max_time > token_age)
164    }
165
166    /// Returns a Catalyst ID from the token.
167    pub(crate) fn catalyst_id(&self) -> &CatalystId {
168        &self.catalyst_id
169    }
170
171    /// Returns a network.
172    #[allow(dead_code)]
173    pub(crate) fn network(&self) -> Network {
174        self.network
175    }
176
177    /// Returns a corresponded registration chain if any registrations present.
178    /// If it is a first call, fetch all data from the database and initialize it.
179    pub(crate) async fn reg_chain(&mut self) -> Result<Option<RegistrationChain>> {
180        if self.reg_chain.is_none() {
181            self.reg_chain = latest_rbac_chain(&self.catalyst_id).await?.map(|i| i.chain);
182        }
183        Ok(self.reg_chain.clone())
184    }
185}
186
187impl Display for CatalystRBACTokenV1 {
188    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
189        write!(
190            f,
191            "{}{}.{}",
192            CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
193            self.catalyst_id,
194            BASE64_URL_SAFE_NO_PAD.encode(self.signature.to_bytes())
195        )
196    }
197}
198
199/// Converts the given token string to raw bytes.
200fn as_raw_bytes(token: &str) -> Vec<u8> {
201    // The signature is calculated over all bytes in the token including the final '.'.
202    CatalystRBACTokenV1::AUTH_TOKEN_PREFIX
203        .bytes()
204        .chain(token.bytes())
205        .chain(".".bytes())
206        .collect()
207}
208
209/// Checks if the given network is supported.
210fn convert_network((network, subnet): &(String, Option<String>)) -> Result<Network> {
211    if network != "cardano" {
212        return Err(anyhow!("Unsupported network: {network}"));
213    }
214
215    match subnet.as_deref() {
216        None => Ok(Network::Mainnet),
217        Some("preprod") => Ok(Network::Preprod),
218        Some("preview") => Ok(Network::Preview),
219        Some(subnet) => Err(anyhow!("Unsupported host: {subnet}.{network}",)),
220    }
221}
222
223#[cfg(test)]
224mod tests {
225
226    use ed25519_dalek::SigningKey;
227    use rand::rngs::OsRng;
228    use test_case::test_case;
229
230    use super::*;
231
232    #[test_case("cardano", None ; "mainnet cardano network")]
233    #[test_case("cardano", Some("preprod") ; "preprod.cardano network")]
234    #[test_case("cardano", Some("preview") ; "preview.cardano network")]
235    fn roundtrip(network: &'static str, subnet: Option<&'static str>) {
236        let mut seed = OsRng;
237        let signing_key: SigningKey = SigningKey::generate(&mut seed);
238        let verifying_key = signing_key.verifying_key();
239        let token = CatalystRBACTokenV1::new(network, subnet, verifying_key, &signing_key).unwrap();
240        assert_eq!(token.catalyst_id().username(), None);
241        assert!(token.catalyst_id().nonce().is_some());
242        assert_eq!(
243            token.catalyst_id().network(),
244            (network.to_string(), subnet.map(ToString::to_string))
245        );
246        assert!(!token.catalyst_id().is_encryption_key());
247        assert!(token.catalyst_id().is_signature_key());
248
249        let token_str = token.to_string();
250        let parsed = CatalystRBACTokenV1::parse(&token_str).unwrap();
251        assert_eq!(token.signature, parsed.signature);
252        assert_eq!(token.raw, parsed.raw);
253        assert_eq!(parsed.catalyst_id().username(), Some(String::new()));
254        assert!(parsed.catalyst_id().nonce().is_some());
255        assert_eq!(
256            parsed.catalyst_id().network(),
257            (network.to_string(), subnet.map(ToString::to_string))
258        );
259        assert!(!token.catalyst_id().is_encryption_key());
260        assert!(token.catalyst_id().is_signature_key());
261
262        let parsed_str = parsed.to_string();
263        assert_eq!(token_str, parsed_str);
264    }
265
266    #[test]
267    fn is_young() {
268        let mut seed = OsRng;
269        let signing_key: SigningKey = SigningKey::generate(&mut seed);
270        let verifying_key = signing_key.verifying_key();
271        let mut token =
272            CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
273                .unwrap();
274
275        // Update the token timestamp to be two seconds in the past.
276        let now = Utc::now();
277        token.catalyst_id = token
278            .catalyst_id
279            .with_specific_nonce(now - Duration::from_secs(2));
280
281        // Check that the token ISN'T young if max_age is one second.
282        let max_age = Duration::from_secs(1);
283        let max_skew = Duration::from_secs(1);
284        assert!(!token.is_young(max_age, max_skew));
285
286        // Check that the token IS young if max_age is three seconds.
287        let max_age = Duration::from_secs(3);
288        assert!(token.is_young(max_age, max_skew));
289
290        // Update the token timestamp to be two seconds in the future.
291        token.catalyst_id = token
292            .catalyst_id
293            .with_specific_nonce(now + Duration::from_secs(2));
294
295        // Check that the token IS too new if max_skew is one seconds.
296        let max_skew = Duration::from_secs(1);
297        assert!(!token.is_young(max_age, max_skew));
298
299        // Check that the token ISN'T too new if max_skew is three seconds.
300        let max_skew = Duration::from_secs(3);
301        assert!(token.is_young(max_age, max_skew));
302    }
303
304    #[test]
305    fn verify() {
306        let mut seed = OsRng;
307        let signing_key: SigningKey = SigningKey::generate(&mut seed);
308        let verifying_key = signing_key.verifying_key();
309        let token =
310            CatalystRBACTokenV1::new("cardano", Some("preprod"), verifying_key, &signing_key)
311                .unwrap();
312        token.verify(&verifying_key).unwrap();
313    }
314}