cat_gateway/service/common/auth/rbac/
token.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! Catalyst RBAC Token utility functions.
use std::{
    fmt::{Display, Formatter},
    time::{Duration, SystemTime},
};

use anyhow::{bail, Ok};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use pallas::codec::minicbor;
use tracing::error;
use ulid::Ulid;

use crate::utils::blake2b_hash::blake2b_128;

/// Key ID - Blake2b-128 hash of the Role 0 Certificate defining the Session public key.
/// BLAKE2b-128 produces digest side of 16 bytes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Kid(pub [u8; 16]);

impl From<&VerifyingKey> for Kid {
    fn from(vk: &VerifyingKey) -> Self {
        Self(blake2b_128(vk.as_bytes()))
    }
}

impl PartialEq<VerifyingKey> for Kid {
    fn eq(&self, other: &VerifyingKey) -> bool {
        self == &Kid::from(other)
    }
}

/// Identifier for this token, encodes both the time the token was issued and a random
/// nonce.
#[derive(Debug, Clone, Copy)]
struct UlidBytes(pub [u8; 16]);

/// Ed25519 signatures are (64 bytes)
#[derive(Debug, Clone)]
pub struct SignatureEd25519(pub [u8; 64]);

/// A Catalyst RBAC Authorization Token.
#[derive(Debug, Clone)]
pub(crate) struct CatalystRBACTokenV1 {
    /// Token Key Identifier
    pub(crate) kid: Kid,
    /// Tokens ULID (Time and Random Nonce)
    pub(crate) ulid: Ulid,
    /// Ed25519 Signature of the Token
    pub(crate) sig: SignatureEd25519,
    /// Raw bytes of the token
    raw: Vec<u8>,
}

impl CatalystRBACTokenV1 {
    /// Bearer Token prefix for this token.
    const AUTH_TOKEN_PREFIX: &str = "catv1";
    /// The message is a Cbor sequence (cbor(kid) + cbor(ulid)):
    /// kid + ulid are 16 bytes a piece, with 1 byte extra due to cbor encoding,
    /// The two fields include their encoding resulting in 17 bytes each.
    const KID_ULID_CBOR_ENCODED_BYTES: u8 = 34;

    /// The Encoded Binary Auth Token is a [CBOR sequence] that consists of 3 fields [
    /// kid, ulid, signature ]. ED25519 Signature over the preceding two fields -
    /// sig(cbor(kid), cbor(ulid))
    #[allow(dead_code, clippy::expect_used)]
    pub(crate) fn new(sk: &SigningKey) -> Self {
        // Calculate the `kid` from the PublicKey.
        let vk: ed25519_dalek::VerifyingKey = sk.verifying_key();

        // Generate the Kid from the Signing Verify Key
        let kid = Kid::from(&vk);

        // Create a enw ulid for this token.
        let ulid = Ulid::new();

        let out: Vec<u8> = Vec::new();
        let mut encoder = minicbor::Encoder::new(out);

        // It is safe to use expect here, because the calls are infallible
        encoder.bytes(&kid.0).expect("This should never fail.");
        encoder
            .bytes(&ulid.to_bytes())
            .expect("This should never fail");

        let sig = SignatureEd25519(sk.sign(encoder.writer()).to_bytes());

        encoder.bytes(&sig.0).expect("This should never fail");

        Self {
            kid,
            ulid,
            sig,
            raw: encoder.writer().clone(),
        }
    }

    /// Decode base64 cbor encoded auth token into constituent parts of (kid, ulid,
    /// signature)
    /// e.g catv1.UAARIjNEVWZ3iJmqu8zd7v9QAZEs7HHPLEwUpV1VhdlNe1hAAAAAAAAAAAAA...
    pub(crate) fn decode(auth_token: &str) -> anyhow::Result<Self> {
        let token = auth_token.split('.').collect::<Vec<&str>>();

        let prefix = token.first().ok_or(anyhow::anyhow!("No valid prefix"))?;
        if *prefix != Self::AUTH_TOKEN_PREFIX {
            return Err(anyhow::anyhow!("Corrupt token, invalid prefix"));
        }
        let token_base64 = token.get(1).ok_or(anyhow::anyhow!("No valid token"))?;
        let token_cbor_encoded = BASE64_URL_SAFE_NO_PAD.decode(token_base64)?;

        // Decode cbor to bytes
        let mut cbor_decoder = minicbor::Decoder::new(&token_cbor_encoded);

        // Raw kid bytes
        // TODO: Check if the KID is not the right length it gets an error.
        let kid = Kid(cbor_decoder
            .bytes()
            .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for kid : {e}")))?
            .try_into()?);

        // TODO: Check what happens if the ULID is NOT 28 bytes long
        let ulid_raw: UlidBytes = UlidBytes(
            cbor_decoder
                .bytes()
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for ulid : {e}")))?
                .try_into()?,
        );
        let ulid = Ulid::from_bytes(ulid_raw.0);

        // Raw signature
        let signature = SignatureEd25519(
            cbor_decoder
                .bytes()
                .map_err(|e| anyhow::anyhow!(format!("Invalid cbor for signature : {e}")))?
                .try_into()?,
        );

        Ok(CatalystRBACTokenV1 {
            kid,
            ulid,
            sig: signature,
            raw: token_cbor_encoded,
        })
    }

    /// Given the `PublicKey`, verify the token was correctly signed.
    pub(crate) fn verify(&self, public_key: &VerifyingKey) -> anyhow::Result<()> {
        // Verify the Kid of the Token matches the PublicKey.
        if self.kid != *public_key {
            error!(token=%self, public_key=?public_key,
                "Tokens Kid did not match verifying Public Key",
            );
            bail!("Kid does not match PublicKey.")
        }

        // We verify the signature on the message which corresponds to a Cbor sequence (cbor(kid)
        // + cbor(ulid)):
        let message_cbor_encoded = self
            .raw
            .get(0..Self::KID_ULID_CBOR_ENCODED_BYTES.into())
            .ok_or(anyhow::anyhow!("No valid token"))?;

        if let Err(error) =
            public_key.verify_strict(message_cbor_encoded, &Signature::from_bytes(&self.sig.0))
        {
            error!(error=%error, token=%self, public_key=?public_key,
                "Token was not signed by the expected Public Key",
            );
            bail!("Token Not Validated");
        }

        Ok(())
    }

    /// Check if the token is young enough.
    /// Old tokens are no longer valid.
    pub(crate) fn is_young(&self, max_age: Duration, max_skew: Duration) -> bool {
        // We check that the token is not too old or too skewed.
        let now = SystemTime::now();
        let token_age = self.ulid.datetime();

        // The token is considered old if it was issued more than max_age ago.
        // And newer than an allowed clock skew value
        // This is a safety measure to avoid replay attacks.
        let Some(min_time) = now.checked_sub(max_age) else {
            return false;
        };
        let Some(max_time) = now.checked_add(max_skew) else {
            return false;
        };
        (min_time < token_age) && (max_time > token_age)
    }
}

impl Display for CatalystRBACTokenV1 {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}.{}",
            CatalystRBACTokenV1::AUTH_TOKEN_PREFIX,
            BASE64_URL_SAFE_NO_PAD.encode(self.raw.clone())
        )
    }
}

#[cfg(test)]
mod tests {

    use ed25519_dalek::SigningKey;
    use rand::rngs::OsRng;

    use super::*;

    #[test]
    fn test_token_generation_and_decoding() {
        let mut random_seed = OsRng;
        let signing_key: SigningKey = SigningKey::generate(&mut random_seed);
        let verifying_key = signing_key.verifying_key();

        let signing_key2: SigningKey = SigningKey::generate(&mut random_seed);
        let verifying_key2 = signing_key2.verifying_key();

        // Generate a Kid and then check it verifies properly against itself.
        // And doesn't against a different verifying key.
        let kid = Kid::from(&verifying_key);
        assert!(kid == verifying_key);
        assert!(kid != verifying_key2);

        // Create a new Catalyst V1 Token
        let token = CatalystRBACTokenV1::new(&signing_key);
        // Check its signed properly against its own key, and not another.
        assert!(token.verify(&verifying_key).is_ok());
        assert!(token.verify(&verifying_key2).is_err());

        let decoded_token = format!("{token}");

        let re_encoded_token = CatalystRBACTokenV1::decode(&decoded_token)
            .expect("Failed to decode a token we encoded.");

        // Check its still signed properly against its own key, and not another.
        assert!(re_encoded_token.verify(&verifying_key).is_ok());
        assert!(re_encoded_token.verify(&verifying_key2).is_err());
    }

    #[test]
    fn is_young() {
        let mut random_seed = OsRng;
        let key = SigningKey::generate(&mut random_seed);
        let mut token = CatalystRBACTokenV1::new(&key);

        // Update the token timestamp to be two seconds in the past.
        let now = SystemTime::now();
        token.ulid = Ulid::from_datetime(now - Duration::from_secs(2));

        // Check that the token ISN'T young if max_age is one second.
        let max_age = Duration::from_secs(1);
        let max_skew = Duration::from_secs(1);
        assert!(!token.is_young(max_age, max_skew));

        // Check that the token IS young if max_age is three seconds.
        let max_age = Duration::from_secs(3);
        assert!(token.is_young(max_age, max_skew));

        // Update the token timestamp to be two seconds in the future.
        token.ulid = Ulid::from_datetime(now + Duration::from_secs(2));

        // Check that the token IS too new if max_skew is one seconds.
        let max_skew = Duration::from_secs(1);
        assert!(!token.is_young(max_age, max_skew));

        // Check that the token ISN'T too new if max_skew is three seconds.
        let max_skew = Duration::from_secs(3);
        assert!(token.is_young(max_age, max_skew));
    }
}