From 5c1cd9f9fca86cfb85d14ab46cb9cdc1f770858f Mon Sep 17 00:00:00 2001
From: centra
Date: Fri, 12 Dec 2025 06:48:15 +0900
Subject: [PATCH] test(lightningstor): Add comprehensive S3 auth security tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Added 9 security tests to verify SigV4 authentication hardening:
- Invalid/malformed auth header rejection
- Signature changes with different secret keys
- Signature changes with different request components (body, URI, headers, query params)
- Credential lookup for unknown keys
- Empty credentials fallback
- Malformed S3_CREDENTIALS handling
Result: 19/19 auth tests passing (10 original + 9 new security tests)
Task: T058.S3 Complete
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5
---
docs/por/POR.md | 18 +-
.../lightningstor-server/src/s3/auth.rs | 330 +++++++++++++++++-
2 files changed, 338 insertions(+), 10 deletions(-)
diff --git a/docs/por/POR.md b/docs/por/POR.md
index e87df54..6467527 100644
--- a/docs/por/POR.md
+++ b/docs/por/POR.md
@@ -11,7 +11,7 @@
## Deliverables (top-level)
> **Naming (2025-12-11):** Nightlight→NightLight, PrismNET→PrismNET, PlasmaCloud→PhotonCloud
- chainfire - cluster KVS lib - crates/chainfire-* - operational (DELETE fixed; 2/3 integration tests pass, 1 flaky)
-- iam (aegis) - IAM platform - iam/crates/* - TESTS FAIL (private module visibility issue)
+- iam (aegis) - IAM platform - iam/crates/* - operational (visibility fixed)
- flaredb - DBaaS KVS - flaredb/crates/* - operational
- plasmavmc - VM infra - plasmavmc/crates/* - operational (T054 Ops Planned)
- lightningstor - object storage - lightningstor/crates/* - operational (T047 Complete, T058 Auth Planned)
@@ -24,7 +24,7 @@
- **creditservice** - credit/quota management - creditservice/crates/* - operational (fixed - uses CAS instead of txn)
## MVP Milestones
-- **MVP-Alpha (PARTIAL)**: All 12 infrastructure components operational + specs | Status: 2/3 audit issues fixed | 2025-12-12 | creditservice✓ chainfire✓ iam pending (1-line fix)
+- **MVP-Alpha (ACHIEVED)**: All 12 infrastructure components operational + specs | Status: T059 complete (creditservice✓ chainfire✓ iam✓) | 2025-12-12
- **MVP-Beta (ACHIEVED)**: E2E tenant path functional + FlareDB metadata unified | Gate: T023 complete ✓ | 2025-12-09
- **MVP-K8s (ACHIEVED)**: K8s hosting with multi-tenant isolation | Gate: T025 S6.1 complete ✓ | 2025-12-09 | IAM auth + PrismNET CNI
- MVP-Production (future): HA, monitoring, production hardening | Gate: post-K8s
@@ -44,10 +44,10 @@
## Roadmap (Now/Next/Later)
- **Now (<= 2 weeks):**
- - **T059 ACTIVE (P0)**: Critical Audit Fix — creditservice compile, chainfire tests, iam tests (BLOCKS MVP-Alpha)
- - **T039 BLOCKED**: Production Deployment — Blocked by T059
- - **T058 PLANNED**: LightningSTOR S3 Auth Hardening — Fix SigV4 Auth for Production (P0)
- - **T052 BLOCKED**: CreditService Persistence — Blocked by T059.S1 (creditservice must compile first)
+ - **T058 ACTIVE (P0)**: LightningSTOR S3 Auth Hardening — S1 SigV4 ✓, S2 IAM ✓, S3 Security Tests (in progress)
+ - **T059 COMPLETE**: Critical Audit Fix — S1 creditservice ✓, S2 chainfire ✓, S3 iam ✓ (MVP-Alpha ACHIEVED)
+ - **T039 ACTIVE**: Production Deployment — Unblocked by T059 completion; blocked by T058 completion
+ - **T052 ACTIVE**: CreditService Persistence — Unblocked by T059.S1
- **T053 PLANNED**: ChainFire Core Finalization — Remove OpenRaft, finish Gossip, clean debt
- **T054 PLANNED**: PlasmaVMC Ops — Hotplug, Reset, Update, Watch
- **T055 PLANNED**: FiberLB Features — Maglev, L7, BGP
@@ -55,9 +55,7 @@
- **T057 PLANNED**: k8shost Resource Management — IPAM & Tenant-aware Scheduler
- **T051 ACTIVE**: FiberLB Integration — S1-S3 complete; S4 Pending
- **T050 ACTIVE**: REST API — S1 Design complete; S2-S8 pending
- - **T047 COMPLETE**: LightningSTOR S3 Compatibility — AWS CLI working (Auth bypassed)
- - **T042 BROKEN**: CreditService (MVP) — Code doesn't compile (missing chainfire_client.txn API)
- - **MVP-Alpha STATUS**: BLOCKED — 3 critical failures (creditservice, chainfire, iam)
+ - **T047 COMPLETE**: LightningSTOR S3 Compatibility — AWS CLI working (Auth bypassed - fixed in T058)
- **Next (2-4 weeks) — Integration & Enhancement:**
- **SDK**: gRPCクライアント一貫性 (T048)
@@ -117,6 +115,8 @@
- Falsify before expand; one decidable next step; stop with pride when wrong; Done = evidence.
## Maintenance & Change Log (append-only, one line each)
+- 2025-12-12 06:46 | peerA | T039 UNBLOCKED: User approved QEMU+VDE VM deployment instead of waiting for real hardware. Delegated to PeerB after T058.S2.
+- 2025-12-12 06:41 | peerA | T059.S3 COMPLETE: iam visibility fixed (pub mod). MVP-Alpha ACHIEVED - all 3 audit issues resolved.
- 2025-12-12 06:39 | peerA | T060 CREATED: IAM Credential Service. T058.S2 Option B approved (env var MVP); proper IAM solution deferred to T060. Unblocks T039.
- 2025-12-12 06:37 | peerA | T059.S1+S2 COMPLETE: creditservice✓ chainfire✓. DELETE fix verified (2/3 tests pass, 1 flaky timing issue). iam S3 pending (1-line pub mod fix). PeerB pivoting to T058.S2.
- 2025-12-12 06:35 | peerA | T059.S1 COMPLETE: PeerB fixed creditservice (CAS instead of txn). Foreman's "false alarm" claim WRONG - ran --lib only, not integration tests. chainfire/iam integration tests still fail. Approved Option A for DELETE fix.
diff --git a/lightningstor/crates/lightningstor-server/src/s3/auth.rs b/lightningstor/crates/lightningstor-server/src/s3/auth.rs
index d512da3..c858ca0 100644
--- a/lightningstor/crates/lightningstor-server/src/s3/auth.rs
+++ b/lightningstor/crates/lightningstor-server/src/s3/auth.rs
@@ -671,7 +671,7 @@ mod tests {
build_canonical_request(method, uri, &headers, &body, signed_headers).unwrap();
// Build string to sign
- let string_to_sign = build_string_to_sign(amz_date, credential_scope, &canonical_request);
+ let _string_to_sign = build_string_to_sign(amz_date, credential_scope, &canonical_request);
// Compute signature
let signature = compute_sigv4_signature(
@@ -706,4 +706,332 @@ mod tests {
assert_eq!(signature, signature2);
assert_eq!(signature.len(), 64); // SHA256 hex = 64 chars
}
+
+ // =============================================================================
+ // Security Tests
+ // =============================================================================
+
+ #[test]
+ fn test_security_invalid_auth_header_format() {
+ // Missing Credential field
+ let malformed1 = "AWS4-HMAC-SHA256 SignedHeaders=host, Signature=abc123";
+ assert!(parse_auth_header(malformed1).is_err());
+
+ // Missing SignedHeaders field
+ let malformed2 = "AWS4-HMAC-SHA256 Credential=KEY/scope, Signature=abc123";
+ assert!(parse_auth_header(malformed2).is_err());
+
+ // Missing Signature field
+ let malformed3 = "AWS4-HMAC-SHA256 Credential=KEY/scope, SignedHeaders=host";
+ assert!(parse_auth_header(malformed3).is_err());
+
+ // Wrong algorithm
+ let malformed4 = "AWS4-HMAC-SHA512 Credential=KEY/scope, SignedHeaders=host, Signature=abc";
+ assert!(parse_auth_header(malformed4).is_err());
+
+ // Empty string
+ assert!(parse_auth_header("").is_err());
+
+ // Random garbage
+ assert!(parse_auth_header("not-an-auth-header").is_err());
+ }
+
+ #[test]
+ fn test_security_signature_changes_with_secret_key() {
+ let method = "GET";
+ let uri = "/test-bucket/object";
+ let amz_date = "20231201T000000Z";
+ let credential_scope = "20231201/us-east-1/s3/aws4_request";
+ let signed_headers = "host;x-amz-date";
+
+ let mut headers = HeaderMap::new();
+ headers.insert("host", HeaderValue::from_static("s3.amazonaws.com"));
+ headers.insert("x-amz-date", HeaderValue::from_static("20231201T000000Z"));
+
+ let body = Bytes::new();
+
+ // Compute signature with first secret key
+ let sig1 = compute_sigv4_signature(
+ "secret1",
+ method,
+ uri,
+ &headers,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Compute signature with different secret key
+ let sig2 = compute_sigv4_signature(
+ "secret2",
+ method,
+ uri,
+ &headers,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signatures MUST be different
+ assert_ne!(sig1, sig2, "Signatures should differ with different secret keys");
+ }
+
+ #[test]
+ fn test_security_signature_changes_with_body() {
+ let secret_key = "test-secret-key";
+ let method = "PUT";
+ let uri = "/test-bucket/object";
+ let amz_date = "20231201T000000Z";
+ let credential_scope = "20231201/us-east-1/s3/aws4_request";
+ let signed_headers = "host;x-amz-date";
+
+ let mut headers = HeaderMap::new();
+ headers.insert("host", HeaderValue::from_static("s3.amazonaws.com"));
+ headers.insert("x-amz-date", HeaderValue::from_static("20231201T000000Z"));
+
+ // Signature with body1
+ let body1 = Bytes::from("original content");
+ let sig1 = compute_sigv4_signature(
+ secret_key,
+ method,
+ uri,
+ &headers,
+ amz_date,
+ &body1,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signature with modified body
+ let body2 = Bytes::from("modified content");
+ let sig2 = compute_sigv4_signature(
+ secret_key,
+ method,
+ uri,
+ &headers,
+ amz_date,
+ &body2,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signatures MUST be different
+ assert_ne!(sig1, sig2, "Signatures should differ with different bodies");
+ }
+
+ #[test]
+ fn test_security_signature_changes_with_uri() {
+ let secret_key = "test-secret-key";
+ let method = "GET";
+ let amz_date = "20231201T000000Z";
+ let credential_scope = "20231201/us-east-1/s3/aws4_request";
+ let signed_headers = "host;x-amz-date";
+
+ let mut headers = HeaderMap::new();
+ headers.insert("host", HeaderValue::from_static("s3.amazonaws.com"));
+ headers.insert("x-amz-date", HeaderValue::from_static("20231201T000000Z"));
+
+ let body = Bytes::new();
+
+ // Signature with uri1
+ let sig1 = compute_sigv4_signature(
+ secret_key,
+ method,
+ "/test-bucket/object1",
+ &headers,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signature with different URI
+ let sig2 = compute_sigv4_signature(
+ secret_key,
+ method,
+ "/test-bucket/object2",
+ &headers,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signatures MUST be different
+ assert_ne!(sig1, sig2, "Signatures should differ with different URIs");
+ }
+
+ #[test]
+ fn test_security_signature_changes_with_headers() {
+ let secret_key = "test-secret-key";
+ let method = "GET";
+ let uri = "/test-bucket/object";
+ let amz_date = "20231201T000000Z";
+ let credential_scope = "20231201/us-east-1/s3/aws4_request";
+ let signed_headers = "host;x-amz-content-sha256;x-amz-date";
+
+ let mut headers1 = HeaderMap::new();
+ headers1.insert("host", HeaderValue::from_static("s3.amazonaws.com"));
+ headers1.insert("x-amz-date", HeaderValue::from_static("20231201T000000Z"));
+ headers1.insert("x-amz-content-sha256", HeaderValue::from_static("hash1"));
+
+ let mut headers2 = HeaderMap::new();
+ headers2.insert("host", HeaderValue::from_static("s3.amazonaws.com"));
+ headers2.insert("x-amz-date", HeaderValue::from_static("20231201T000000Z"));
+ headers2.insert("x-amz-content-sha256", HeaderValue::from_static("hash2"));
+
+ let body = Bytes::new();
+
+ let sig1 = compute_sigv4_signature(
+ secret_key,
+ method,
+ uri,
+ &headers1,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ let sig2 = compute_sigv4_signature(
+ secret_key,
+ method,
+ uri,
+ &headers2,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signatures MUST be different
+ assert_ne!(sig1, sig2, "Signatures should differ with different header values");
+ }
+
+ #[test]
+ fn test_security_signature_changes_with_query_params() {
+ let secret_key = "test-secret-key";
+ let method = "GET";
+ let amz_date = "20231201T000000Z";
+ let credential_scope = "20231201/us-east-1/s3/aws4_request";
+ let signed_headers = "host;x-amz-date";
+
+ let mut headers = HeaderMap::new();
+ headers.insert("host", HeaderValue::from_static("s3.amazonaws.com"));
+ headers.insert("x-amz-date", HeaderValue::from_static("20231201T000000Z"));
+
+ let body = Bytes::new();
+
+ // URI with query param
+ let sig1 = compute_sigv4_signature(
+ secret_key,
+ method,
+ "/test-bucket/object?prefix=foo",
+ &headers,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // URI with different query param
+ let sig2 = compute_sigv4_signature(
+ secret_key,
+ method,
+ "/test-bucket/object?prefix=bar",
+ &headers,
+ amz_date,
+ &body,
+ credential_scope,
+ signed_headers,
+ "us-east-1",
+ "s3",
+ )
+ .unwrap();
+
+ // Signatures MUST be different
+ assert_ne!(sig1, sig2, "Signatures should differ with different query parameters");
+ }
+
+ #[test]
+ fn test_security_credential_lookup_unknown_key() {
+ // Test that unknown access keys return the correct result
+ std::env::remove_var("S3_CREDENTIALS");
+ std::env::set_var("S3_ACCESS_KEY_ID", "known_key");
+ std::env::set_var("S3_SECRET_KEY", "known_secret");
+
+ let client = IamClient::new();
+
+ // Known key should be found in credentials map
+ assert_eq!(client.credentials.get("known_key"), Some(&"known_secret".to_string()));
+
+ // Unknown key should not be found
+ assert_eq!(client.credentials.get("unknown_key"), None);
+
+ std::env::remove_var("S3_ACCESS_KEY_ID");
+ std::env::remove_var("S3_SECRET_KEY");
+ }
+
+ #[test]
+ fn test_security_empty_credentials() {
+ // Test that IamClient falls back to dummy credentials when none provided
+ std::env::remove_var("S3_CREDENTIALS");
+ std::env::remove_var("S3_ACCESS_KEY_ID");
+ std::env::remove_var("S3_SECRET_KEY");
+
+ let client = IamClient::new();
+
+ // Should have dummy AWS example credentials (for testing only)
+ assert_eq!(client.credentials.len(), 1);
+ assert!(client.credentials.contains_key("AKIAIOSFODNN7EXAMPLE"));
+ }
+
+ #[test]
+ fn test_security_malformed_s3_credentials_env() {
+ // Test that malformed S3_CREDENTIALS are handled gracefully
+
+ // Missing colon separator
+ std::env::set_var("S3_CREDENTIALS", "key1_secret1,key2:secret2");
+ let client = IamClient::new();
+ // Should only parse the valid pair (key2:secret2)
+ assert_eq!(client.credentials.len(), 1);
+ assert!(client.credentials.contains_key("key2"));
+
+ // Empty pairs
+ std::env::set_var("S3_CREDENTIALS", "key1:secret1,,key2:secret2");
+ let client2 = IamClient::new();
+ // Should parse both valid pairs, skip empty
+ assert_eq!(client2.credentials.len(), 2);
+
+ std::env::remove_var("S3_CREDENTIALS");
+ }
}
\ No newline at end of file