use std::time::SystemTime;
use microvms_app::control::transport::{
Call, Reply, SIGNING_NAME, Transport, endpoint_for, error_type_name,
};
use microvms_app::error::{Error, ErrorKind};
use microvms_app::region::Region;
pub struct SignedTransport {
region: Region,
endpoint: String,
credentials: aws_credential_types::provider::SharedCredentialsProvider,
http: reqwest::Client,
}
impl std::fmt::Debug for SignedTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SignedTransport")
.field("region", &self.region)
.field("endpoint", &self.endpoint)
.finish_non_exhaustive()
}
}
impl SignedTransport {
pub async fn new(region: Region) -> Result<Self, Error> {
let credentials = default_credentials(®ion).await?;
let http = http_client(std::time::Duration::from_secs(60))?;
Ok(Self {
endpoint: endpoint_for(®ion),
region,
credentials,
http,
})
}
fn url(&self, call: &Call) -> String {
format!("{}{}", self.endpoint, call.path)
}
}
pub(crate) async fn default_credentials(
region: &Region,
) -> Result<aws_credential_types::provider::SharedCredentialsProvider, Error> {
let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(aws_config::Region::new(region.as_str().to_string()))
.load()
.await;
config.credentials_provider().ok_or_else(|| {
Error::new(
ErrorKind::Credentials,
"no credentials provider resolved. The default chain looks at environment \
variables, the shared config files, SSO, a credential process, then the EC2 \
instance metadata service; none of them answered. `aws sts get-caller-identity` \
is the cheapest way to see the same failure.",
)
})
}
pub(crate) fn http_client(timeout: std::time::Duration) -> Result<reqwest::Client, Error> {
reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(10))
.timeout(timeout)
.build()
.map_err(|error| {
Error::new(
ErrorKind::Precondition,
format!("could not build the HTTP client: {error}"),
)
.with_source(error)
})
}
pub(crate) async fn resolve_credentials(
provider: &aws_credential_types::provider::SharedCredentialsProvider,
operation: &str,
) -> Result<aws_credential_types::Credentials, Error> {
use aws_credential_types::provider::ProvideCredentials as _;
provider.provide_credentials().await.map_err(|error| {
Error::new(
ErrorKind::Credentials,
format!(
"could not resolve credentials for {operation}: {error}. Waiting will not fix \
this — the identity is wrong or absent."
),
)
.with_source(error)
})
}
impl Transport for SignedTransport {
fn resolve_credentials(
&self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Error>> + Send + '_>> {
Box::pin(async move {
use aws_credential_types::provider::ProvideCredentials as _;
self.credentials
.provide_credentials()
.await
.map(|_| ())
.map_err(|error| {
Error::new(
ErrorKind::Credentials,
format!(
"the default credential chain resolved no credentials for {}: \
{error}. It looks at environment variables, the shared config \
files, SSO, a credential process, then the instance metadata \
service.",
self.region
),
)
.with_source(error)
})
})
}
fn send(
&self,
call: Call,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Reply, Error>> + Send + '_>>
{
Box::pin(async move {
let credentials = resolve_credentials(&self.credentials, call.operation).await?;
let url = self.url(&call);
let body = call.body_bytes().to_vec();
let mut request = http::Request::builder()
.method(call.method.as_str())
.uri(&url)
.header("content-type", "application/json")
.body(body.clone())
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not build the {} request: {error}", call.operation),
)
.with_source(error)
})?;
sign_in_place(&mut request, &credentials, &self.region)?;
let (parts, body) = request.into_parts();
let request = http::Request::from_parts(parts, reqwest::Body::from(body));
let request = reqwest::Request::try_from(request).map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not convert the signed request: {error}"),
)
.with_source(error)
})?;
let response = self.http.execute(request).await.map_err(|error| {
Error::new(
ErrorKind::Retryable,
format!(
"the {} request to {url} did not complete: {error}",
call.operation
),
)
.with_source(error)
})?;
let status = response.status().as_u16();
let error_type = response
.headers()
.get("x-amzn-errortype")
.and_then(|value| value.to_str().ok())
.map(|value| error_type_name(value).to_string());
let body = response
.bytes()
.await
.map_err(|error| {
Error::new(
ErrorKind::Retryable,
format!(
"could not read the {} response body: {error}",
call.operation
),
)
.with_source(error)
})?
.to_vec();
Ok(Reply {
status,
body,
error_type,
})
})
}
}
fn sign_in_place(
request: &mut http::Request<Vec<u8>>,
credentials: &aws_credential_types::Credentials,
region: &Region,
) -> Result<(), Error> {
sign_for(
request,
credentials,
region,
SIGNING_NAME,
aws_sigv4::http_request::SigningSettings::default(),
)
}
pub(crate) fn sign_for(
request: &mut http::Request<Vec<u8>>,
credentials: &aws_credential_types::Credentials,
region: &Region,
service: &str,
settings: aws_sigv4::http_request::SigningSettings,
) -> Result<(), Error> {
use aws_sigv4::http_request::{SignableBody, SignableRequest, sign};
use aws_sigv4::sign::v4;
let identity = credentials.clone().into();
let params: aws_sigv4::http_request::SigningParams = v4::SigningParams::builder()
.identity(&identity)
.region(region.as_str())
.name(service)
.time(SystemTime::now())
.settings(settings)
.build()
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not build SigV4 signing parameters: {error}"),
)
.with_source(error)
})?
.into();
let instructions = {
let signable = SignableRequest::new(
request.method().as_str(),
request.uri().to_string(),
request.headers().iter().filter_map(|(name, value)| {
value.to_str().ok().map(|value| (name.as_str(), value))
}),
SignableBody::Bytes(request.body()),
)
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not build the signable request: {error}"),
)
.with_source(error)
})?;
sign(signable, ¶ms)
.map_err(|error| {
Error::new(
ErrorKind::Credentials,
format!("SigV4 signing failed: {error}"),
)
.with_source(error)
})?
.into_parts()
.0
};
instructions.apply_to_request_http1x(request);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use microvms_app::control::transport::Method;
#[tokio::test]
async fn constructing_the_real_transport_returns_a_result_rather_than_panicking() {
let _ = SignedTransport::new(Region::UsEast1).await;
}
#[test]
fn signing_names_the_region_and_the_service_in_the_credential_scope() {
let credentials = aws_credential_types::Credentials::new(
"AKIDEXAMPLE",
"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
Some("session-token".to_string()),
None,
"test",
);
let mut request = http::Request::builder()
.method("POST")
.uri("https://lambda.eu-west-1.amazonaws.com/2025-09-09/microvms")
.header("content-type", "application/json")
.body(b"{}".to_vec())
.expect("builds");
sign_in_place(&mut request, &credentials, &Region::EuWest1).expect("signs");
let headers = request.headers();
let authorization = headers
.get("authorization")
.expect("a signature was applied")
.to_str()
.expect("ascii");
assert!(
authorization.starts_with("AWS4-HMAC-SHA256 "),
"{authorization}"
);
assert!(
authorization.contains("/eu-west-1/lambda/aws4_request"),
"the credential scope must name the request region and the signing name: \
{authorization}"
);
assert!(
headers.contains_key("x-amz-date"),
"a signed date is required"
);
assert!(
headers.contains_key("x-amz-security-token"),
"temporary credentials carry a session token in the canonical request"
);
}
#[test]
fn a_bodyless_get_is_signed_rather_than_sent_unsigned() {
let credentials =
aws_credential_types::Credentials::new("AKIDEXAMPLE", "secret", None, None, "test");
let mut request = http::Request::builder()
.method("GET")
.uri("https://lambda.us-east-1.amazonaws.com/2025-09-09/microvms/mvm-1")
.body(Vec::new())
.expect("builds");
sign_in_place(&mut request, &credentials, &Region::UsEast1).expect("signs");
assert!(request.headers().contains_key("authorization"));
assert!(
!request.headers().contains_key("x-amz-security-token"),
"static credentials carry no session token"
);
}
#[test]
fn a_patch_is_signed_with_its_method_in_the_canonical_request() {
let credentials =
aws_credential_types::Credentials::new("AKIDEXAMPLE", "secret", None, None, "test");
let mut request = http::Request::builder()
.method(Method::Patch.as_str())
.uri(
"https://lambda.us-east-1.amazonaws.com/2025-09-09/microvm-images/img/versions/2.0",
)
.header("content-type", "application/json")
.body(br#"{"status":"INACTIVE"}"#.to_vec())
.expect("builds");
sign_in_place(&mut request, &credentials, &Region::UsEast1).expect("signs");
assert_eq!(request.method().as_str(), "PATCH");
assert!(request.headers().contains_key("authorization"));
}
}