Skip to content

Commit 41c5c3a

Browse files
authored
feat(security): deprecate weak hash algorithms and enforce strong sec… (#35)
* feat(security): deprecate weak hash algorithms and enforce strong secrets - Mark SHA-1 and MD5 as deprecated in HashAlgorithm enum with security warnings - Add minimum 32-character requirement for JWT secret keys to prevent brute-force attacks - Implement secret length validation in jwt_sign function with detailed error messages - Update JWT examples to use strong secrets with at least 32 characters - Add comprehensive tests for weak secret rejection and minimum valid secret acceptance - Remove deprecated test binary configuration and source file - Add request body size validation in HTTP client to prevent DoS attacks - Introduce max_request_size configuration with 10MB default and 100MB maximum - Add validation for request size limits in HTTP configuration - Reduce WebSocket pong timeout from 90 seconds to 30 seconds for faster zombie detection - Improve circuit breaker failure recording with atomic state transitions - Enhance rate limiter calculations using u128 to prevent overflow for long uptimes - Update documentation comments and test descriptions with security considerations * chore(auth): add deprecated attribute allowances for hash algorithm implementations - Added #[allow(deprecated)] attribute to HashAlgorithm::from_str method - Added #[allow(deprecated)] attribute to HashAlgorithm Display implementation - Added #[allow(deprecated)] attribute to hmac signing function - Added #[allow(deprecated)] attribute to hash function implementation
1 parent 8853995 commit 41c5c3a

14 files changed

Lines changed: 393 additions & 100 deletions

File tree

‎Cargo.toml‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,6 @@ url = { workspace = true }
141141
regex = { workspace = true }
142142
paste = { workspace = true }
143143

144-
[[bin]]
145-
name = "test_u64_since"
146-
path = "test_u64_since.rs"
147-
148144
[dev-dependencies]
149145
tokio-test = { workspace = true }
150146
criterion = { workspace = true }

‎ccxt-core/src/auth.rs‎

Lines changed: 204 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,50 @@ use sha3::Keccak256;
2323
use std::fmt;
2424

2525
/// Supported cryptographic hash algorithms.
26+
///
27+
/// # Security Warning
28+
///
29+
/// **MD5 and SHA-1 are deprecated** due to known cryptographic vulnerabilities.
30+
/// Use [Sha256](Self::Sha256) or stronger algorithms for new implementations.
31+
/// The deprecated variants are maintained only for backward compatibility with
32+
/// legacy exchanges that still require them.
2633
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2734
pub enum HashAlgorithm {
2835
/// SHA-1 hash algorithm
36+
///
37+
/// # Security Warning
38+
///
39+
/// **DEPRECATED** - SHA-1 has been cryptographically broken since 2017.
40+
/// Collision attacks are practical. Do NOT use for new implementations.
41+
/// Use [Sha256](Self::Sha256) instead.
42+
#[deprecated(
43+
since = "0.1.3",
44+
note = "SHA-1 is cryptographically broken, use Sha256 instead"
45+
)]
2946
Sha1,
30-
/// SHA-256 hash algorithm
47+
48+
/// SHA-256 hash algorithm (recommended)
3149
Sha256,
50+
3251
/// SHA-384 hash algorithm
3352
Sha384,
53+
3454
/// SHA-512 hash algorithm
3555
Sha512,
56+
3657
/// MD5 hash algorithm
58+
///
59+
/// # Security Warning
60+
///
61+
/// **DEPRECATED** - MD5 has been cryptographically broken since 2004.
62+
/// Collision attacks are practical. Do NOT use for new implementations.
63+
/// Use [Sha256](Self::Sha256) instead.
64+
#[deprecated(
65+
since = "0.1.3",
66+
note = "MD5 is cryptographically broken, use Sha256 instead"
67+
)]
3768
Md5,
69+
3870
/// Keccak-256 (SHA-3) hash algorithm
3971
Keccak,
4072
}
@@ -50,6 +82,7 @@ impl HashAlgorithm {
5082
// Lint: should_implement_trait
5183
// Reason: This method returns Result<Self> with custom error type, not compatible with FromStr trait
5284
#[allow(clippy::should_implement_trait)]
85+
#[allow(deprecated)]
5386
pub fn from_str(s: &str) -> Result<Self> {
5487
match s.to_lowercase().as_str() {
5588
"sha1" => Ok(HashAlgorithm::Sha1),
@@ -66,6 +99,7 @@ impl HashAlgorithm {
6699
}
67100

68101
impl fmt::Display for HashAlgorithm {
102+
#[allow(deprecated)]
69103
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70104
let s = match self {
71105
HashAlgorithm::Sha1 => "sha1",
@@ -145,6 +179,7 @@ pub fn hmac_sign(
145179
algorithm: HashAlgorithm,
146180
digest: DigestFormat,
147181
) -> Result<String> {
182+
#[allow(deprecated)]
148183
let signature = match algorithm {
149184
HashAlgorithm::Sha256 => hmac_sha256(message.as_bytes(), secret.as_bytes()),
150185
HashAlgorithm::Sha512 => hmac_sha512(message.as_bytes(), secret.as_bytes()),
@@ -237,6 +272,7 @@ fn hmac_md5(data: &[u8], secret: &[u8]) -> Vec<u8> {
237272
/// let hashed = hash("test", HashAlgorithm::Sha256, DigestFormat::Hex).unwrap();
238273
/// ```
239274
pub fn hash(data: &str, algorithm: HashAlgorithm, digest: DigestFormat) -> Result<String> {
275+
#[allow(deprecated)]
240276
let hash_bytes = match algorithm {
241277
HashAlgorithm::Sha256 => hash_sha256(data.as_bytes()),
242278
HashAlgorithm::Sha512 => hash_sha512(data.as_bytes()),
@@ -386,7 +422,7 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
386422
///
387423
/// # Arguments
388424
/// * `payload` - JWT payload as JSON object
389-
/// * `secret` - Secret key for signing
425+
/// * `secret` - Secret key for signing (must be at least 32 characters for security)
390426
/// * `algorithm` - Hash algorithm for HMAC (supports Sha256, Sha384, Sha512)
391427
/// * `header_options` - Optional additional header fields
392428
///
@@ -398,6 +434,14 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
398434
/// - JSON serialization fails
399435
/// - Signing fails
400436
/// - Unsupported algorithm is provided (only HS256, HS384, HS512 are supported)
437+
/// - Secret key is less than 32 characters (security requirement)
438+
///
439+
/// # Security
440+
///
441+
/// **Minimum secret length: 32 characters**
442+
///
443+
/// Short secrets are vulnerable to brute-force attacks. The minimum 32-character
444+
/// requirement ensures sufficient entropy for secure HMAC signatures.
401445
///
402446
/// # Examples
403447
/// ```
@@ -409,18 +453,40 @@ pub fn eddsa_sign(data: &str, secret_key: &[u8]) -> Result<String> {
409453
/// "exp": 1234567890
410454
/// });
411455
///
412-
/// // Using HS256
413-
/// let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
456+
/// // Using HS256 with a strong secret (at least 32 characters)
457+
/// let token = jwt_sign(
458+
/// &payload,
459+
/// "my-very-secure-secret-key-at-least-32-chars",
460+
/// HashAlgorithm::Sha256,
461+
/// None
462+
/// ).unwrap();
414463
///
415464
/// // Using HS512
416-
/// let token_512 = jwt_sign(&payload, "secret", HashAlgorithm::Sha512, None).unwrap();
465+
/// let token_512 = jwt_sign(
466+
/// &payload,
467+
/// "my-very-secure-secret-key-at-least-32-chars",
468+
/// HashAlgorithm::Sha512,
469+
/// None
470+
/// ).unwrap();
417471
/// ```
418472
pub fn jwt_sign(
419473
payload: &serde_json::Value,
420474
secret: &str,
421475
algorithm: HashAlgorithm,
422476
header_options: Option<serde_json::Map<String, serde_json::Value>>,
423477
) -> Result<String> {
478+
// Validate secret key strength (minimum 32 characters for security)
479+
const MIN_SECRET_LENGTH: usize = 32;
480+
481+
if secret.len() < MIN_SECRET_LENGTH {
482+
return Err(Error::invalid_argument(format!(
483+
"JWT secret must be at least {MIN_SECRET_LENGTH} characters for security. \
484+
Provided: {} characters. \
485+
Use a longer secret to protect against brute-force attacks.",
486+
secret.len()
487+
)));
488+
}
489+
424490
// Map HashAlgorithm to JWT algorithm string
425491
let alg_str = match algorithm {
426492
HashAlgorithm::Sha256 => "HS256",
@@ -552,7 +618,7 @@ mod tests {
552618
#[test]
553619
fn test_hash_keccak() {
554620
let result = hash("test", HashAlgorithm::Keccak, DigestFormat::Hex).unwrap();
555-
assert_eq!(result.len(), 64); // Keccak256输出32字节=64个hex字符
621+
assert_eq!(result.len(), 64); // Keccak256 outputs 32 bytes = 64 hex characters
556622
}
557623

558624
#[test]
@@ -578,9 +644,16 @@ mod tests {
578644
"exp": 1234567890
579645
});
580646

581-
let token = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
647+
// Use a strong secret (at least 32 characters)
648+
let token = jwt_sign(
649+
&payload,
650+
"my-very-secure-secret-key-at-least-32-chars",
651+
HashAlgorithm::Sha256,
652+
None,
653+
)
654+
.unwrap();
582655

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

@@ -599,20 +672,22 @@ mod tests {
599672
"exp": 1234567890
600673
});
601674

675+
let strong_secret = "my-very-secure-secret-key-at-least-32-chars";
676+
602677
// Test HS256
603-
let token_256 = jwt_sign(&payload, "secret", HashAlgorithm::Sha256, None).unwrap();
678+
let token_256 = jwt_sign(&payload, strong_secret, HashAlgorithm::Sha256, None).unwrap();
604679
let parts_256: Vec<&str> = token_256.split('.').collect();
605680
let header_256 = String::from_utf8(base64url_decode(parts_256[0]).unwrap()).unwrap();
606681
assert!(header_256.contains("HS256"));
607682

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

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

630705
// SHA1 is not supported for JWT
631-
let result = jwt_sign(&payload, "secret", HashAlgorithm::Sha1, None);
706+
let result = jwt_sign(
707+
&payload,
708+
"my-very-secure-secret-key-at-least-32-chars",
709+
HashAlgorithm::Sha1,
710+
None,
711+
);
632712
assert!(result.is_err());
633713
assert!(result.unwrap_err().to_string().contains("does not support"));
634714

635715
// MD5 is not supported for JWT
636-
let result = jwt_sign(&payload, "secret", HashAlgorithm::Md5, None);
716+
let result = jwt_sign(
717+
&payload,
718+
"my-very-secure-secret-key-at-least-32-chars",
719+
HashAlgorithm::Md5,
720+
None,
721+
);
637722
assert!(result.is_err());
638723

639724
// Keccak is not supported for JWT
640-
let result = jwt_sign(&payload, "secret", HashAlgorithm::Keccak, None);
725+
let result = jwt_sign(
726+
&payload,
727+
"my-very-secure-secret-key-at-least-32-chars",
728+
HashAlgorithm::Keccak,
729+
None,
730+
);
641731
assert!(result.is_err());
642732
}
643733

@@ -659,18 +749,115 @@ mod tests {
659749
assert_eq!(DigestFormat::from_str("hex"), DigestFormat::Hex);
660750
assert_eq!(DigestFormat::from_str("base64"), DigestFormat::Base64);
661751
assert_eq!(DigestFormat::from_str("binary"), DigestFormat::Binary);
662-
assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // 默认
752+
assert_eq!(DigestFormat::from_str("unknown"), DigestFormat::Hex); // defaults to Hex
663753
}
664754

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

671761
#[test]
672762
fn test_hash_md5() {
673763
let result = hash("test", HashAlgorithm::Md5, DigestFormat::Hex).unwrap();
674-
assert_eq!(result.len(), 32); // MD5输出16字节=32个hex字符
764+
assert_eq!(result.len(), 32); // MD5 outputs 16 bytes = 32 hex characters
765+
}
766+
767+
#[test]
768+
fn test_jwt_sign_weak_secret_rejected() {
769+
use serde_json::json;
770+
771+
let payload = json!({
772+
"user_id": "123",
773+
"exp": 1234567890
774+
});
775+
776+
// Test various weak secrets (less than 32 characters)
777+
let weak_secrets = vec![
778+
"", // empty
779+
"a", // 1 character
780+
"short", // 5 characters
781+
"this-is-still-too-short-123", // 31 characters (just below threshold)
782+
];
783+
784+
for weak_secret in weak_secrets {
785+
let result = jwt_sign(&payload, weak_secret, HashAlgorithm::Sha256, None);
786+
assert!(
787+
result.is_err(),
788+
"Secret with {} characters should be rejected",
789+
weak_secret.len()
790+
);
791+
792+
if let Err(e) = result {
793+
let error_msg = e.to_string();
794+
assert!(
795+
error_msg.contains("32 characters"),
796+
"Error message should mention 32 character requirement"
797+
);
798+
assert!(
799+
error_msg.contains("security"),
800+
"Error message should mention security"
801+
);
802+
}
803+
}
804+
}
805+
806+
#[test]
807+
fn test_jwt_sign_minimum_valid_secret() {
808+
use serde_json::json;
809+
810+
let payload = json!({
811+
"user_id": "123",
812+
"exp": 1234567890
813+
});
814+
815+
// Test exactly 32 characters (should succeed)
816+
let exactly_32_chars = "12345678901234567890123456789012"; // exactly 32 chars
817+
let result = jwt_sign(&payload, exactly_32_chars, HashAlgorithm::Sha256, None);
818+
assert!(
819+
result.is_ok(),
820+
"Secret with exactly 32 characters should be accepted"
821+
);
822+
823+
// Test 33 characters (should succeed)
824+
let exactly_33_chars = "123456789012345678901234567890123"; // exactly 33 chars
825+
let result = jwt_sign(&payload, exactly_33_chars, HashAlgorithm::Sha256, None);
826+
assert!(
827+
result.is_ok(),
828+
"Secret with 33 characters should be accepted"
829+
);
830+
}
831+
832+
#[test]
833+
fn test_jwt_sign_with_custom_header_and_strong_secret() {
834+
use serde_json::json;
835+
836+
let payload = json!({
837+
"user_id": "123",
838+
"exp": 1234567890
839+
});
840+
841+
let mut custom_header = serde_json::Map::new();
842+
custom_header.insert(
843+
"kid".to_string(),
844+
serde_json::Value::String("key-123".to_string()),
845+
);
846+
847+
let strong_secret = "my-very-secure-secret-key-at-least-32-chars";
848+
let token = jwt_sign(
849+
&payload,
850+
strong_secret,
851+
HashAlgorithm::Sha256,
852+
Some(custom_header),
853+
)
854+
.unwrap();
855+
856+
let parts: Vec<&str> = token.split('.').collect();
857+
assert_eq!(parts.len(), 3);
858+
859+
let header_bytes = base64url_decode(parts[0]).unwrap();
860+
let header_str = String::from_utf8(header_bytes).unwrap();
861+
assert!(header_str.contains("\"kid\":\"key-123\""));
675862
}
676863
}

‎ccxt-core/src/base_exchange/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ impl BaseExchange {
9494
..crate::retry_strategy::RetryConfig::default()
9595
}),
9696
max_response_size: 10 * 1024 * 1024, // 10MB default
97+
max_request_size: 10 * 1024 * 1024, // 10MB default
9798
circuit_breaker: None, // Disabled by default for backward compatibility
9899
pool_max_idle_per_host: 10, // Default: 10 idle connections per host
99100
pool_idle_timeout: Duration::from_secs(90), // Default: 90 seconds

0 commit comments

Comments
 (0)