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