Skip to content

Commit 451bf86

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 451bf86

2 files changed

Lines changed: 71 additions & 19 deletions

File tree

ldk-server-client/src/client.rs

Lines changed: 8 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,15 @@ 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().map(|b| format!("{:02x}", b)).collect::<String>();
84+
85+
Ok(Self { base_url, client, api_key, key_id })
8186
}
8287

8388
/// Computes the HMAC-SHA256 authentication header value.
84-
/// Format: "HMAC <timestamp>:<hmac_hex>"
89+
/// Format: "HMAC <key_id>:<timestamp>:<hmac_hex>"
8590
fn compute_auth_header(&self, body: &[u8]) -> String {
8691
let timestamp = SystemTime::now()
8792
.duration_since(UNIX_EPOCH)
@@ -94,7 +99,7 @@ impl LdkServerClient {
9499
hmac_engine.input(body);
95100
let hmac_result = Hmac::<sha256::Hash>::from_engine(hmac_engine);
96101

97-
format!("HMAC {}:{}", timestamp, hmac_result)
102+
format!("HMAC {}:{}:{}", self.key_id, timestamp, hmac_result)
98103
}
99104

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

ldk-server/src/service.rs

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,27 +84,44 @@ 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().map(|b| format!("{:02x}", b)).collect::<String>()
96+
}
97+
9198
/// Extracts authentication parameters from request headers.
92-
/// Returns (timestamp, hmac_hex) if valid format, or error.
99+
/// Returns (key_id, timestamp, hmac_hex) if valid format, or error.
93100
fn extract_auth_params<B>(req: &Request<B>) -> Result<AuthParams, LdkServerError> {
94101
let auth_header = req
95102
.headers()
96103
.get("X-Auth")
97104
.and_then(|v| v.to_str().ok())
98105
.ok_or_else(|| LdkServerError::new(AuthError, "Missing X-Auth header"))?;
99106

100-
// Format: "HMAC <timestamp>:<hmac_hex>"
107+
// Format: "HMAC <key_id>:<timestamp>:<hmac_hex>"
101108
let auth_data = auth_header
102109
.strip_prefix("HMAC ")
103110
.ok_or_else(|| LdkServerError::new(AuthError, "Invalid X-Auth header format"))?;
104111

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

109126
let timestamp = timestamp_str
110127
.parse::<u64>()
@@ -115,13 +132,19 @@ fn extract_auth_params<B>(req: &Request<B>) -> Result<AuthParams, LdkServerError
115132
return Err(LdkServerError::new(AuthError, "Invalid HMAC in X-Auth header"));
116133
}
117134

118-
Ok(AuthParams { timestamp, hmac_hex: hmac_hex.to_string() })
135+
Ok(AuthParams { key_id: key_id.to_string(), timestamp, hmac_hex: hmac_hex.to_string() })
119136
}
120137

121138
/// Validates the HMAC authentication after the request body has been read.
122139
fn validate_hmac_auth(
123-
timestamp: u64, provided_hmac_hex: &str, body: &[u8], api_key: &str,
140+
key_id: &str, timestamp: u64, provided_hmac_hex: &str, body: &[u8], api_key: &str,
124141
) -> Result<(), LdkServerError> {
142+
// Verify the key_id matches the api_key
143+
let expected_key_id = compute_key_id(api_key);
144+
if key_id != expected_key_id {
145+
return Err(LdkServerError::new(AuthError, "Invalid credentials"));
146+
}
147+
125148
// Validate timestamp is within acceptable window
126149
let now = std::time::SystemTime::now()
127150
.duration_since(std::time::UNIX_EPOCH)
@@ -406,9 +429,13 @@ async fn handle_request<
406429
};
407430

408431
// 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-
{
432+
if let Err(e) = validate_hmac_auth(
433+
&auth_params.key_id,
434+
auth_params.timestamp,
435+
&auth_params.hmac_hex,
436+
&bytes,
437+
&api_key,
438+
) {
412439
let (error_response, status_code) = to_error_response(e);
413440
return Ok(Response::builder()
414441
.status(status_code)
@@ -468,13 +495,15 @@ mod tests {
468495
let timestamp =
469496
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
470497
let hmac = "8f5a33c2c68fb253899a588308fd13dcaf162d2788966a1fb6cc3aa2e0c51a93";
471-
let auth_header = format!("HMAC {timestamp}:{hmac}");
498+
let key_id = "abcdef0123456789";
499+
let auth_header = format!("HMAC {key_id}:{timestamp}:{hmac}");
472500

473501
let req = create_test_request(Some(auth_header));
474502

475503
let result = extract_auth_params(&req);
476504
assert!(result.is_ok());
477-
let AuthParams { timestamp: ts, hmac_hex } = result.unwrap();
505+
let AuthParams { key_id: kid, timestamp: ts, hmac_hex } = result.unwrap();
506+
assert_eq!(kid, key_id);
478507
assert_eq!(ts, timestamp);
479508
assert_eq!(hmac_hex, hmac);
480509
}
@@ -501,47 +530,65 @@ mod tests {
501530
#[test]
502531
fn test_validate_hmac_auth_success() {
503532
let api_key = "test_api_key".to_string();
533+
let key_id = compute_key_id(&api_key);
504534
let body = b"test request body";
505535
let timestamp =
506536
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
507537
let hmac = compute_hmac(&api_key, timestamp, body);
508538

509-
let result = validate_hmac_auth(timestamp, &hmac, body, &api_key);
539+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, body, &api_key);
510540
assert!(result.is_ok());
511541
}
512542

513543
#[test]
514544
fn test_validate_hmac_auth_wrong_key() {
515545
let api_key = "test_api_key".to_string();
546+
let key_id = compute_key_id(&api_key);
516547
let body = b"test request body";
517548
let timestamp =
518549
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
519550
// Compute HMAC with wrong key
520551
let hmac = compute_hmac("wrong_key", timestamp, body);
521552

522-
let result = validate_hmac_auth(timestamp, &hmac, body, &api_key);
553+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, body, &api_key);
554+
assert!(result.is_err());
555+
assert_eq!(result.unwrap_err().error_code, AuthError);
556+
}
557+
558+
#[test]
559+
fn test_validate_hmac_auth_wrong_key_id() {
560+
let api_key = "test_api_key".to_string();
561+
let wrong_key_id = "0000000000000000";
562+
let body = b"test request body";
563+
let timestamp =
564+
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
565+
let hmac = compute_hmac(&api_key, timestamp, body);
566+
567+
let result = validate_hmac_auth(wrong_key_id, timestamp, &hmac, body, &api_key);
523568
assert!(result.is_err());
524569
assert_eq!(result.unwrap_err().error_code, AuthError);
525570
}
526571

527572
#[test]
528573
fn test_validate_hmac_auth_expired_timestamp() {
529574
let api_key = "test_api_key".to_string();
575+
let key_id = compute_key_id(&api_key);
530576
let body = b"test request body";
531577
// Use a timestamp from 10 minutes ago
532578
let timestamp =
533579
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
534580
- 600;
535581
let hmac = compute_hmac(&api_key, timestamp, body);
536582

537-
let result = validate_hmac_auth(timestamp, &hmac, body, &api_key);
583+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, body, &api_key);
538584
assert!(result.is_err());
539585
assert_eq!(result.unwrap_err().error_code, AuthError);
540586
}
541587

542588
#[test]
543589
fn test_validate_hmac_auth_tampered_body() {
544590
let api_key = "test_api_key".to_string();
591+
let key_id = compute_key_id(&api_key);
545592
let original_body = b"test request body";
546593
let tampered_body = b"tampered body";
547594
let timestamp =
@@ -550,7 +597,7 @@ mod tests {
550597
let hmac = compute_hmac(&api_key, timestamp, original_body);
551598

552599
// Try to validate with tampered body
553-
let result = validate_hmac_auth(timestamp, &hmac, tampered_body, &api_key);
600+
let result = validate_hmac_auth(&key_id, timestamp, &hmac, tampered_body, &api_key);
554601
assert!(result.is_err());
555602
assert_eq!(result.unwrap_err().error_code, AuthError);
556603
}

0 commit comments

Comments
 (0)