Skip to content

Commit edf9a38

Browse files
benthecarmanclaude
andcommitted
Add key ID to HMAC auth header for O(1) key lookup
Change the auth header format from `HMAC <timestamp>:<hmac>` to `HMAC <key_id>:<timestamp>:<hmac>` where key_id is the first 8 bytes of SHA256(api_key), hex-encoded (16 chars). The server validates the key_id before computing the HMAC, enabling O(1) key lookup when multiple API keys are supported. The client precomputes the key_id at construction time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9b33ccd commit edf9a38

2 files changed

Lines changed: 79 additions & 19 deletions

File tree

ldk-server-client/src/client.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub struct LdkServerClient {
5959
base_url: String,
6060
client: Client,
6161
api_key: String,
62+
key_id: String,
6263
}
6364

6465
impl LdkServerClient {
@@ -77,11 +78,19 @@ impl LdkServerClient {
7778
.build()
7879
.map_err(|e| format!("Failed to build HTTP client: {e}"))?;
7980

80-
Ok(Self { base_url, client, api_key })
81+
// Compute key_id as first 8 bytes of SHA256(api_key), hex-encoded (16 chars)
82+
let hash = sha256::Hash::hash(api_key.as_bytes());
83+
let key_id = hash[..8].iter().fold(String::with_capacity(16), |mut acc, b| {
84+
use std::fmt::Write;
85+
let _ = write!(acc, "{:02x}", b);
86+
acc
87+
});
88+
89+
Ok(Self { base_url, client, api_key, key_id })
8190
}
8291

8392
/// Computes the HMAC-SHA256 authentication header value.
84-
/// Format: "HMAC <timestamp>:<hmac_hex>"
93+
/// Format: "HMAC <key_id>:<timestamp>:<hmac_hex>"
8594
fn compute_auth_header(&self, body: &[u8]) -> String {
8695
let timestamp = SystemTime::now()
8796
.duration_since(UNIX_EPOCH)
@@ -94,7 +103,7 @@ impl LdkServerClient {
94103
hmac_engine.input(body);
95104
let hmac_result = Hmac::<sha256::Hash>::from_engine(hmac_engine);
96105

97-
format!("HMAC {}:{}", timestamp, hmac_result)
106+
format!("HMAC {}:{}:{}", self.key_id, timestamp, hmac_result)
98107
}
99108

100109
/// Retrieve the latest node info like `node_id`, `current_best_block` etc.

ldk-server/src/service.rs

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,27 +84,48 @@ const AUTH_TIMESTAMP_TOLERANCE_SECS: u64 = 60;
8484

8585
#[derive(Debug, Clone)]
8686
pub(crate) struct AuthParams {
87+
key_id: String,
8788
timestamp: u64,
8889
hmac_hex: String,
8990
}
9091

92+
/// Computes the key_id for an API key: first 8 bytes of SHA256(api_key), hex-encoded (16 chars).
93+
fn compute_key_id(api_key: &str) -> String {
94+
let hash = sha256::Hash::hash(api_key.as_bytes());
95+
hash[..8].iter().fold(String::with_capacity(16), |mut acc, b| {
96+
use std::fmt::Write;
97+
let _ = write!(acc, "{:02x}", b);
98+
acc
99+
})
100+
}
101+
91102
/// Extracts authentication parameters from request headers.
92-
/// Returns (timestamp, hmac_hex) if valid format, or error.
103+
/// Returns (key_id, timestamp, hmac_hex) if valid format, or error.
93104
fn extract_auth_params<B>(req: &Request<B>) -> Result<AuthParams, LdkServerError> {
94105
let auth_header = req
95106
.headers()
96107
.get("X-Auth")
97108
.and_then(|v| v.to_str().ok())
98109
.ok_or_else(|| LdkServerError::new(AuthError, "Missing X-Auth header"))?;
99110

100-
// Format: "HMAC <timestamp>:<hmac_hex>"
111+
// Format: "HMAC <key_id>:<timestamp>:<hmac_hex>"
101112
let auth_data = auth_header
102113
.strip_prefix("HMAC ")
103114
.ok_or_else(|| LdkServerError::new(AuthError, "Invalid X-Auth header format"))?;
104115

105-
let (timestamp_str, hmac_hex) = auth_data
106-
.split_once(':')
107-
.ok_or_else(|| LdkServerError::new(AuthError, "Invalid X-Auth header format"))?;
116+
let parts: Vec<&str> = auth_data.splitn(3, ':').collect();
117+
if parts.len() != 3 {
118+
return Err(LdkServerError::new(AuthError, "Invalid X-Auth header format"));
119+
}
120+
121+
let key_id = parts[0];
122+
let timestamp_str = parts[1];
123+
let hmac_hex = parts[2];
124+
125+
// Validate key_id is 16 hex chars
126+
if key_id.len() != 16 || !key_id.chars().all(|c| c.is_ascii_hexdigit()) {
127+
return Err(LdkServerError::new(AuthError, "Invalid key_id in X-Auth header"));
128+
}
108129

109130
let timestamp = timestamp_str
110131
.parse::<u64>()
@@ -115,13 +136,19 @@ fn extract_auth_params<B>(req: &Request<B>) -> Result<AuthParams, LdkServerError
115136
return Err(LdkServerError::new(AuthError, "Invalid HMAC in X-Auth header"));
116137
}
117138

118-
Ok(AuthParams { timestamp, hmac_hex: hmac_hex.to_string() })
139+
Ok(AuthParams { key_id: key_id.to_string(), timestamp, hmac_hex: hmac_hex.to_string() })
119140
}
120141

121142
/// Validates the HMAC authentication after the request body has been read.
122143
fn validate_hmac_auth(
123-
timestamp: u64, provided_hmac_hex: &str, body: &[u8], api_key: &str,
144+
key_id: &str, timestamp: u64, provided_hmac_hex: &str, body: &[u8], api_key: &str,
124145
) -> Result<(), LdkServerError> {
146+
// Verify the key_id matches the api_key
147+
let expected_key_id = compute_key_id(api_key);
148+
if key_id != expected_key_id {
149+
return Err(LdkServerError::new(AuthError, "Invalid credentials"));
150+
}
151+
125152
// Validate timestamp is within acceptable window
126153
let now = std::time::SystemTime::now()
127154
.duration_since(std::time::UNIX_EPOCH)
@@ -406,9 +433,13 @@ async fn handle_request<
406433
};
407434

408435
// Validate HMAC authentication with the request body
409-
if let Err(e) =
410-
validate_hmac_auth(auth_params.timestamp, &auth_params.hmac_hex, &bytes, &api_key)
411-
{
436+
if let Err(e) = validate_hmac_auth(
437+
&auth_params.key_id,
438+
auth_params.timestamp,
439+
&auth_params.hmac_hex,
440+
&bytes,
441+
&api_key,
442+
) {
412443
let (error_response, status_code) = to_error_response(e);
413444
return Ok(Response::builder()
414445
.status(status_code)
@@ -468,13 +499,15 @@ mod tests {
468499
let timestamp =
469500
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
470501
let hmac = "8f5a33c2c68fb253899a588308fd13dcaf162d2788966a1fb6cc3aa2e0c51a93";
471-
let auth_header = format!("HMAC {timestamp}:{hmac}");
502+
let key_id = "abcdef0123456789";
503+
let auth_header = format!("HMAC {key_id}:{timestamp}:{hmac}");
472504

473505
let req = create_test_request(Some(auth_header));
474506

475507
let result = extract_auth_params(&req);
476508
assert!(result.is_ok());
477-
let AuthParams { timestamp: ts, hmac_hex } = result.unwrap();
509+
let AuthParams { key_id: kid, timestamp: ts, hmac_hex } = result.unwrap();
510+
assert_eq!(kid, key_id);
478511
assert_eq!(ts, timestamp);
479512
assert_eq!(hmac_hex, hmac);
480513
}
@@ -501,47 +534,65 @@ mod tests {
501534
#[test]
502535
fn test_validate_hmac_auth_success() {
503536
let api_key = "test_api_key".to_string();
537+
let key_id = compute_key_id(&api_key);
504538
let body = b"test request body";
505539
let timestamp =
506540
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
507541
let hmac = compute_hmac(&api_key, timestamp, body);
508542

509-
let result = validate_hmac_auth(timestamp, &hmac, body, &api_key);
543+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, body, &api_key);
510544
assert!(result.is_ok());
511545
}
512546

513547
#[test]
514548
fn test_validate_hmac_auth_wrong_key() {
515549
let api_key = "test_api_key".to_string();
550+
let key_id = compute_key_id(&api_key);
516551
let body = b"test request body";
517552
let timestamp =
518553
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
519554
// Compute HMAC with wrong key
520555
let hmac = compute_hmac("wrong_key", timestamp, body);
521556

522-
let result = validate_hmac_auth(timestamp, &hmac, body, &api_key);
557+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, body, &api_key);
558+
assert!(result.is_err());
559+
assert_eq!(result.unwrap_err().error_code, AuthError);
560+
}
561+
562+
#[test]
563+
fn test_validate_hmac_auth_wrong_key_id() {
564+
let api_key = "test_api_key".to_string();
565+
let wrong_key_id = "0000000000000000";
566+
let body = b"test request body";
567+
let timestamp =
568+
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
569+
let hmac = compute_hmac(&api_key, timestamp, body);
570+
571+
let result = validate_hmac_auth(wrong_key_id, timestamp, &hmac, body, &api_key);
523572
assert!(result.is_err());
524573
assert_eq!(result.unwrap_err().error_code, AuthError);
525574
}
526575

527576
#[test]
528577
fn test_validate_hmac_auth_expired_timestamp() {
529578
let api_key = "test_api_key".to_string();
579+
let key_id = compute_key_id(&api_key);
530580
let body = b"test request body";
531581
// Use a timestamp from 10 minutes ago
532582
let timestamp =
533583
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
534584
- 600;
535585
let hmac = compute_hmac(&api_key, timestamp, body);
536586

537-
let result = validate_hmac_auth(timestamp, &hmac, body, &api_key);
587+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, body, &api_key);
538588
assert!(result.is_err());
539589
assert_eq!(result.unwrap_err().error_code, AuthError);
540590
}
541591

542592
#[test]
543593
fn test_validate_hmac_auth_tampered_body() {
544594
let api_key = "test_api_key".to_string();
595+
let key_id = compute_key_id(&api_key);
545596
let original_body = b"test request body";
546597
let tampered_body = b"tampered body";
547598
let timestamp =
@@ -550,7 +601,7 @@ mod tests {
550601
let hmac = compute_hmac(&api_key, timestamp, original_body);
551602

552603
// Try to validate with tampered body
553-
let result = validate_hmac_auth(timestamp, &hmac, tampered_body, &api_key);
604+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, tampered_body, &api_key);
554605
assert!(result.is_err());
555606
assert_eq!(result.unwrap_err().error_code, AuthError);
556607
}

0 commit comments

Comments
 (0)