1#![allow(unused_assignments)] use std::{
21 collections::HashMap,
22 fmt::{Debug, Formatter},
23};
24
25use aws_lc_rs::hmac;
26use hex;
27use nautilus_core::{UUID4, time::get_atomic_clock_realtime};
28use ustr::Ustr;
29use zeroize::ZeroizeOnDrop;
30
31use crate::http::error::DeribitHttpError;
32
33#[derive(Clone, ZeroizeOnDrop)]
38pub struct Credential {
39 #[zeroize(skip)]
40 pub api_key: Ustr,
41 api_secret: Box<[u8]>,
42}
43
44impl Debug for Credential {
45 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct(stringify!(Credential))
47 .field("api_key", &self.api_key)
48 .field("api_secret", &"<redacted>")
49 .finish()
50 }
51}
52
53impl Credential {
54 #[must_use]
56 pub fn new(api_key: String, api_secret: String) -> Self {
57 Self {
58 api_key: api_key.into(),
59 api_secret: api_secret.into_bytes().into_boxed_slice(),
60 }
61 }
62
63 #[must_use]
65 pub fn api_key(&self) -> &Ustr {
66 &self.api_key
67 }
68
69 #[must_use]
74 pub fn api_key_masked(&self) -> String {
75 nautilus_core::string::mask_api_key(self.api_key.as_str())
76 }
77
78 #[must_use]
91 pub fn sign_ws_auth(&self, timestamp: u64, nonce: &str, data: &str) -> String {
92 let string_to_sign = format!("{timestamp}\n{nonce}\n{data}");
94
95 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
97 let tag = hmac::sign(&key, string_to_sign.as_bytes());
98
99 hex::encode(tag.as_ref())
101 }
102
103 #[must_use]
123 fn sign_message(&self, timestamp: i64, nonce: &str, request_data: &str) -> String {
124 let string_to_sign = format!("{timestamp}\n{nonce}\n{request_data}");
126
127 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
129 let tag = hmac::sign(&key, string_to_sign.as_bytes());
130
131 hex::encode(tag.as_ref())
133 }
134
135 pub fn sign_auth_headers(
150 &self,
151 method: &str,
152 uri: &str,
153 body: &[u8],
154 ) -> Result<HashMap<String, String>, DeribitHttpError> {
155 let timestamp = get_atomic_clock_realtime().get_time_ms() as i64;
157
158 let nonce_uuid = UUID4::new();
160 let nonce = nonce_uuid.as_str();
161
162 let request_data = format!(
164 "{}\n{}\n{}\n",
165 method.to_uppercase(),
166 uri,
167 String::from_utf8_lossy(body)
168 );
169
170 let signature = self.sign_message(timestamp, nonce, &request_data);
172
173 let auth_header = format!(
175 "deri-hmac-sha256 id={},ts={},nonce={},sig={}",
176 self.api_key(),
177 timestamp,
178 nonce,
179 signature
180 );
181
182 let mut headers = HashMap::new();
183 headers.insert("Authorization".to_string(), auth_header);
184
185 Ok(headers)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use rstest::rstest;
192
193 use super::*;
194
195 #[rstest]
196 #[case("test_api_key", "test_api_secret")]
197 #[case("my_key", "my_secret")]
198 fn test_credential_creation(#[case] api_key: &str, #[case] api_secret: &str) {
199 let credential = Credential::new(api_key.to_string(), api_secret.to_string());
200
201 assert_eq!(credential.api_key().as_str(), api_key);
202 }
203
204 #[rstest]
205 fn test_signature_generation() {
206 let credential = Credential::new(
207 "test_client_id".to_string(),
208 "test_client_secret".to_string(),
209 );
210
211 let timestamp = 1609459200000i64;
212 let nonce = "550e8400-e29b-41d4-a716-446655440000";
213 let request_data = "POST\n/api/v2\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"private/get_account_summaries\",\"params\":{}}\n";
214
215 let signature = credential.sign_message(timestamp, nonce, request_data);
216
217 assert!(
219 signature.chars().all(|c| c.is_ascii_hexdigit()),
220 "Signature should be hex-encoded"
221 );
222
223 assert_eq!(
225 signature.len(),
226 64,
227 "HMAC-SHA256 should produce 64 hex characters"
228 );
229
230 let signature2 = credential.sign_message(timestamp, nonce, request_data);
232 assert_eq!(signature, signature2, "Signature should be deterministic");
233 }
234
235 #[rstest]
236 #[case(1000, 2000)]
237 #[case(1000, 5000)]
238 fn test_signature_changes_with_timestamp(#[case] ts1: i64, #[case] ts2: i64) {
239 let credential = Credential::new("key".to_string(), "secret".to_string());
240 let nonce = "nonce";
241 let request_data = "POST\n/api/v2\n{}\n";
242
243 let sig1 = credential.sign_message(ts1, nonce, request_data);
244 let sig2 = credential.sign_message(ts2, nonce, request_data);
245
246 assert_ne!(sig1, sig2, "Signature should change with timestamp");
247 }
248
249 #[rstest]
250 #[case("nonce1", "nonce2")]
251 #[case("abc", "xyz")]
252 fn test_signature_changes_with_nonce(#[case] nonce1: &str, #[case] nonce2: &str) {
253 let credential = Credential::new("key".to_string(), "secret".to_string());
254 let timestamp = 1000;
255 let request_data = "POST\n/api/v2\n{}\n";
256
257 let sig1 = credential.sign_message(timestamp, nonce1, request_data);
258 let sig2 = credential.sign_message(timestamp, nonce2, request_data);
259
260 assert_ne!(sig1, sig2, "Signature should change with nonce");
261 }
262
263 #[rstest]
264 #[case("POST\n/api/v2\n{\"a\":1}\n", "POST\n/api/v2\n{\"b\":2}\n")]
265 #[case("GET\n/test\n\n", "POST\n/test\n\n")]
266 fn test_signature_changes_with_request_data(#[case] data1: &str, #[case] data2: &str) {
267 let credential = Credential::new("key".to_string(), "secret".to_string());
268 let timestamp = 1000;
269 let nonce = "nonce";
270
271 let sig1 = credential.sign_message(timestamp, nonce, data1);
272 let sig2 = credential.sign_message(timestamp, nonce, data2);
273
274 assert_ne!(sig1, sig2, "Signature should change with request data");
275 }
276
277 #[rstest]
278 fn test_debug_redacts_secret() {
279 let credential = Credential::new("my_api_key".to_string(), "super_secret".to_string());
280
281 let debug_output = format!("{credential:?}");
282
283 assert!(
284 debug_output.contains("<redacted>"),
285 "Debug output should redact secret"
286 );
287 assert!(
288 !debug_output.contains("super_secret"),
289 "Debug output should not contain raw secret"
290 );
291 assert!(
292 debug_output.contains("my_api_key"),
293 "Debug output should contain API key"
294 );
295 }
296
297 #[rstest]
298 #[case("short")]
299 #[case("xyz")]
300 fn test_api_key_masked_short_key(#[case] key: &str) {
301 let credential = Credential::new(key.to_string(), "secret".to_string());
302 let masked = credential.api_key_masked();
303
304 assert_ne!(masked, key, "Short key should be masked");
306 }
307
308 #[rstest]
309 #[case("abcdefgh-1234-5678-ijkl", "abcd", "ijkl")]
310 #[case("very-long-api-key-12345", "very", "2345")]
311 fn test_api_key_masked_long_key(#[case] key: &str, #[case] start: &str, #[case] end: &str) {
312 let credential = Credential::new(key.to_string(), "secret".to_string());
313 let masked = credential.api_key_masked();
314
315 assert!(
317 masked.starts_with(start),
318 "Masked key should start with first 4 chars"
319 );
320 assert!(
321 masked.ends_with(end),
322 "Masked key should end with last 4 chars"
323 );
324 assert!(masked.contains("..."), "Masked key should contain ellipsis");
325 }
326
327 #[rstest]
328 #[case("POST", "/api/v2", b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"private/get_account_summaries\",\"params\":{}}")]
329 #[case("GET", "/api/v2/public/test", b"")]
330 #[case(
331 "POST",
332 "/api/v2/private/buy",
333 b"{\"instrument_name\":\"BTC-PERPETUAL\",\"amount\":100}"
334 )]
335 fn test_sign_auth_headers(#[case] method: &str, #[case] uri: &str, #[case] body: &[u8]) {
336 let credential = Credential::new(
337 "test_client_id".to_string(),
338 "test_client_secret".to_string(),
339 );
340
341 let result = credential.sign_auth_headers(method, uri, body);
342
343 assert!(result.is_ok(), "Should successfully sign auth headers");
344
345 let headers = result.unwrap();
346
347 assert!(
349 headers.contains_key("Authorization"),
350 "Should contain Authorization header"
351 );
352
353 let auth_header = headers.get("Authorization").unwrap();
354
355 assert!(
357 auth_header.starts_with("deri-hmac-sha256 "),
358 "Authorization header should start with 'deri-hmac-sha256 '"
359 );
360
361 assert!(
363 auth_header.contains("id=test_client_id"),
364 "Should contain client ID"
365 );
366 assert!(auth_header.contains("ts="), "Should contain timestamp");
367 assert!(auth_header.contains("nonce="), "Should contain nonce");
368 assert!(auth_header.contains("sig="), "Should contain signature");
369
370 let sig_part = auth_header.split("sig=").nth(1).unwrap();
372 assert_eq!(
373 sig_part.len(),
374 64,
375 "Signature should be 64 hex characters (HMAC-SHA256)"
376 );
377 assert!(
378 sig_part.chars().all(|c| c.is_ascii_hexdigit()),
379 "Signature should be hex-encoded"
380 );
381 }
382
383 #[rstest]
384 fn test_sign_auth_headers_changes_each_call() {
385 let credential = Credential::new("key".to_string(), "secret".to_string());
386
387 let method = "POST";
388 let uri = "/api/v2";
389 let body = b"{}";
390
391 let headers1 = credential.sign_auth_headers(method, uri, body).unwrap();
392 std::thread::sleep(std::time::Duration::from_millis(10));
394 let headers2 = credential.sign_auth_headers(method, uri, body).unwrap();
395
396 let auth1 = headers1.get("Authorization").unwrap();
397 let auth2 = headers2.get("Authorization").unwrap();
398
399 assert_ne!(
401 auth1, auth2,
402 "Authorization headers should differ between calls due to timestamp/nonce"
403 );
404 }
405
406 #[rstest]
407 fn test_sign_ws_auth_basic() {
408 let credential = Credential::new(
409 "test_client_id".to_string(),
410 "test_client_secret".to_string(),
411 );
412
413 let timestamp = 1576074319000u64;
414 let nonce = "1iqt2wls";
415 let data = "";
416
417 let signature = credential.sign_ws_auth(timestamp, nonce, data);
418
419 assert!(
420 signature.chars().all(|c| c.is_ascii_hexdigit()),
421 "Signature should be hex-encoded"
422 );
423 assert_eq!(
424 signature.len(),
425 64,
426 "HMAC-SHA256 should produce 64 hex characters"
427 );
428 let signature2 = credential.sign_ws_auth(timestamp, nonce, data);
429 assert_eq!(signature, signature2, "Signature should be deterministic");
430 }
431
432 #[rstest]
433 fn test_sign_ws_auth_with_known_values() {
434 let credential = Credential::new("AMANDA".to_string(), "AMANDASECRECT".to_string());
438
439 let timestamp = 1576074319000u64;
440 let nonce = "1iqt2wls";
441 let data = "";
442
443 let signature = credential.sign_ws_auth(timestamp, nonce, data);
444
445 assert_eq!(
446 signature, "56590594f97921b09b18f166befe0d1319b198bbcdad7ca73382de2f88fe9aa1",
447 "Signature should match Deribit documentation example"
448 );
449 }
450
451 #[rstest]
452 #[case(1000, 2000)]
453 #[case(1576074319000, 1576074320000)]
454 fn test_sign_ws_auth_changes_with_timestamp(#[case] ts1: u64, #[case] ts2: u64) {
455 let credential = Credential::new("key".to_string(), "secret".to_string());
456 let nonce = "nonce";
457 let data = "";
458
459 let sig1 = credential.sign_ws_auth(ts1, nonce, data);
460 let sig2 = credential.sign_ws_auth(ts2, nonce, data);
461
462 assert_ne!(sig1, sig2, "Signature should change with timestamp");
463 }
464
465 #[rstest]
466 #[case("nonce1", "nonce2")]
467 #[case("abc123", "xyz789")]
468 fn test_sign_ws_auth_changes_with_nonce(#[case] nonce1: &str, #[case] nonce2: &str) {
469 let credential = Credential::new("key".to_string(), "secret".to_string());
470 let timestamp = 1576074319000u64;
471 let data = "";
472
473 let sig1 = credential.sign_ws_auth(timestamp, nonce1, data);
474 let sig2 = credential.sign_ws_auth(timestamp, nonce2, data);
475
476 assert_ne!(sig1, sig2, "Signature should change with nonce");
477 }
478}