1#![allow(unused_assignments)] use std::fmt::{Debug, Display};
29
30use aws_lc_rs::hmac;
31use ed25519_dalek::{Signature, Signer, SigningKey};
32use nautilus_core::{
33 hex,
34 string::secret::{REDACTED, SecretString},
35};
36use zeroize::{ZeroizeOnDrop, Zeroizing};
37
38use super::enums::{BinanceEnvironment, BinanceProductType};
39
40pub fn resolve_credentials(
55 config_api_key: Option<String>,
56 config_api_secret: Option<String>,
57 environment: BinanceEnvironment,
58 product_type: BinanceProductType,
59) -> anyhow::Result<(String, String)> {
60 if let (Some(key), Some(secret)) = (config_api_key.clone(), config_api_secret.clone()) {
61 return Ok((key, secret));
62 }
63
64 let (deprecated_key_var, deprecated_secret_var, standard_key_var, standard_secret_var) =
65 match environment {
66 BinanceEnvironment::Testnet => match product_type {
67 BinanceProductType::Spot
68 | BinanceProductType::Margin
69 | BinanceProductType::Options => (
70 "BINANCE_TESTNET_ED25519_API_KEY",
71 "BINANCE_TESTNET_ED25519_API_SECRET",
72 "BINANCE_TESTNET_API_KEY",
73 "BINANCE_TESTNET_API_SECRET",
74 ),
75 BinanceProductType::UsdM | BinanceProductType::CoinM => (
76 "BINANCE_FUTURES_TESTNET_ED25519_API_KEY",
77 "BINANCE_FUTURES_TESTNET_ED25519_API_SECRET",
78 "BINANCE_FUTURES_TESTNET_API_KEY",
79 "BINANCE_FUTURES_TESTNET_API_SECRET",
80 ),
81 },
82
83 BinanceEnvironment::Demo => ("", "", "BINANCE_DEMO_API_KEY", "BINANCE_DEMO_API_SECRET"),
85 BinanceEnvironment::Live => (
86 "BINANCE_ED25519_API_KEY",
87 "BINANCE_ED25519_API_SECRET",
88 "BINANCE_API_KEY",
89 "BINANCE_API_SECRET",
90 ),
91 };
92
93 let is_futures = matches!(
96 product_type,
97 BinanceProductType::UsdM | BinanceProductType::CoinM
98 );
99
100 let api_key = config_api_key
101 .or_else(|| std::env::var(standard_key_var).ok())
102 .or_else(|| resolve_deprecated_var(deprecated_key_var, standard_key_var, is_futures))
103 .ok_or_else(|| anyhow::anyhow!("{standard_key_var} not found in config or environment"))?;
104
105 let api_secret = config_api_secret
106 .or_else(|| std::env::var(standard_secret_var).ok())
107 .or_else(|| resolve_deprecated_var(deprecated_secret_var, standard_secret_var, is_futures))
108 .ok_or_else(|| {
109 anyhow::anyhow!("{standard_secret_var} not found in config or environment")
110 })?;
111
112 Ok((api_key, api_secret))
113}
114
115fn resolve_deprecated_var(
116 deprecated_var: &str,
117 standard_var: &str,
118 allow_fallback: bool,
119) -> Option<String> {
120 if deprecated_var.is_empty() {
121 return None;
122 }
123
124 let value = std::env::var(deprecated_var).ok()?;
125
126 if allow_fallback {
127 log::warn!(
128 "'{deprecated_var}' is deprecated and will be removed in a future version. \
129 Rename it to '{standard_var}' (Ed25519 keys are now auto-detected)"
130 );
131 Some(value)
132 } else {
133 log::error!(
134 "'{deprecated_var}' has been removed. \
135 Rename it to '{standard_var}' (Ed25519 keys are now auto-detected)"
136 );
137 None
138 }
139}
140
141#[derive(Clone, ZeroizeOnDrop)]
145pub struct Credential {
146 api_key: Box<str>,
147 api_secret: Box<[u8]>,
148}
149
150#[derive(ZeroizeOnDrop)]
155pub struct Ed25519Credential {
156 api_key: Box<str>,
157 signing_key: SigningKey,
158}
159
160impl Debug for Credential {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 f.debug_struct(stringify!(Credential))
163 .field("api_key", &REDACTED)
164 .field("api_secret", &REDACTED)
165 .finish()
166 }
167}
168
169impl Credential {
170 #[must_use]
172 pub fn new(api_key: String, api_secret: String) -> Self {
173 Self {
174 api_key: api_key.into_boxed_str(),
175 api_secret: api_secret.into_bytes().into_boxed_slice(),
176 }
177 }
178
179 #[must_use]
181 pub fn api_key(&self) -> &str {
182 &self.api_key
183 }
184
185 #[must_use]
187 pub fn sign(&self, message: &str) -> String {
188 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
189 let tag = hmac::sign(&key, message.as_bytes());
190 hex::encode(tag.as_ref())
191 }
192}
193
194impl Debug for Ed25519Credential {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct(stringify!(Ed25519Credential))
197 .field("api_key", &REDACTED)
198 .field("signing_key", &REDACTED)
199 .finish()
200 }
201}
202
203const ED25519_OID: [u8; 5] = [0x06, 0x03, 0x2B, 0x65, 0x70];
210
211impl Ed25519Credential {
212 pub fn new(
229 api_key: SecretString,
230 private_key_base64: SecretString,
231 ) -> Result<Self, Ed25519CredentialError> {
232 let private_key_base64 = Zeroizing::new(private_key_base64.into_inner());
233
234 let key_data = Zeroizing::new(
236 private_key_base64
237 .lines()
238 .filter(|line| !line.starts_with("-----"))
239 .collect::<String>(),
240 );
241
242 let private_key_bytes = Zeroizing::new(
243 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &key_data)
244 .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?,
245 );
246
247 if !contains_subslice(&private_key_bytes, &ED25519_OID) {
248 return Err(Ed25519CredentialError::NotEd25519);
249 }
250
251 if private_key_bytes.len() < 32 {
252 return Err(Ed25519CredentialError::InvalidKeyLength);
253 }
254 let seed_start = private_key_bytes.len() - 32;
255 let key_bytes = Zeroizing::new(
256 private_key_bytes[seed_start..]
257 .try_into()
258 .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?,
259 );
260
261 let signing_key = SigningKey::from_bytes(&key_bytes);
262
263 Ok(Self {
264 api_key: api_key.into_inner().into_boxed_str(),
265 signing_key,
266 })
267 }
268
269 #[must_use]
271 pub fn api_key(&self) -> &str {
272 &self.api_key
273 }
274
275 #[must_use]
277 pub fn sign(&self, message: &[u8]) -> String {
278 let signature: Signature = self.signing_key.sign(message);
279 base64::Engine::encode(
280 &base64::engine::general_purpose::STANDARD,
281 signature.to_bytes(),
282 )
283 }
284}
285
286#[derive(Debug, Clone)]
288pub enum Ed25519CredentialError {
289 InvalidBase64(String),
291 NotEd25519,
293 InvalidKeyLength,
295}
296
297impl Display for Ed25519CredentialError {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match self {
300 Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
301 Self::NotEd25519 => write!(f, "Decoded key does not carry the Ed25519 PKCS#8 OID"),
302 Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
303 }
304 }
305}
306
307impl std::error::Error for Ed25519CredentialError {}
308
309#[derive(Clone)]
319pub enum SigningCredential {
320 Hmac(Credential),
322 Ed25519(Box<Ed25519Credential>),
324}
325
326impl Debug for SigningCredential {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 match self {
329 Self::Hmac(c) => f.debug_tuple("Hmac").field(c).finish(),
330 Self::Ed25519(c) => f.debug_tuple("Ed25519").field(c).finish(),
331 }
332 }
333}
334
335impl SigningCredential {
336 #[must_use]
341 pub fn new(api_key: String, api_secret: String) -> Self {
342 let api_key = SecretString::from(api_key);
343 let api_secret = SecretString::from(api_secret);
344
345 match Ed25519Credential::new(api_key.clone(), api_secret.clone()) {
346 Ok(ed25519) => {
347 log::debug!("Auto-detected Ed25519 API key");
348 Self::Ed25519(Box::new(ed25519))
349 }
350 Err(_) => {
351 log::debug!("Using HMAC SHA256 API key");
352 Self::Hmac(Credential::new(
353 api_key.into_inner(),
354 api_secret.into_inner(),
355 ))
356 }
357 }
358 }
359
360 #[must_use]
362 pub fn api_key(&self) -> &str {
363 match self {
364 Self::Hmac(c) => c.api_key(),
365 Self::Ed25519(c) => c.api_key(),
366 }
367 }
368
369 #[must_use]
374 pub fn sign(&self, message: &str) -> String {
375 match self {
376 Self::Hmac(c) => c.sign(message),
377 Self::Ed25519(c) => c.sign(message.as_bytes()),
378 }
379 }
380
381 #[must_use]
383 pub fn is_ed25519(&self) -> bool {
384 matches!(self, Self::Ed25519(_))
385 }
386}
387
388impl Clone for Ed25519Credential {
391 fn clone(&self) -> Self {
392 let key_bytes = Zeroizing::new(self.signing_key.to_bytes());
394 Self {
395 api_key: self.api_key.clone(),
396 signing_key: SigningKey::from_bytes(&key_bytes),
397 }
398 }
399}
400
401fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
402 if needle.is_empty() || needle.len() > haystack.len() {
403 return false;
404 }
405 haystack.windows(needle.len()).any(|w| w == needle)
406}
407
408pub(crate) fn canonical_ws_query_string<'a, I>(
428 params: I,
429) -> Result<String, serde_urlencoded::ser::Error>
430where
431 I: IntoIterator<Item = (&'a str, &'a serde_json::Value)>,
432{
433 let sorted: std::collections::BTreeMap<&str, &serde_json::Value> = params.into_iter().collect();
437 serde_urlencoded::to_string(sorted)
438}
439
440#[cfg(test)]
441mod tests {
442 use rstest::rstest;
443
444 use super::*;
445
446 const BINANCE_TEST_SECRET: &str =
449 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
450
451 #[rstest]
452 fn test_sign_matches_binance_test_vector_simple() {
453 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
454 let message = "timestamp=1578963600000";
455 let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
456
457 assert_eq!(cred.sign(message), expected);
458 }
459
460 #[rstest]
461 fn test_sign_matches_binance_test_vector_order() {
462 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
463 let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
464 let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
465
466 assert_eq!(cred.sign(message), expected);
467 }
468
469 #[rstest]
470 fn test_canonical_ws_query_string_is_key_sorted_regardless_of_input_order() {
471 let symbol = serde_json::json!("LTCBTC");
477 let side = serde_json::json!("BUY");
478 let quantity = serde_json::json!("1");
479 let timestamp = serde_json::json!(1_499_827_319_559i64);
480 let api_key = serde_json::json!("mykey");
481 let unsorted = [
482 ("symbol", &symbol),
483 ("side", &side),
484 ("quantity", &quantity),
485 ("timestamp", ×tamp),
486 ("apiKey", &api_key),
487 ];
488
489 let query = canonical_ws_query_string(unsorted).unwrap();
490
491 assert_eq!(
492 query,
493 "apiKey=mykey&quantity=1&side=BUY&symbol=LTCBTC×tamp=1499827319559"
494 );
495 }
496
497 #[rstest]
498 fn test_canonical_ws_query_string_preserves_urlencoding() {
499 let symbol = serde_json::json!("LTCBTC");
500 let new_client_order_id = serde_json::json!("desk alpha");
501 let unsorted = [
502 ("symbol", &symbol),
503 ("newClientOrderId", &new_client_order_id),
504 ];
505
506 let query = canonical_ws_query_string(unsorted).unwrap();
507
508 assert_eq!(query, "newClientOrderId=desk+alpha&symbol=LTCBTC");
509 }
510
511 #[rstest]
512 fn test_debug_redacts_secret() {
513 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
514 let dbg_out = format!("{cred:?}");
515
516 assert_eq!(dbg_out.matches(REDACTED).count(), 2);
517 assert!(!dbg_out.contains("test_key"));
518 assert!(!dbg_out.contains("NhqPtmdSJYdKjVHjA7PZj4"));
519 }
520
521 const ED25519_PKCS8_TEST_VECTOR: [u8; 48] = [
526 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04,
527 0x20, 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec,
528 0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c,
529 0xae, 0x7f, 0x60,
530 ];
531
532 #[rstest]
533 fn test_ed25519_matches_rfc_8032_vector() {
534 let key_b64 = base64::Engine::encode(
535 &base64::engine::general_purpose::STANDARD,
536 ED25519_PKCS8_TEST_VECTOR,
537 );
538
539 let cred = Ed25519Credential::new("test_key".into(), key_b64.into()).unwrap();
540
541 let signature = cred.sign(b"");
542
543 assert_eq!(
544 signature,
545 "5VZDAMNgrHKQhuLMgG6CioSHfx645dl02HPgZSJJAVVfuIIVkKM7rMYeOXAc+bRr0lv18FlbviRlUUFDjnoQCw=="
546 );
547 }
548
549 #[rstest]
550 fn test_ed25519_rejects_raw_32_byte_seed() {
551 let seed = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0xABu8; 32]);
555
556 let result = Ed25519Credential::new("test_key".into(), seed.into());
557
558 assert!(matches!(result, Err(Ed25519CredentialError::NotEd25519)));
559 }
560
561 #[rstest]
562 fn test_ed25519_rejects_binance_hmac_secret() {
563 let result = Ed25519Credential::new("test_key".into(), BINANCE_TEST_SECRET.into());
567
568 assert!(matches!(result, Err(Ed25519CredentialError::NotEd25519)));
569 }
570
571 #[rstest]
572 fn test_signing_credential_autodetect_falls_back_to_hmac_on_binance_secret() {
573 let cred = SigningCredential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
577
578 assert!(matches!(cred, SigningCredential::Hmac(_)));
579 }
580
581 #[rstest]
582 fn test_ed25519_debug_redacts_secret() {
583 let key_b64 = base64::Engine::encode(
584 &base64::engine::general_purpose::STANDARD,
585 ED25519_PKCS8_TEST_VECTOR,
586 );
587
588 let cred = Ed25519Credential::new("test_key".into(), key_b64.clone().into()).unwrap();
589 let dbg_out = format!("{cred:?}");
590
591 assert_eq!(dbg_out.matches(REDACTED).count(), 2);
592 assert!(!dbg_out.contains("test_key"));
593 assert!(!dbg_out.contains(&key_b64));
594 }
595}