codetether_agent/provider/bedrock/
sigv4.rs1use super::BedrockProvider;
24use super::auth::BedrockAuth;
25use anyhow::{Context, Result};
26use hmac::{Hmac, Mac};
27use reqwest::Url;
28use sha2::{Digest, Sha256};
29
30impl BedrockProvider {
31 pub(super) fn base_url(&self) -> String {
41 format!("https://bedrock-runtime.{}.amazonaws.com", self.region)
42 }
43
44 pub(super) fn management_url(&self) -> String {
46 format!("https://bedrock.{}.amazonaws.com", self.region)
47 }
48
49 pub async fn send_converse_request(&self, url: &str, body: &[u8]) -> Result<reqwest::Response> {
75 self.send_request("POST", url, Some(body), "bedrock").await
76 }
77
78 pub(super) async fn send_request(
80 &self,
81 method: &str,
82 url: &str,
83 body: Option<&[u8]>,
84 service: &str,
85 ) -> Result<reqwest::Response> {
86 match &self.auth {
87 BedrockAuth::SigV4(_) => {
88 self.send_signed_request(method, url, body.unwrap_or(b""), service)
89 .await
90 }
91 BedrockAuth::BearerToken(token) => {
92 let mut req = self
93 .client
94 .request(method.parse().unwrap_or(reqwest::Method::GET), url)
95 .bearer_auth(token)
96 .header("content-type", "application/json")
97 .header("accept", "application/json");
98 if let Some(b) = body {
99 req = req.body(b.to_vec());
100 }
101 req.send()
102 .await
103 .context("Failed to send request to Bedrock")
104 }
105 }
106 }
107
108 async fn send_signed_request(
110 &self,
111 method: &str,
112 url: &str,
113 body: &[u8],
114 service: &str,
115 ) -> Result<reqwest::Response> {
116 let creds = match &self.auth {
117 BedrockAuth::SigV4(c) => c,
118 BedrockAuth::BearerToken(_) => {
119 anyhow::bail!("send_signed_request called with bearer token auth");
120 }
121 };
122
123 let now = chrono::Utc::now();
124 let datestamp = now.format("%Y%m%d").to_string();
125 let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
126 let canonical_url = canonicalize_url(url)?;
127 let host = canonical_url.host;
128 let canonical_uri = canonical_url.canonical_uri;
129 let canonical_querystring = canonical_url.canonical_querystring;
130
131 let payload_hash = sha256_hex(body);
132
133 let mut headers_map: Vec<(&str, String)> = vec![
134 ("content-type", "application/json".to_string()),
135 ("host", host.clone()),
136 ("x-amz-content-sha256", payload_hash.clone()),
137 ("x-amz-date", amz_date.clone()),
138 ];
139 if let Some(token) = &creds.session_token {
140 headers_map.push(("x-amz-security-token", token.clone()));
141 }
142 headers_map.sort_by_key(|(k, _)| *k);
143
144 let canonical_headers: String = headers_map
145 .iter()
146 .map(|(k, v)| format!("{k}:{v}"))
147 .collect::<Vec<_>>()
148 .join("\n")
149 + "\n";
150 let signed_headers: String = headers_map
151 .iter()
152 .map(|(k, _)| *k)
153 .collect::<Vec<_>>()
154 .join(";");
155
156 let canonical_request = format!(
157 "{method}\n{canonical_uri}\n{canonical_querystring}\n{canonical_headers}\n{signed_headers}\n{payload_hash}"
158 );
159
160 let credential_scope = format!("{datestamp}/{}/{service}/aws4_request", self.region);
161
162 let string_to_sign = format!(
163 "AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{}",
164 sha256_hex(canonical_request.as_bytes())
165 );
166
167 let k_date = hmac_sha256(
168 format!("AWS4{}", creds.secret_access_key).as_bytes(),
169 datestamp.as_bytes(),
170 );
171 let k_region = hmac_sha256(&k_date, self.region.as_bytes());
172 let k_service = hmac_sha256(&k_region, service.as_bytes());
173 let k_signing = hmac_sha256(&k_service, b"aws4_request");
174
175 let signature = hex::encode(hmac_sha256(&k_signing, string_to_sign.as_bytes()));
176
177 let authorization = format!(
178 "AWS4-HMAC-SHA256 Credential={}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}",
179 creds.access_key_id
180 );
181
182 let mut req = self
183 .client
184 .request(method.parse().unwrap_or(reqwest::Method::POST), url)
185 .header("content-type", "application/json")
186 .header("host", &host)
187 .header("x-amz-date", &amz_date)
188 .header("x-amz-content-sha256", &payload_hash)
189 .header("authorization", &authorization);
190
191 if let Some(token) = &creds.session_token {
192 req = req.header("x-amz-security-token", token);
193 }
194
195 if method == "POST" || method == "PUT" {
196 req = req.body(body.to_vec());
197 }
198
199 req.send()
200 .await
201 .context("Failed to send signed request to Bedrock")
202 }
203}
204
205#[derive(Debug, PartialEq, Eq)]
206pub(super) struct CanonicalUrl {
207 pub(super) host: String,
208 pub(super) canonical_uri: String,
209 pub(super) canonical_querystring: String,
210}
211
212pub(super) fn canonicalize_url(url: &str) -> Result<CanonicalUrl> {
213 let parsed = Url::parse(url).with_context(|| format!("Invalid Bedrock URL: {url}"))?;
214 let host = canonical_host(&parsed).context("Bedrock URL missing host")?;
215 let canonical_uri = canonical_uri(&parsed)?;
216 let canonical_querystring = canonical_querystring(&parsed);
217 Ok(CanonicalUrl {
218 host,
219 canonical_uri,
220 canonical_querystring,
221 })
222}
223
224fn canonical_host(url: &Url) -> Option<String> {
225 let host = url.host_str()?.to_string();
226 Some(match url.port() {
227 Some(port) => format!("{host}:{port}"),
228 None => host,
229 })
230}
231
232fn canonical_uri(url: &Url) -> Result<String> {
233 let segments = url.path_segments().context("Bedrock URL missing path")?;
234 let encoded = segments
235 .map(canonical_path_segment)
236 .collect::<Result<Vec<_>>>()?;
237 Ok(format!("/{}", encoded.join("/")))
238}
239
240fn canonical_path_segment(segment: &str) -> Result<String> {
241 let decoded = urlencoding::decode(segment)
242 .with_context(|| format!("invalid percent-encoding in path segment `{segment}`"))?;
243 Ok(urlencoding::encode(&decoded).into_owned())
244}
245
246fn canonical_querystring(url: &Url) -> String {
247 let mut pairs = url
248 .query_pairs()
249 .map(|(key, value)| {
250 (
251 urlencoding::encode(&key).into_owned(),
252 urlencoding::encode(&value).into_owned(),
253 )
254 })
255 .collect::<Vec<_>>();
256 pairs.sort();
257 pairs
258 .into_iter()
259 .map(|(key, value)| format!("{key}={value}"))
260 .collect::<Vec<_>>()
261 .join("&")
262}
263
264fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
266 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC can take key of any size");
267 mac.update(data);
268 mac.finalize().into_bytes().to_vec()
269}
270
271fn sha256_hex(data: &[u8]) -> String {
273 let mut hasher = Sha256::new();
274 hasher.update(data);
275 hex::encode(hasher.finalize())
276}