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 thiserror::Error;
26use ustr::Ustr;
27use zeroize::ZeroizeOnDrop;
28
29use crate::http::error::DeribitHttpError;
30
31#[derive(Debug, Error)]
33pub enum CredentialError {
34 #[error("API key provided but secret is missing")]
36 MissingSecret,
37 #[error("API secret provided but key is missing")]
39 MissingKey,
40}
41
42#[derive(Clone, ZeroizeOnDrop)]
47pub struct Credential {
48 #[zeroize(skip)]
49 pub api_key: Ustr,
50 api_secret: Box<[u8]>,
51}
52
53impl Debug for Credential {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct(stringify!(Credential))
56 .field("api_key", &self.api_key)
57 .field("api_secret", &"<redacted>")
58 .finish()
59 }
60}
61
62impl Credential {
63 #[must_use]
65 pub fn new(api_key: String, api_secret: String) -> Self {
66 Self {
67 api_key: api_key.into(),
68 api_secret: api_secret.into_bytes().into_boxed_slice(),
69 }
70 }
71
72 #[must_use]
79 pub fn from_env(is_testnet: bool) -> Option<Self> {
80 let (key_var, secret_var) = if is_testnet {
81 ("DERIBIT_TESTNET_API_KEY", "DERIBIT_TESTNET_API_SECRET")
82 } else {
83 ("DERIBIT_API_KEY", "DERIBIT_API_SECRET")
84 };
85
86 let key = std::env::var(key_var).ok()?;
87 let secret = std::env::var(secret_var).ok()?;
88
89 Some(Self::new(key, secret))
90 }
91
92 pub fn resolve(
101 api_key: Option<String>,
102 api_secret: Option<String>,
103 is_testnet: bool,
104 ) -> Result<Option<Self>, CredentialError> {
105 Self::resolve_with_env_fallback(api_key, api_secret, is_testnet, true)
106 }
107
108 pub fn resolve_with_env_fallback(
119 api_key: Option<String>,
120 api_secret: Option<String>,
121 is_testnet: bool,
122 env_fallback: bool,
123 ) -> Result<Option<Self>, CredentialError> {
124 match (api_key, api_secret) {
125 (Some(k), Some(s)) => Ok(Some(Self::new(k, s))),
126 (None, None) if env_fallback => Ok(Self::from_env(is_testnet)),
127 (None, None) => Ok(None),
128 (Some(_), None) => Err(CredentialError::MissingSecret),
129 (None, Some(_)) => Err(CredentialError::MissingKey),
130 }
131 }
132
133 #[must_use]
135 pub fn api_key(&self) -> &Ustr {
136 &self.api_key
137 }
138
139 #[must_use]
144 pub fn api_key_masked(&self) -> String {
145 nautilus_core::string::mask_api_key(self.api_key.as_str())
146 }
147
148 #[must_use]
161 pub fn sign_ws_auth(&self, timestamp: u64, nonce: &str, data: &str) -> String {
162 let string_to_sign = format!("{timestamp}\n{nonce}\n{data}");
164
165 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
167 let tag = hmac::sign(&key, string_to_sign.as_bytes());
168
169 hex::encode(tag.as_ref())
171 }
172
173 #[must_use]
193 fn sign_message(&self, timestamp: i64, nonce: &str, request_data: &str) -> String {
194 let string_to_sign = format!("{timestamp}\n{nonce}\n{request_data}");
196
197 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
199 let tag = hmac::sign(&key, string_to_sign.as_bytes());
200
201 hex::encode(tag.as_ref())
203 }
204
205 pub fn sign_auth_headers(
220 &self,
221 method: &str,
222 uri: &str,
223 body: &[u8],
224 ) -> Result<HashMap<String, String>, DeribitHttpError> {
225 let timestamp = get_atomic_clock_realtime().get_time_ms() as i64;
227
228 let nonce_uuid = UUID4::new();
230 let nonce = nonce_uuid.as_str();
231
232 let request_data = format!(
234 "{}\n{}\n{}\n",
235 method.to_uppercase(),
236 uri,
237 String::from_utf8_lossy(body)
238 );
239
240 let signature = self.sign_message(timestamp, nonce, &request_data);
242
243 let auth_header = format!(
245 "deri-hmac-sha256 id={},ts={},nonce={},sig={}",
246 self.api_key(),
247 timestamp,
248 nonce,
249 signature
250 );
251
252 let mut headers = HashMap::new();
253 headers.insert("Authorization".to_string(), auth_header);
254
255 Ok(headers)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use std::time::Duration;
262
263 use rstest::rstest;
264
265 use super::*;
266
267 #[rstest]
268 #[case("test_api_key", "test_api_secret")]
269 #[case("my_key", "my_secret")]
270 fn test_credential_creation(#[case] api_key: &str, #[case] api_secret: &str) {
271 let credential = Credential::new(api_key.to_string(), api_secret.to_string());
272
273 assert_eq!(credential.api_key().as_str(), api_key);
274 }
275
276 #[rstest]
277 fn test_signature_generation() {
278 let credential = Credential::new(
279 "test_client_id".to_string(),
280 "test_client_secret".to_string(),
281 );
282
283 let timestamp = 1609459200000i64;
284 let nonce = "550e8400-e29b-41d4-a716-446655440000";
285 let request_data = "POST\n/api/v2\n{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"private/get_account_summaries\",\"params\":{}}\n";
286
287 let signature = credential.sign_message(timestamp, nonce, request_data);
288
289 assert!(
291 signature.chars().all(|c| c.is_ascii_hexdigit()),
292 "Signature should be hex-encoded"
293 );
294
295 assert_eq!(
297 signature.len(),
298 64,
299 "HMAC-SHA256 should produce 64 hex characters"
300 );
301
302 let signature2 = credential.sign_message(timestamp, nonce, request_data);
304 assert_eq!(signature, signature2, "Signature should be deterministic");
305 }
306
307 #[rstest]
308 #[case(1000, 2000)]
309 #[case(1000, 5000)]
310 fn test_signature_changes_with_timestamp(#[case] ts1: i64, #[case] ts2: i64) {
311 let credential = Credential::new("key".to_string(), "secret".to_string());
312 let nonce = "nonce";
313 let request_data = "POST\n/api/v2\n{}\n";
314
315 let sig1 = credential.sign_message(ts1, nonce, request_data);
316 let sig2 = credential.sign_message(ts2, nonce, request_data);
317
318 assert_ne!(sig1, sig2, "Signature should change with timestamp");
319 }
320
321 #[rstest]
322 #[case("nonce1", "nonce2")]
323 #[case("abc", "xyz")]
324 fn test_signature_changes_with_nonce(#[case] nonce1: &str, #[case] nonce2: &str) {
325 let credential = Credential::new("key".to_string(), "secret".to_string());
326 let timestamp = 1000;
327 let request_data = "POST\n/api/v2\n{}\n";
328
329 let sig1 = credential.sign_message(timestamp, nonce1, request_data);
330 let sig2 = credential.sign_message(timestamp, nonce2, request_data);
331
332 assert_ne!(sig1, sig2, "Signature should change with nonce");
333 }
334
335 #[rstest]
336 #[case("POST\n/api/v2\n{\"a\":1}\n", "POST\n/api/v2\n{\"b\":2}\n")]
337 #[case("GET\n/test\n\n", "POST\n/test\n\n")]
338 fn test_signature_changes_with_request_data(#[case] data1: &str, #[case] data2: &str) {
339 let credential = Credential::new("key".to_string(), "secret".to_string());
340 let timestamp = 1000;
341 let nonce = "nonce";
342
343 let sig1 = credential.sign_message(timestamp, nonce, data1);
344 let sig2 = credential.sign_message(timestamp, nonce, data2);
345
346 assert_ne!(sig1, sig2, "Signature should change with request data");
347 }
348
349 #[rstest]
350 fn test_debug_redacts_secret() {
351 let credential = Credential::new("my_api_key".to_string(), "super_secret".to_string());
352
353 let debug_output = format!("{credential:?}");
354
355 assert!(
356 debug_output.contains("<redacted>"),
357 "Debug output should redact secret"
358 );
359 assert!(
360 !debug_output.contains("super_secret"),
361 "Debug output should not contain raw secret"
362 );
363 assert!(
364 debug_output.contains("my_api_key"),
365 "Debug output should contain API key"
366 );
367 }
368
369 #[rstest]
370 #[case("short")]
371 #[case("xyz")]
372 fn test_api_key_masked_short_key(#[case] key: &str) {
373 let credential = Credential::new(key.to_string(), "secret".to_string());
374 let masked = credential.api_key_masked();
375
376 assert_ne!(masked, key, "Short key should be masked");
378 }
379
380 #[rstest]
381 #[case("abcdefgh-1234-5678-ijkl", "abcd", "ijkl")]
382 #[case("very-long-api-key-12345", "very", "2345")]
383 fn test_api_key_masked_long_key(#[case] key: &str, #[case] start: &str, #[case] end: &str) {
384 let credential = Credential::new(key.to_string(), "secret".to_string());
385 let masked = credential.api_key_masked();
386
387 assert!(
389 masked.starts_with(start),
390 "Masked key should start with first 4 chars"
391 );
392 assert!(
393 masked.ends_with(end),
394 "Masked key should end with last 4 chars"
395 );
396 assert!(masked.contains("..."), "Masked key should contain ellipsis");
397 }
398
399 #[rstest]
400 #[case("POST", "/api/v2", b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"private/get_account_summaries\",\"params\":{}}")]
401 #[case("GET", "/api/v2/public/test", b"")]
402 #[case(
403 "POST",
404 "/api/v2/private/buy",
405 b"{\"instrument_name\":\"BTC-PERPETUAL\",\"amount\":100}"
406 )]
407 fn test_sign_auth_headers(#[case] method: &str, #[case] uri: &str, #[case] body: &[u8]) {
408 let credential = Credential::new(
409 "test_client_id".to_string(),
410 "test_client_secret".to_string(),
411 );
412
413 let result = credential.sign_auth_headers(method, uri, body);
414
415 assert!(result.is_ok(), "Should successfully sign auth headers");
416
417 let headers = result.unwrap();
418
419 assert!(
421 headers.contains_key("Authorization"),
422 "Should contain Authorization header"
423 );
424
425 let auth_header = headers.get("Authorization").unwrap();
426
427 assert!(
429 auth_header.starts_with("deri-hmac-sha256 "),
430 "Authorization header should start with 'deri-hmac-sha256 '"
431 );
432
433 assert!(
435 auth_header.contains("id=test_client_id"),
436 "Should contain client ID"
437 );
438 assert!(auth_header.contains("ts="), "Should contain timestamp");
439 assert!(auth_header.contains("nonce="), "Should contain nonce");
440 assert!(auth_header.contains("sig="), "Should contain signature");
441
442 let sig_part = auth_header.split("sig=").nth(1).unwrap();
444 assert_eq!(
445 sig_part.len(),
446 64,
447 "Signature should be 64 hex characters (HMAC-SHA256)"
448 );
449 assert!(
450 sig_part.chars().all(|c| c.is_ascii_hexdigit()),
451 "Signature should be hex-encoded"
452 );
453 }
454
455 #[rstest]
456 fn test_sign_auth_headers_changes_each_call() {
457 let credential = Credential::new("key".to_string(), "secret".to_string());
458
459 let method = "POST";
460 let uri = "/api/v2";
461 let body = b"{}";
462
463 let headers1 = credential.sign_auth_headers(method, uri, body).unwrap();
464 std::thread::sleep(Duration::from_millis(10));
466 let headers2 = credential.sign_auth_headers(method, uri, body).unwrap();
467
468 let auth1 = headers1.get("Authorization").unwrap();
469 let auth2 = headers2.get("Authorization").unwrap();
470
471 assert_ne!(
473 auth1, auth2,
474 "Authorization headers should differ between calls due to timestamp/nonce"
475 );
476 }
477
478 #[rstest]
479 fn test_sign_ws_auth_basic() {
480 let credential = Credential::new(
481 "test_client_id".to_string(),
482 "test_client_secret".to_string(),
483 );
484
485 let timestamp = 1576074319000u64;
486 let nonce = "1iqt2wls";
487 let data = "";
488
489 let signature = credential.sign_ws_auth(timestamp, nonce, data);
490
491 assert!(
492 signature.chars().all(|c| c.is_ascii_hexdigit()),
493 "Signature should be hex-encoded"
494 );
495 assert_eq!(
496 signature.len(),
497 64,
498 "HMAC-SHA256 should produce 64 hex characters"
499 );
500 let signature2 = credential.sign_ws_auth(timestamp, nonce, data);
501 assert_eq!(signature, signature2, "Signature should be deterministic");
502 }
503
504 #[rstest]
505 fn test_sign_ws_auth_with_known_values() {
506 let credential = Credential::new("AMANDA".to_string(), "AMANDASECRECT".to_string());
510
511 let timestamp = 1576074319000u64;
512 let nonce = "1iqt2wls";
513 let data = "";
514
515 let signature = credential.sign_ws_auth(timestamp, nonce, data);
516
517 assert_eq!(
518 signature, "56590594f97921b09b18f166befe0d1319b198bbcdad7ca73382de2f88fe9aa1",
519 "Signature should match Deribit documentation example"
520 );
521 }
522
523 #[rstest]
524 #[case(1000, 2000)]
525 #[case(1576074319000, 1576074320000)]
526 fn test_sign_ws_auth_changes_with_timestamp(#[case] ts1: u64, #[case] ts2: u64) {
527 let credential = Credential::new("key".to_string(), "secret".to_string());
528 let nonce = "nonce";
529 let data = "";
530
531 let sig1 = credential.sign_ws_auth(ts1, nonce, data);
532 let sig2 = credential.sign_ws_auth(ts2, nonce, data);
533
534 assert_ne!(sig1, sig2, "Signature should change with timestamp");
535 }
536
537 #[rstest]
538 #[case("nonce1", "nonce2")]
539 #[case("abc123", "xyz789")]
540 fn test_sign_ws_auth_changes_with_nonce(#[case] nonce1: &str, #[case] nonce2: &str) {
541 let credential = Credential::new("key".to_string(), "secret".to_string());
542 let timestamp = 1576074319000u64;
543 let data = "";
544
545 let sig1 = credential.sign_ws_auth(timestamp, nonce1, data);
546 let sig2 = credential.sign_ws_auth(timestamp, nonce2, data);
547
548 assert_ne!(sig1, sig2, "Signature should change with nonce");
549 }
550
551 #[rstest]
552 fn test_resolve_with_both_credentials() {
553 let result = Credential::resolve_with_env_fallback(
554 Some("key".to_string()),
555 Some("secret".to_string()),
556 false,
557 false,
558 );
559
560 assert!(result.is_ok());
561 let credential = result.unwrap();
562 assert!(credential.is_some());
563 assert_eq!(credential.unwrap().api_key().as_str(), "key");
564 }
565
566 #[rstest]
567 fn test_resolve_with_no_credentials_no_fallback() {
568 let result = Credential::resolve_with_env_fallback(None, None, false, false);
569
570 assert!(result.is_ok());
571 assert!(result.unwrap().is_none());
572 }
573
574 #[rstest]
575 fn test_resolve_partial_key_only_returns_error() {
576 let result =
577 Credential::resolve_with_env_fallback(Some("key".to_string()), None, false, false);
578
579 assert!(result.is_err());
580 assert!(matches!(
581 result.unwrap_err(),
582 CredentialError::MissingSecret
583 ));
584 }
585
586 #[rstest]
587 fn test_resolve_partial_secret_only_returns_error() {
588 let result =
589 Credential::resolve_with_env_fallback(None, Some("secret".to_string()), false, false);
590
591 assert!(result.is_err());
592 assert!(matches!(result.unwrap_err(), CredentialError::MissingKey));
593 }
594}