Skip to main content

nautilus_binance/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Binance API credential handling and request signing.
17//!
18//! This module provides two types of credentials:
19//! - [`Credential`]: HMAC SHA256 signing for REST API and standard WebSocket
20//! - [`Ed25519Credential`]: Ed25519 signing for WebSocket API and SBE streams
21//!
22//! Ed25519 keys are required. Credentials are resolved from standard
23//! environment variables (`BINANCE_API_KEY`/`BINANCE_API_SECRET`), falling
24//! back to deprecated `*_ED25519_*` variables with a warning.
25
26#![allow(unused_assignments)] // Fields are used in methods; false positive on some toolchains
27
28use std::fmt::{Debug, Display};
29
30use aws_lc_rs::hmac;
31use ed25519_dalek::{Signature, Signer, SigningKey};
32use ustr::Ustr;
33use zeroize::ZeroizeOnDrop;
34
35use super::enums::{BinanceEnvironment, BinanceProductType};
36
37/// Resolves API credentials from config or environment variables.
38///
39/// Checks standard environment variables first, then falls back to
40/// deprecated `*_ED25519_*` variables with a deprecation warning.
41///
42/// For live environments:
43/// - Deprecated: `BINANCE_ED25519_API_KEY` / `BINANCE_ED25519_API_SECRET`
44/// - Standard: `BINANCE_API_KEY` / `BINANCE_API_SECRET`
45///
46/// For testnet environments (Spot):
47/// - Deprecated: `BINANCE_TESTNET_ED25519_API_KEY` / `BINANCE_TESTNET_ED25519_API_SECRET`
48/// - Standard: `BINANCE_TESTNET_API_KEY` / `BINANCE_TESTNET_API_SECRET`
49///
50/// For testnet environments (Futures):
51/// - Deprecated: `BINANCE_FUTURES_TESTNET_ED25519_API_KEY` / `BINANCE_FUTURES_TESTNET_ED25519_API_SECRET`
52/// - Standard: `BINANCE_FUTURES_TESTNET_API_KEY` / `BINANCE_FUTURES_TESTNET_API_SECRET`
53///
54/// # Errors
55///
56/// Returns an error if credentials cannot be resolved from config or environment.
57pub fn resolve_credentials(
58    config_api_key: Option<String>,
59    config_api_secret: Option<String>,
60    environment: BinanceEnvironment,
61    product_type: BinanceProductType,
62) -> anyhow::Result<(String, String)> {
63    if let (Some(key), Some(secret)) = (config_api_key.clone(), config_api_secret.clone()) {
64        return Ok((key, secret));
65    }
66
67    let (deprecated_key_var, deprecated_secret_var, standard_key_var, standard_secret_var) =
68        match environment {
69            BinanceEnvironment::Testnet => match product_type {
70                BinanceProductType::Spot
71                | BinanceProductType::Margin
72                | BinanceProductType::Options => (
73                    "BINANCE_TESTNET_ED25519_API_KEY",
74                    "BINANCE_TESTNET_ED25519_API_SECRET",
75                    "BINANCE_TESTNET_API_KEY",
76                    "BINANCE_TESTNET_API_SECRET",
77                ),
78                BinanceProductType::UsdM | BinanceProductType::CoinM => (
79                    "BINANCE_FUTURES_TESTNET_ED25519_API_KEY",
80                    "BINANCE_FUTURES_TESTNET_ED25519_API_SECRET",
81                    "BINANCE_FUTURES_TESTNET_API_KEY",
82                    "BINANCE_FUTURES_TESTNET_API_SECRET",
83                ),
84            },
85
86            // Demo shares API keys across all product types
87            BinanceEnvironment::Demo => ("", "", "BINANCE_DEMO_API_KEY", "BINANCE_DEMO_API_SECRET"),
88            BinanceEnvironment::Mainnet => (
89                "BINANCE_ED25519_API_KEY",
90                "BINANCE_ED25519_API_SECRET",
91                "BINANCE_API_KEY",
92                "BINANCE_API_SECRET",
93            ),
94        };
95
96    let api_key = config_api_key
97        .or_else(|| std::env::var(standard_key_var).ok())
98        .or_else(|| {
99            std::env::var(deprecated_key_var).ok().inspect(|_| {
100                log::warn!(
101                    "'{deprecated_key_var}' is deprecated, \
102                     use '{standard_key_var}' instead"
103                );
104            })
105        })
106        .ok_or_else(|| anyhow::anyhow!("{standard_key_var} not found in config or environment"))?;
107
108    let api_secret = config_api_secret
109        .or_else(|| std::env::var(standard_secret_var).ok())
110        .or_else(|| {
111            std::env::var(deprecated_secret_var).ok().inspect(|_| {
112                log::warn!(
113                    "'{deprecated_secret_var}' is deprecated, \
114                     use '{standard_secret_var}' instead"
115                );
116            })
117        })
118        .ok_or_else(|| {
119            anyhow::anyhow!("{standard_secret_var} not found in config or environment")
120        })?;
121
122    Ok((api_key, api_secret))
123}
124
125/// Binance API credentials for signing requests (HMAC SHA256).
126///
127/// Uses HMAC SHA256 with hexadecimal encoding, as required by Binance REST API signing.
128#[derive(Clone, ZeroizeOnDrop)]
129pub struct Credential {
130    #[zeroize(skip)]
131    pub api_key: Ustr,
132    api_secret: Box<[u8]>,
133}
134
135/// Binance Ed25519 credentials for WebSocket API authentication.
136///
137/// Ed25519 is required for WebSocket API authentication (`session.logon`).
138/// This is the only key type supported for execution clients.
139#[derive(ZeroizeOnDrop)]
140pub struct Ed25519Credential {
141    #[zeroize(skip)]
142    pub api_key: Ustr,
143    signing_key: SigningKey,
144}
145
146impl Debug for Credential {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct(stringify!(Credential))
149            .field("api_key", &self.api_key)
150            .field("api_secret", &"<redacted>")
151            .finish()
152    }
153}
154
155impl Credential {
156    /// Creates a new [`Credential`] instance.
157    #[must_use]
158    pub fn new(api_key: String, api_secret: String) -> Self {
159        Self {
160            api_key: api_key.into(),
161            api_secret: api_secret.into_bytes().into_boxed_slice(),
162        }
163    }
164
165    /// Returns the API key.
166    #[must_use]
167    pub fn api_key(&self) -> &str {
168        self.api_key.as_str()
169    }
170
171    /// Signs a message with HMAC SHA256 and returns a lowercase hex digest.
172    #[must_use]
173    pub fn sign(&self, message: &str) -> String {
174        let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
175        let tag = hmac::sign(&key, message.as_bytes());
176        hex::encode(tag.as_ref())
177    }
178}
179
180impl Debug for Ed25519Credential {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.debug_struct(stringify!(Ed25519Credential))
183            .field("api_key", &self.api_key)
184            .field("signing_key", &"<redacted>")
185            .finish()
186    }
187}
188
189impl Ed25519Credential {
190    /// Creates a new [`Ed25519Credential`] from API key and base64-encoded private key.
191    ///
192    /// The private key can be provided as:
193    /// - Raw 32-byte seed (base64 encoded)
194    /// - PKCS#8 DER format (48 bytes, as generated by OpenSSL)
195    /// - PEM format (with or without headers)
196    ///
197    /// For PKCS#8/PEM format, the 32-byte seed is extracted from the last 32 bytes.
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if the private key is not valid base64 or not a valid
202    /// Ed25519 private key.
203    pub fn new(api_key: String, private_key_base64: &str) -> Result<Self, Ed25519CredentialError> {
204        // Strip PEM headers/footers if present
205        let key_data: String = private_key_base64
206            .lines()
207            .filter(|line| !line.starts_with("-----"))
208            .collect();
209
210        let private_key_bytes =
211            base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &key_data)
212                .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
213
214        // Extract 32-byte seed: works for both raw (32 bytes) and PKCS#8 (48 bytes)
215        if private_key_bytes.len() < 32 {
216            return Err(Ed25519CredentialError::InvalidKeyLength);
217        }
218        let seed_start = private_key_bytes.len() - 32;
219        let key_bytes: [u8; 32] = private_key_bytes[seed_start..]
220            .try_into()
221            .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
222
223        let signing_key = SigningKey::from_bytes(&key_bytes);
224
225        Ok(Self {
226            api_key: api_key.into(),
227            signing_key,
228        })
229    }
230
231    /// Returns the API key.
232    #[must_use]
233    pub fn api_key(&self) -> &str {
234        self.api_key.as_str()
235    }
236
237    /// Signs a message with Ed25519 and returns a base64-encoded signature.
238    #[must_use]
239    pub fn sign(&self, message: &[u8]) -> String {
240        let signature: Signature = self.signing_key.sign(message);
241        base64::Engine::encode(
242            &base64::engine::general_purpose::STANDARD,
243            signature.to_bytes(),
244        )
245    }
246}
247
248/// Error type for Ed25519 credential creation.
249#[derive(Debug, Clone)]
250pub enum Ed25519CredentialError {
251    /// The private key is not valid base64.
252    InvalidBase64(String),
253    /// The private key is not 32 bytes.
254    InvalidKeyLength,
255}
256
257impl Display for Ed25519CredentialError {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        match self {
260            Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
261            Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
262        }
263    }
264}
265
266impl std::error::Error for Ed25519CredentialError {}
267
268#[cfg(test)]
269mod tests {
270    use rstest::rstest;
271
272    use super::*;
273
274    // Official Binance test vectors from:
275    // https://github.com/binance/binance-signature-examples
276    const BINANCE_TEST_SECRET: &str =
277        "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
278
279    #[rstest]
280    fn test_sign_matches_binance_test_vector_simple() {
281        let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
282        let message = "timestamp=1578963600000";
283        let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
284
285        assert_eq!(cred.sign(message), expected);
286    }
287
288    #[rstest]
289    fn test_sign_matches_binance_test_vector_order() {
290        let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
291        let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559";
292        let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
293
294        assert_eq!(cred.sign(message), expected);
295    }
296}