use multistore::auth::{constant_time_eq, parse_sigv4_auth, verify_sigv4_signature};
use multistore::error::ProxyError;
use multistore::route_handler::RequestInfo;
use sha2::{Digest, Sha256};
use crate::responses::{build_caller_identity_response, build_sts_error_response};
use crate::TokenKey;
pub fn handle_get_caller_identity(
req: &RequestInfo<'_>,
token_key: Option<&TokenKey>,
) -> (u16, String) {
match resolve_caller_identity(req, token_key) {
Ok(xml) => (200, xml),
Err(e) => {
tracing::warn!(error = %e, "GetCallerIdentity request rejected");
build_sts_error_response(&e)
}
}
}
fn resolve_caller_identity(
req: &RequestInfo<'_>,
token_key: Option<&TokenKey>,
) -> Result<String, ProxyError> {
let token_key = token_key.ok_or_else(|| {
tracing::error!("GetCallerIdentity received but SESSION_TOKEN_KEY is not configured");
ProxyError::ConfigError("STS requires SESSION_TOKEN_KEY to be configured".into())
})?;
let session_token = header(req, "x-amz-security-token").ok_or(ProxyError::AccessDenied)?;
let creds = token_key
.unseal(session_token)?
.ok_or(ProxyError::AccessDenied)?;
let auth_header = header(req, "authorization").ok_or(ProxyError::AccessDenied)?;
let sig = parse_sigv4_auth(auth_header)?;
if !constant_time_eq(sig.access_key_id.as_bytes(), creds.access_key_id.as_bytes()) {
tracing::warn!(
header_key = %sig.access_key_id,
resolved_key = %creds.access_key_id,
"access key mismatch between auth header and session token"
);
return Err(ProxyError::AccessDenied);
}
let body = req.form_body.unwrap_or_default();
let payload_hash = header(req, "x-amz-content-sha256")
.map(str::to_owned)
.unwrap_or_else(|| hex::encode(Sha256::digest(body.as_bytes())));
let signing_path = req.signing_path.unwrap_or(req.path);
let signing_query = req.signing_query.or(req.query).unwrap_or("");
if !verify_sigv4_signature(
req.method,
signing_path,
signing_query,
req.headers,
&sig,
&creds.secret_access_key,
&payload_hash,
)? {
return Err(ProxyError::SignatureDoesNotMatch);
}
Ok(build_caller_identity_response(&creds))
}
fn header<'a>(req: &'a RequestInfo<'_>, name: &str) -> Option<&'a str> {
req.headers.get(name).and_then(|v| v.to_str().ok())
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
use http::{HeaderMap, Method};
fn test_key() -> crate::TokenKey {
let encoded = base64::engine::general_purpose::STANDARD.encode([0x11u8; 32]);
crate::TokenKey::from_base64(&encoded).unwrap()
}
fn req_with_headers<'a>(
method: &'a Method,
path: &'a str,
headers: &'a HeaderMap,
form_body: Option<&'a str>,
) -> RequestInfo<'a> {
RequestInfo::new(method, path, None, headers, None).with_form_body(form_body)
}
#[test]
fn missing_session_token_is_access_denied() {
let key = test_key();
let method = Method::POST;
let headers = HeaderMap::new();
let req = req_with_headers(
&method,
"/.sts/",
&headers,
Some("Action=GetCallerIdentity&Version=2011-06-15"),
);
let (status, xml) = handle_get_caller_identity(&req, Some(&key));
assert_eq!(status, 403, "{xml}");
assert!(xml.contains("AccessDenied"), "{xml}");
}
#[test]
fn missing_token_key_is_internal_error() {
let method = Method::POST;
let headers = HeaderMap::new();
let req = req_with_headers(&method, "/.sts/", &headers, None);
let (status, _) = handle_get_caller_identity(&req, None);
assert_eq!(status, 500);
}
}