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