Skip to content

Commit 95ef1f8

Browse files
committed
test(sandbox): add SigV4 integration test against real Bedrock
Add a standalone integration test that exercises the full proxy-side re-signing flow: take a raw HTTP request with fake AWS auth headers, strip them, re-sign with real credentials via apply_sigv4_to_request, send over TLS to Bedrock, and verify a 200 response. Marked #[ignore] — requires real AWS credentials: AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx cargo test \ -p openshell-sandbox --test sigv4_signing -- --ignored --nocapture Also makes the sigv4 module pub for integration test access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
1 parent 37f0499 commit 95ef1f8

3 files changed

Lines changed: 98 additions & 1 deletion

File tree

crates/openshell-sandbox/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ seccompiler = "0.5"
9494
tempfile = "3"
9595
uuid = { version = "1", features = ["v4"] }
9696

97+
[[test]]
98+
name = "sigv4_signing"
99+
path = "tests/sigv4_signing.rs"
100+
97101
[dev-dependencies]
98102
tempfile = "3"
99103
temp-env = "0.3"

crates/openshell-sandbox/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod provider_credentials;
2323
pub mod proxy;
2424
mod sandbox;
2525
mod secrets;
26-
mod sigv4;
26+
pub mod sigv4;
2727
mod skills;
2828
mod ssh;
2929
mod supervisor_session;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Integration test for SigV4 proxy-side re-signing.
5+
//!
6+
//! Simulates what the proxy does: takes a raw HTTP request (like the AWS SDK
7+
//! would generate with placeholder credentials), strips the invalid AWS auth
8+
//! headers, re-signs with real credentials, and sends to Bedrock.
9+
//!
10+
//! Run with real AWS credentials:
11+
//! AWS_ACCESS_KEY_ID=AKIAxxx AWS_SECRET_ACCESS_KEY=xxx cargo test \
12+
//! -p openshell-sandbox --test sigv4_signing -- --ignored --nocapture
13+
14+
use std::io::{Read, Write};
15+
use std::net::TcpStream;
16+
17+
#[test]
18+
#[ignore] // requires real AWS credentials
19+
fn sigv4_resign_and_call_bedrock() {
20+
let access_key =
21+
std::env::var("AWS_ACCESS_KEY_ID").expect("AWS_ACCESS_KEY_ID must be set");
22+
let secret_key =
23+
std::env::var("AWS_SECRET_ACCESS_KEY").expect("AWS_SECRET_ACCESS_KEY must be set");
24+
let session_token = std::env::var("AWS_SESSION_TOKEN").ok();
25+
let region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-2".to_string());
26+
let host = format!("bedrock.{region}.amazonaws.com");
27+
28+
// Build a raw HTTP request as if the AWS SDK generated it with fake creds.
29+
// This is what arrives at the proxy from inside the sandbox.
30+
let fake_signed_request = format!(
31+
"GET /foundation-models HTTP/1.1\r\n\
32+
Host: {host}\r\n\
33+
Content-Type: application/json\r\n\
34+
Authorization: AWS4-HMAC-SHA256 Credential=FAKEFAKEFAKE/20260101/us-east-2/bedrock/aws4_request, SignedHeaders=host, Signature=0000000000000000000000000000000000000000000000000000000000000000\r\n\
35+
X-Amz-Date: 20260101T000000Z\r\n\
36+
X-Amz-Content-Sha256: fake-hash\r\n\
37+
Accept: application/json\r\n\
38+
Connection: keep-alive\r\n\
39+
\r\n"
40+
);
41+
42+
// Step 1: Strip invalid AWS auth headers (proxy does this before
43+
// the fail-closed placeholder scan)
44+
let stripped = openshell_sandbox::sigv4::strip_aws_headers(fake_signed_request.as_bytes());
45+
let stripped_str = String::from_utf8_lossy(&stripped);
46+
assert!(!stripped_str.contains("FAKEFAKEFAKE"), "old auth should be stripped");
47+
assert!(!stripped_str.contains("fake-hash"), "old hash should be stripped");
48+
49+
// Step 2: Re-sign with real credentials
50+
let signed = openshell_sandbox::sigv4::apply_sigv4_to_request(
51+
&stripped,
52+
&host,
53+
&region,
54+
"bedrock",
55+
&access_key,
56+
&secret_key,
57+
session_token.as_deref(),
58+
);
59+
60+
let signed_str = String::from_utf8_lossy(&signed);
61+
eprintln!("--- Signed request headers ---");
62+
if let Some(end) = signed_str.find("\r\n\r\n") {
63+
eprintln!("{}", &signed_str[..end]);
64+
}
65+
66+
// Step 3: Send to Bedrock over TLS
67+
let mut tcp = TcpStream::connect(format!("{host}:443")).expect("TCP connect");
68+
let mut root_store = rustls::RootCertStore::empty();
69+
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
70+
let config = rustls::ClientConfig::builder()
71+
.with_root_certificates(root_store)
72+
.with_no_client_auth();
73+
let server_name: rustls::pki_types::ServerName = host.clone().try_into().unwrap();
74+
let mut tls = rustls::ClientConnection::new(std::sync::Arc::new(config), server_name).unwrap();
75+
let mut stream = rustls::Stream::new(&mut tls, &mut tcp);
76+
77+
stream.write_all(&signed).expect("TLS write");
78+
stream.flush().expect("TLS flush");
79+
80+
let mut response = vec![0u8; 4096];
81+
let n = stream.read(&mut response).expect("TLS read");
82+
let response_str = String::from_utf8_lossy(&response[..n]);
83+
84+
eprintln!("\n--- Response (first {n} bytes) ---");
85+
eprintln!("{response_str}");
86+
87+
// Verify we got HTTP 200, not 403 InvalidSignatureException
88+
assert!(
89+
response_str.starts_with("HTTP/1.1 200"),
90+
"Expected 200 OK but got: {}",
91+
response_str.lines().next().unwrap_or("(empty)")
92+
);
93+
}

0 commit comments

Comments
 (0)