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