cassandra_sigv4/
lib.rs

1use cassandra_cpp_sys::{
2    cass_authenticator_set_response, cass_cluster_set_authenticator_callbacks, CassAuthenticator,
3    CassAuthenticatorCallbacks, CassCluster,
4};
5use chrono::prelude::*;
6use sha256;
7use std::{
8    env,
9    ffi::{c_char, c_void, CStr},
10    time::SystemTime,
11};
12
13const AWS_ACCESS_KEY_ID: &str = "AWS_ACCESS_KEY_ID";
14const AWS_SECRET_ACCESS_KEY: &str = "AWS_SECRET_ACCESS_KEY";
15const AWS_SESSION_TOKEN: &str = "AWS_SESSION_TOKEN";
16const AWS_DEFAULT_REGION: &str = "AWS_DEFAULT_REGION";
17const AWS_REGION: &str = "AWS_REGION";
18
19const INITIAL_RESPONSE: &str = "SigV4\0\0";
20
21unsafe extern "C" fn initial_callback(
22    auth: *mut cassandra_cpp_sys::CassAuthenticator,
23    _: *mut c_void,
24) {
25    cass_authenticator_set_response(
26        auth,
27        INITIAL_RESPONSE.as_ptr().cast(),
28        INITIAL_RESPONSE.len(),
29    );
30}
31
32fn extract_none(token: &str) -> String {
33    let params: Vec<&str> = token.split(",").collect();
34    let nonce = params
35        .iter()
36        .find(|p| p.starts_with("nonce="))
37        .unwrap()
38        .replace("nonce=", "");
39    nonce
40}
41
42fn form_canonical_request(
43    access_key: &String,
44    scope: &String,
45    t: &DateTime<Utc>,
46    nonce: &String,
47) -> String {
48    let nonce_hash: String = sha256::digest(nonce.as_bytes());
49    let mut headers = vec![
50        String::from("X-Amz-Algorithm=AWS4-HMAC-SHA256"),
51        format!(
52            "X-Amz-Credential={}%2F{}",
53            access_key,
54            url_escape::encode_component(scope)
55        ),
56        format!(
57            "X-Amz-Date={}",
58            url_escape::encode_component(&t.to_rfc3339_opts(SecondsFormat::Millis, true))
59        ),
60        String::from("X-Amz-Expires=900"),
61    ];
62    headers.sort_unstable();
63    let query_string = headers.join("&");
64
65    let cr = format!(
66        "PUT\n/authenticate\n{}\nhost:cassandra\n\nhost\n{}",
67        query_string, nonce_hash
68    );
69    cr
70}
71
72fn create_signature(
73    canonical_request: &String,
74    t: &DateTime<Utc>,
75    signing_scope: &String,
76    signing_key: &[u8],
77) -> String {
78    let digest: String = sha256::digest(canonical_request.as_bytes());
79    let s = format!(
80        "AWS4-HMAC-SHA256\n{}\n{}\n{}",
81        t.to_rfc3339_opts(SecondsFormat::Millis, true),
82        signing_scope,
83        digest
84    );
85
86    aws_sigv4::sign::calculate_signature(signing_key, s.as_bytes())
87}
88
89fn compute_scope(t: &DateTime<Utc>, region: &String) -> String {
90    format!("{}/{}/cassandra/aws4_request", t.format("%Y%m%d"), region)
91}
92
93fn derive_secret_key(
94    secret_access_key: &String,
95    t: SystemTime,
96    region: &String,
97) -> impl AsRef<[u8]> {
98    aws_sigv4::sign::generate_signing_key(
99        secret_access_key.as_str(),
100        t,
101        region.as_str(),
102        "cassandra",
103    )
104}
105
106fn build_signed_response(
107    region: &String,
108    nonce: &String,
109    access_key: &String,
110    secret: &String,
111    session_token: &Option<String>,
112    t: SystemTime,
113) -> String {
114    let c_t = DateTime::<Utc>::from(t);
115    let scope = compute_scope(&c_t, &region);
116    let canonical_request = form_canonical_request(access_key, &scope, &c_t, &nonce);
117    let sk = derive_secret_key(secret, t, region);
118
119    let signature = create_signature(&canonical_request, &c_t, &scope, sk.as_ref());
120
121    let mut result = format!(
122        "signature={},access_key={},amzdate={}",
123        signature,
124        access_key,
125        c_t.to_rfc3339_opts(SecondsFormat::Millis, true)
126    );
127    if let Some(session_token) = session_token {
128        result.push_str(",session_token=");
129        result.push_str(session_token)
130    }
131
132    result
133}
134
135unsafe extern "C" fn challenge_callback(
136    auth: *mut CassAuthenticator,
137    data: *mut c_void,
138    token: *const c_char,
139    _token_size: usize,
140) {
141    let config = data as *mut Config;
142    let access_key = &config.as_ref().unwrap().access_key;
143    let secret_access_key = &config.as_ref().unwrap().secret_access_key;
144    let session_token = &config.as_ref().unwrap().session_token;
145    let region = &config.as_ref().unwrap().region;
146
147    let token = CStr::from_ptr(token).to_str().unwrap();
148    let nonce = extract_none(token);
149
150    let now = Utc::now();
151
152    let result = build_signed_response(
153        region,
154        &nonce,
155        access_key,
156        secret_access_key,
157        session_token,
158        now.into(),
159    );
160    let result = format!("{}", result);
161    let result_c_str = result.as_bytes();
162
163    cass_authenticator_set_response(auth, result_c_str.as_ptr().cast(), result_c_str.len());
164}
165
166const CALLBACKS: CassAuthenticatorCallbacks = CassAuthenticatorCallbacks {
167    initial_callback: Some(initial_callback),
168    challenge_callback: Some(challenge_callback),
169    success_callback: None,
170    cleanup_callback: None,
171};
172
173fn sigv4_authenticators() -> *const CassAuthenticatorCallbacks {
174    &CALLBACKS
175}
176
177pub struct Authenticator {
178    access_key: String,
179    secret_access_key: String,
180    session_token: Option<String>,
181    region: String,
182}
183
184struct Config {
185    access_key: String,
186    secret_access_key: String,
187    session_token: Option<String>,
188    region: String,
189}
190
191unsafe extern "C" fn cleanup_config(data: *mut c_void) {
192    let config = Box::from_raw(data);
193    drop(config);
194}
195
196impl Authenticator {
197    pub fn new(
198        access_key: String,
199        secret_access_key: String,
200        session_token: Option<String>,
201        region: String,
202    ) -> Self {
203        return Authenticator {
204            access_key,
205            secret_access_key,
206            session_token,
207            region,
208        };
209    }
210
211    pub fn default() -> Self {
212        let access_key =
213            env::var(AWS_ACCESS_KEY_ID).expect("AWS_ACCESS_KEY_ID env variable is missing");
214        let secret_access_key =
215            env::var(AWS_SECRET_ACCESS_KEY).expect("AWS_SECRET_ACCESS_KEY env variable is missing");
216        let session_token = env::var(AWS_SESSION_TOKEN).ok();
217        let region = env::var(AWS_DEFAULT_REGION).or_else(|_| env::var(AWS_REGION)).expect("AWS_DEFAULT_REGION and AWS_REGION env variables are missing. Setup at least one of them ");
218        Authenticator {
219            access_key,
220            secret_access_key,
221            session_token,
222            region,
223        }
224    }
225
226    pub fn set_authenticator(&self, cluster: *mut CassCluster) {
227        unsafe {
228            let config = Box::new(Config {
229                access_key: self.access_key.clone(),
230                secret_access_key: self.secret_access_key.clone(),
231                session_token: self.session_token.clone(),
232                region: self.region.clone(),
233            });
234            let config_ptr = Box::into_raw(config) as *mut Config as *mut c_void;
235            cass_cluster_set_authenticator_callbacks(
236                cluster,
237                sigv4_authenticators(),
238                Some(cleanup_config),
239                config_ptr,
240            );
241        }
242    }
243}
244
245#[cfg(test)]
246mod lib_test {
247    use super::*;
248
249    const NONCE: &str = "91703fdc2ef562e19fbdab0f58e42fe5";
250    const REGION: &str = "us-west-2";
251    const ACCESS_KEY_ID: &str = "UserID-1";
252    const SECRET: &str = "UserSecretKey-1";
253
254    fn time() -> DateTime<Utc> {
255        DateTime::parse_from_rfc3339("2020-06-09T22:41:51Z")
256            .unwrap()
257            .into()
258    }
259
260    #[test]
261    fn extract_nonce_test() {
262        let challenge = "nonce=1256";
263        let actual_nonce = extract_none(challenge);
264        assert_eq!(actual_nonce, "1256");
265    }
266
267    #[test]
268    fn extract_nonce_test_multiple_params() {
269        let challenge = "param1=dfg,nonce=1256,param2=hhhh";
270        let actual_nonce = extract_none(challenge);
271        assert_eq!(actual_nonce, "1256");
272    }
273
274    #[test]
275    #[should_panic]
276    fn extract_no_nonce() {
277        let challenge = "n1256";
278        extract_none(challenge);
279    }
280
281    #[test]
282    fn compute_scope_test() {
283        let scope = compute_scope(&time(), &"us-west-2".to_string());
284        assert_eq!("20200609/us-west-2/cassandra/aws4_request", scope);
285    }
286
287    #[test]
288    fn form_canonical_request_test() {
289        let scope = String::from("20200609/us-west-2/cassandra/aws4_request");
290        let mut canonical_request = String::from("");
291        canonical_request.push_str("PUT\n");
292        canonical_request.push_str("/authenticate\n");
293        canonical_request.push_str("X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=UserID-1%2F20200609%2Fus-west-2%2Fcassandra%2Faws4_request&X-Amz-Date=2020-06-09T22%3A41%3A51.000Z&X-Amz-Expires=900\n");
294        canonical_request.push_str("host:cassandra\n\n");
295        canonical_request.push_str("host\n");
296        canonical_request
297            .push_str("ddf250111597b3f35e51e649f59e3f8b30ff5b247166d709dc1b1e60bd927070");
298
299        let actual = form_canonical_request(
300            &String::from("UserID-1"),
301            &scope,
302            &time(),
303            &NONCE.to_string(),
304        );
305        assert_eq!(canonical_request, actual);
306    }
307
308    #[test]
309    fn get_signing_key() {
310        let mock_now = SystemTime::from(time());
311        let expected =
312            hex::decode("7fb139473f153aec1b05747b0cd5cd77a1186d22ae895a3a0128e699d72e1aba")
313                .unwrap();
314        let actual = derive_secret_key(&SECRET.to_string(), mock_now, &REGION.to_string());
315        assert_eq!(expected, actual.as_ref());
316    }
317
318    #[test]
319    fn create_signature_test() {
320        let sk = hex::decode("7fb139473f153aec1b05747b0cd5cd77a1186d22ae895a3a0128e699d72e1aba")
321            .unwrap();
322        let scope = String::from("20200609/us-west-2/cassandra/aws4_request");
323
324        let mut canonical_request = String::from("");
325        canonical_request.push_str("PUT\n");
326        canonical_request.push_str("/authenticate\n");
327        canonical_request.push_str("X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=UserID-1%2F20200609%2Fus-west-2%2Fcassandra%2Faws4_request&X-Amz-Date=2020-06-09T22%3A41%3A51.000Z&X-Amz-Expires=900\n");
328        canonical_request.push_str("host:cassandra\n\n");
329        canonical_request.push_str("host\n");
330        canonical_request
331            .push_str("ddf250111597b3f35e51e649f59e3f8b30ff5b247166d709dc1b1e60bd927070");
332
333        let actual = create_signature(&canonical_request, &time(), &scope, &sk);
334        let expected = "7f3691c18a81b8ce7457699effbfae5b09b4e0714ab38c1292dbdf082c9ddd87";
335
336        assert_eq!(expected, actual);
337    }
338
339    #[test]
340    fn build_signed_response_test() {
341        let actual = build_signed_response(
342            &REGION.to_string(),
343            &NONCE.to_string(),
344            &ACCESS_KEY_ID.to_string(),
345            &SECRET.to_string(),
346            &None,
347            time().into(),
348        );
349        let expected = "signature=7f3691c18a81b8ce7457699effbfae5b09b4e0714ab38c1292dbdf082c9ddd87,access_key=UserID-1,amzdate=2020-06-09T22:41:51.000Z";
350        assert_eq!(expected, actual);
351    }
352
353    #[test]
354    fn build_signed_response_session_test() {
355        let actual = build_signed_response(
356            &REGION.to_string(),
357            &NONCE.to_string(),
358            &ACCESS_KEY_ID.to_string(),
359            &SECRET.to_string(),
360            &Some("sess-token-1".to_string()),
361            time().into(),
362        );
363        let expected = "signature=7f3691c18a81b8ce7457699effbfae5b09b4e0714ab38c1292dbdf082c9ddd87,access_key=UserID-1,amzdate=2020-06-09T22:41:51.000Z,session_token=sess-token-1";
364        assert_eq!(expected, actual);
365    }
366}