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, ®ion);
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, ®ION.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 ®ION.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 ®ION.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}