nautilus_binance/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 SBE market data streams
21
22#![allow(unused_assignments)] // Fields are used in methods; false positive on some toolchains
23
24use std::fmt::Debug;
25
26use aws_lc_rs::hmac;
27use ed25519_dalek::{Signature, Signer, SigningKey};
28use ustr::Ustr;
29use zeroize::ZeroizeOnDrop;
30
31/// Binance API credentials for signing requests (HMAC SHA256).
32///
33/// Uses HMAC SHA256 with hexadecimal encoding, as required by Binance REST API signing.
34#[derive(Clone, ZeroizeOnDrop)]
35pub struct Credential {
36    #[zeroize(skip)]
37    pub api_key: Ustr,
38    api_secret: Box<[u8]>,
39}
40
41/// Binance Ed25519 credentials for SBE market data streams.
42///
43/// SBE market data streams at `stream-sbe.binance.com` require Ed25519 API key
44/// authentication via the `X-MBX-APIKEY` header.
45#[derive(ZeroizeOnDrop)]
46pub struct Ed25519Credential {
47    #[zeroize(skip)]
48    pub api_key: Ustr,
49    signing_key: SigningKey,
50}
51
52impl Debug for Credential {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct(stringify!(Credential))
55            .field("api_key", &self.api_key)
56            .field("api_secret", &"<redacted>")
57            .finish()
58    }
59}
60
61impl Credential {
62    /// Creates a new [`Credential`] instance.
63    #[must_use]
64    pub fn new(api_key: String, api_secret: String) -> Self {
65        Self {
66            api_key: api_key.into(),
67            api_secret: api_secret.into_bytes().into_boxed_slice(),
68        }
69    }
70
71    /// Returns the API key.
72    #[must_use]
73    pub fn api_key(&self) -> &str {
74        self.api_key.as_str()
75    }
76
77    /// Signs a message with HMAC SHA256 and returns a lowercase hex digest.
78    #[must_use]
79    pub fn sign(&self, message: &str) -> String {
80        let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
81        let tag = hmac::sign(&key, message.as_bytes());
82        hex::encode(tag.as_ref())
83    }
84}
85
86impl Debug for Ed25519Credential {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct(stringify!(Ed25519Credential))
89            .field("api_key", &self.api_key)
90            .field("signing_key", &"<redacted>")
91            .finish()
92    }
93}
94
95impl Ed25519Credential {
96    /// Creates a new [`Ed25519Credential`] from API key and base64-encoded private key.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the private key is not valid base64 or not a valid
101    /// Ed25519 private key (32 bytes).
102    pub fn new(api_key: String, private_key_base64: &str) -> Result<Self, Ed25519CredentialError> {
103        let private_key_bytes = base64::Engine::decode(
104            &base64::engine::general_purpose::STANDARD,
105            private_key_base64,
106        )
107        .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
108
109        let key_bytes: [u8; 32] = private_key_bytes
110            .try_into()
111            .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
112
113        let signing_key = SigningKey::from_bytes(&key_bytes);
114
115        Ok(Self {
116            api_key: api_key.into(),
117            signing_key,
118        })
119    }
120
121    /// Returns the API key.
122    #[must_use]
123    pub fn api_key(&self) -> &str {
124        self.api_key.as_str()
125    }
126
127    /// Signs a message with Ed25519 and returns a base64-encoded signature.
128    #[must_use]
129    pub fn sign(&self, message: &[u8]) -> String {
130        let signature: Signature = self.signing_key.sign(message);
131        base64::Engine::encode(
132            &base64::engine::general_purpose::STANDARD,
133            signature.to_bytes(),
134        )
135    }
136}
137
138/// Error type for Ed25519 credential creation.
139#[derive(Debug, Clone)]
140pub enum Ed25519CredentialError {
141    /// The private key is not valid base64.
142    InvalidBase64(String),
143    /// The private key is not 32 bytes.
144    InvalidKeyLength,
145}
146
147impl std::fmt::Display for Ed25519CredentialError {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
151            Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
152        }
153    }
154}
155
156impl std::error::Error for Ed25519CredentialError {}
157
158#[cfg(test)]
159mod tests {
160    use rstest::rstest;
161
162    use super::*;
163
164    // Official Binance test vectors from:
165    // https://github.com/binance/binance-signature-examples
166    const BINANCE_TEST_SECRET: &str =
167        "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
168
169    #[rstest]
170    fn test_sign_matches_binance_test_vector_simple() {
171        let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
172        let message = "timestamp=1578963600000";
173        let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
174
175        assert_eq!(cred.sign(message), expected);
176    }
177
178    #[rstest]
179    fn test_sign_matches_binance_test_vector_order() {
180        let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
181        let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559";
182        let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
183
184        assert_eq!(cred.sign(message), expected);
185    }
186}