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