Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,6 @@ url = { workspace = true }
regex = { workspace = true }
paste = { workspace = true }

[[bin]]
name = "test_u64_since"
path = "test_u64_since.rs"

[dev-dependencies]
tokio-test = { workspace = true }
criterion = { workspace = true }
Expand Down
221 changes: 204 additions & 17 deletions ccxt-core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,50 @@ use sha3::Keccak256;
use std::fmt;

/// Supported cryptographic hash algorithms.
///
/// # Security Warning
///
/// **MD5 and SHA-1 are deprecated** due to known cryptographic vulnerabilities.
/// Use [Sha256](Self::Sha256) or stronger algorithms for new implementations.
/// The deprecated variants are maintained only for backward compatibility with
/// legacy exchanges that still require them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlgorithm {
/// SHA-1 hash algorithm
///
/// # Security Warning
///
/// **DEPRECATED** - SHA-1 has been cryptographically broken since 2017.
/// Collision attacks are practical. Do NOT use for new implementations.
/// Use [Sha256](Self::Sha256) instead.
#[deprecated(
since = "0.1.3",
note = "SHA-1 is cryptographically broken, use Sha256 instead"
)]
Sha1,
/// SHA-256 hash algorithm

/// SHA-256 hash algorithm (recommended)
Sha256,

/// SHA-384 hash algorithm
Sha384,

/// SHA-512 hash algorithm
Sha512,

/// MD5 hash algorithm
///
/// # Security Warning
///
/// **DEPRECATED** - MD5 has been cryptographically broken since 2004.
/// Collision attacks are practical. Do NOT use for new implementations.
/// Use [Sha256](Self::Sha256) instead.
#[deprecated(
since = "0.1.3",
note = "MD5 is cryptographically broken, use Sha256 instead"
)]
Md5,

/// Keccak-256 (SHA-3) hash algorithm
Keccak,
}
Expand All @@ -50,6 +82,7 @@ impl HashAlgorithm {
// Lint: should_implement_trait
// Reason: This method returns Result<Self> with custom error type, not compatible with FromStr trait
#[allow(clippy::should_implement_trait)]
#[allow(deprecated)]
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"sha1" => Ok(HashAlgorithm::Sha1),
Expand All @@ -66,6 +99,7 @@ impl HashAlgorithm {
}

impl fmt::Display for HashAlgorithm {
#[allow(deprecated)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
HashAlgorithm::Sha1 => "sha1",
Expand Down Expand Up @@ -145,6 +179,7 @@ pub fn hmac_sign(
algorithm: HashAlgorithm,
digest: DigestFormat,
) -> Result<String> {
#[allow(deprecated)]
let signature = match algorithm {
HashAlgorithm::Sha256 => hmac_sha256(message.as_bytes(), secret.as_bytes()),
HashAlgorithm::Sha512 => hmac_sha512(message.as_bytes(), secret.as_bytes()),
Expand Down Expand Up @@ -237,6 +272,7 @@ fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec<u8> {
/// let hashed = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
/// ```
pub fn hash(data: &str, algorithm: HashAlgorithm, digest: DigestFormat) -> Result<String> {
#[allow(deprecated)]
let hash_bytes = match algorithm {
HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()),
HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),
Expand Down Expand Up @@ -386,7 +422,7 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
///
/// # Arguments
/// * `payload` - JWT payload as JSON object
/// * `secret` - Secret key for signing
/// * `secret` - Secret key for signing (must be at least 32 characters for security)
/// * `algorithm` - Hash algorithm for HMAC (supports Sha256, Sha384, Sha512)
/// * `header_options` - Optional additional header fields
///
Expand All @@ -398,6 +434,14 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
/// - JSON serialization fails
/// - Signing fails
/// - Unsupported algorithm is provided (only HS256, HS384, HS512 are supported)
/// - Secret key is less than 32 characters (security requirement)
///
/// # Security
///
/// **Minimum secret length: 32 characters**
///
/// Short secrets are vulnerable to brute-force attacks. The minimum 32-character
/// requirement ensures sufficient entropy for secure HMAC signatures.
///
/// # Examples
/// ```
Expand All @@ -409,18 +453,40 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
/// "exp": 1234567890
/// });
///
/// // Using HS256
/// let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
/// // Using HS256 with a strong secret (at least 32 characters)
/// let token = jwt_sign(
/// &payload,
/// "my-very-secure-secret-key-at-least-32-chars",
/// HashAlgorithm::Sha256,
/// None
/// ).unwrap();
///
/// // Using HS512
/// let token_512 = jwt_sign(&payload, "secret", HashAlgorithm::Sha512, None).unwrap();
/// let token_512 = jwt_sign(
/// &payload,
/// "my-very-secure-secret-key-at-least-32-chars",
/// HashAlgorithm::Sha512,
/// None
/// ).unwrap();
/// ```
pub fn jwt_sign(
payload: &serde_json::Value,
secret: &str,
algorithm: HashAlgorithm,
header_options: Option<serde_json::Map<String, serde_json::Value>>,
) -> Result<String> {
// Validate secret key strength (minimum 32 characters for security)
const MIN_SECRET_LENGTH: usize = 32;

if secret.len() < MIN_SECRET_LENGTH {
return Err(Error::invalid_argument(format!(
"JWT secret must be at least {MIN_SECRET_LENGTH} characters for security. \
Provided: {} characters. \
Use a longer secret to protect against brute-force attacks.",
secret.len()
)));
}

// Map HashAlgorithm to JWT algorithm string
let alg_str = match algorithm {
HashAlgorithm::Sha256 => "HS256",
Expand Down Expand Up @@ -552,7 +618,7 @@ mod tests {
#[test]
fn test_hash_keccak() {
let result = hash("test", HashAlgorithm::Keccak, DigestFormat::Hex).unwrap();
assert_eq!(result.len(), 64); // Keccak256输出32字节=64个hex字符
assert_eq!(result.len(), 64); // Keccak256 outputs 32 bytes = 64 hex characters
}

#[test]
Expand All @@ -578,9 +644,16 @@ mod tests {
"exp": 1234567890
});

let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
// Use a strong secret (at least 32 characters)
let token = jwt_sign(
&payload,
"my-very-secure-secret-key-at-least-32-chars",
HashAlgorithm::Sha256,
None,
)
.unwrap();

// JWT应该有3部分,用.分隔
// JWT should have 3 parts separated by .
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3);

Expand All @@ -599,20 +672,22 @@ mod tests {
"exp": 1234567890
});

let strong_secret = "my-very-secure-secret-key-at-least-32-chars";

// Test HS256
let token_256 = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
let token_256 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha256, None).unwrap();
let parts_256: Vec<&str> = token_256.split('.').collect();
let header_256 = String::from_utf8(base64url_decode(parts_256[0]).unwrap()).unwrap();
assert!(header_256.contains("HS256"));

// Test HS384
let token_384 = jwt_sign(&payload, "secret", HashAlgorithm::Sha384, None).unwrap();
let token_384 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha384, None).unwrap();
let parts_384: Vec<&str> = token_384.split('.').collect();
let header_384 = String::from_utf8(base64url_decode(parts_384[0]).unwrap()).unwrap();
assert!(header_384.contains("HS384"));

// Test HS512
let token_512 = jwt_sign(&payload, "secret", HashAlgorithm::Sha512, None).unwrap();
let token_512 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha512, None).unwrap();
let parts_512: Vec<&str> = token_512.split('.').collect();
let header_512 = String::from_utf8(base64url_decode(parts_512[0]).unwrap()).unwrap();
assert!(header_512.contains("HS512"));
Expand All @@ -628,16 +703,31 @@ mod tests {
});

// SHA1 is not supported for JWT
let result = jwt_sign(&payload, "secret", HashAlgorithm::Sha1, None);
let result = jwt_sign(
&payload,
"my-very-secure-secret-key-at-least-32-chars",
HashAlgorithm::Sha1,
None,
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("does not support"));

// MD5 is not supported for JWT
let result = jwt_sign(&payload, "secret", HashAlgorithm::Md5, None);
let result = jwt_sign(
&payload,
"my-very-secure-secret-key-at-least-32-chars",
HashAlgorithm::Md5,
None,
);
assert!(result.is_err());

// Keccak is not supported for JWT
let result = jwt_sign(&payload, "secret", HashAlgorithm::Keccak, None);
let result = jwt_sign(
&payload,
"my-very-secure-secret-key-at-least-32-chars",
HashAlgorithm::Keccak,
None,
);
assert!(result.is_err());
}

Expand All @@ -659,18 +749,115 @@ mod tests {
assert_eq!(DigestFormat::from_str("hex"), DigestFormat::Hex);
assert_eq!(DigestFormat::from_str("base64"), DigestFormat::Base64);
assert_eq!(DigestFormat::from_str("binary"), DigestFormat::Binary);
assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // 默认
assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // defaults to Hex
}

#[test]
fn test_hmac_sha512() {
let result = hmac_sign("test", "secret", HashAlgorithm::Sha512, DigestFormat::Hex).unwrap();
assert_eq!(result.len(), 128); // SHA512输出64字节=128个hex字符
assert_eq!(result.len(), 128); // SHA512 outputs 64 bytes = 128 hex characters
}

#[test]
fn test_hash_md5() {
let result = hash("test", HashAlgorithm::Md5, DigestFormat::Hex).unwrap();
assert_eq!(result.len(), 32); // MD5输出16字节=32个hex字符
assert_eq!(result.len(), 32); // MD5 outputs 16 bytes = 32 hex characters
}

#[test]
fn test_jwt_sign_weak_secret_rejected() {
use serde_json::json;

let payload = json!({
"user_id": "123",
"exp": 1234567890
});

// Test various weak secrets (less than 32 characters)
let weak_secrets = vec![
"", // empty
"a", // 1 character
"short", // 5 characters
"this-is-still-too-short-123", // 31 characters (just below threshold)
];

for weak_secret in weak_secrets {
let result = jwt_sign(&payload, weak_secret, HashAlgorithm::Sha256, None);
assert!(
result.is_err(),
"Secret with {} characters should be rejected",
weak_secret.len()
);

if let Err(e) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("32 characters"),
"Error message should mention 32 character requirement"
);
assert!(
error_msg.contains("security"),
"Error message should mention security"
);
}
}
}

#[test]
fn test_jwt_sign_minimum_valid_secret() {
use serde_json::json;

let payload = json!({
"user_id": "123",
"exp": 1234567890
});

// Test exactly 32 characters (should succeed)
let exactly_32_chars = "12345678901234567890123456789012"; // exactly 32 chars
let result = jwt_sign(&payload, exactly_32_chars, HashAlgorithm::Sha256, None);
assert!(
result.is_ok(),
"Secret with exactly 32 characters should be accepted"
);

// Test 33 characters (should succeed)
let exactly_33_chars = "123456789012345678901234567890123"; // exactly 33 chars
let result = jwt_sign(&payload, exactly_33_chars, HashAlgorithm::Sha256, None);
assert!(
result.is_ok(),
"Secret with 33 characters should be accepted"
);
}

#[test]
fn test_jwt_sign_with_custom_header_and_strong_secret() {
use serde_json::json;

let payload = json!({
"user_id": "123",
"exp": 1234567890
});

let mut custom_header = serde_json::Map::new();
custom_header.insert(
"kid".to_string(),
serde_json::Value::String("key-123".to_string()),
);

let strong_secret = "my-very-secure-secret-key-at-least-32-chars";
let token = jwt_sign(
&payload,
strong_secret,
HashAlgorithm::Sha256,
Some(custom_header),
)
.unwrap();

let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3);

let header_bytes = base64url_decode(parts[0]).unwrap();
let header_str = String::from_utf8(header_bytes).unwrap();
assert!(header_str.contains("\"kid\":\"key-123\""));
}
}
1 change: 1 addition & 0 deletions ccxt-core/src/base_exchange/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl BaseExchange {
..crate::retry_strategy::RetryConfig::default()
}),
max_response_size: 10 * 1024 * 1024, // 10MB default
max_request_size: 10 * 1024 * 1024, // 10MB default
circuit_breaker: None, // Disabled by default for backward compatibility
pool_max_idle_per_host: 10, // Default: 10 idle connections per host
pool_idle_timeout: Duration::from_secs(90), // Default: 90 seconds
Expand Down
Loading