///|
pub(all) struct Credentials {
access_key_id : String
secret_access_key : String
session_token : String?
} derive(Eq)
///|
pub fn Credentials::Credentials(
access_key_id : String,
secret_access_key : String,
session_token? : String,
) -> Credentials {
{ access_key_id, secret_access_key, session_token, }
}
///|
fn sha256_hex(data : String) -> String {
@crypto.bytes_to_hex_string(@crypto.sha256(@utf8.encode(data)))
}
///|
fn hmac_sha256(key : BytesView, data : String) -> FixedArray[Byte] {
@crypto.hmac(@crypto.SHA256::new(), key, @utf8.encode(data))
}
///|
fn hmac_sha256_hex(key : BytesView, data : String) -> String {
@crypto.bytes_to_hex_string(hmac_sha256(key, data))
}
///|
/// Derive the SigV4 signing key for the given date, region and service.
pub fn signing_key(
secret : String,
date : String,
region : String,
service : String,
) -> FixedArray[Byte] {
let k_date = hmac_sha256(@utf8.encode("AWS4\{secret}"), date)
let k_region = hmac_sha256(k_date.unsafe_reinterpret_as_bytes(), region)
let k_service = hmac_sha256(k_region.unsafe_reinterpret_as_bytes(), service)
hmac_sha256(k_service.unsafe_reinterpret_as_bytes(), "aws4_request")
}
///|
fn normalize_header_value(value : String) -> String {
let out = StringBuilder()
let mut in_whitespace = false
for ch in value.trim() {
if ch.is_ascii_whitespace() {
if !in_whitespace {
out.write_char(' ')
in_whitespace = true
}
} else {
out.write_char(ch)
in_whitespace = false
}
}
out.to_string()
}
///|
fn canonical_headers(headers : Map[String, String]) -> (String, String) {
let pairs : Array[(String, String)] = []
for key, value in headers {
pairs.push((key.to_lower(), normalize_header_value(value)))
}
pairs.sort_by(fn(left, right) { ascii_compare(left.0, right.0) })
let coalesced : Array[(String, Array[String])] = []
for pair in pairs {
let (name, value) = pair
match coalesced.last() {
Some((last_name, values)) if last_name == name => values.push(value)
_ => coalesced.push((name, [value]))
}
}
let canonical = coalesced
.map(fn(pair) { "\{pair.0}:\{pair.1.join(",")}\n" })
.join("")
let signed = coalesced.map(fn(pair) { pair.0 }).join(";")
(canonical, signed)
}
///|
fn ascii_compare(left : String, right : String) -> Int {
let min_len = Int::min(left.length(), right.length())
for i in 0.. Map[String, String] {
let date = timestamp[:8].to_owned()
let (canonical, signed_headers) = canonical_headers(headers)
let canonical_request = [
meth,
path,
canonical_query(query),
canonical,
signed_headers,
payload_hash,
].join("\n")
let scope = "\{date}/\{region}/\{service}/aws4_request"
let string_to_sign = [
"AWS4-HMAC-SHA256",
timestamp,
scope,
sha256_hex(canonical_request),
].join("\n")
let signature = hmac_sha256_hex(
signing_key(credentials.secret_access_key, date, region, service).unsafe_reinterpret_as_bytes(),
string_to_sign,
)
let signed = headers.copy()
signed["Authorization"] = "AWS4-HMAC-SHA256 Credential=\{credentials.access_key_id}/\{scope}, SignedHeaders=\{signed_headers}, Signature=\{signature}"
signed
}
///|
/// Build the query parameters of a presigned URL: the `X-Amz-*` authorization
/// parameters plus the computed `X-Amz-Signature`. The payload is always
/// treated as `UNSIGNED-PAYLOAD`, as required for presigned URLs. Only `host`
/// plus the names in `signed_headers` are included in the signature; the
/// caller is responsible for sending those headers verbatim with the request.
pub fn presign_query(
meth : String,
path : String,
host : String,
credentials : Credentials,
region : String,
service : String,
timestamp : String,
expires : Int,
signed_headers? : Map[String, String] = {},
) -> Map[String, String] {
let date = timestamp[:8].to_owned()
let scope = "\{date}/\{region}/\{service}/aws4_request"
let query : Map[String, String] = Map([])
query["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256"
query["X-Amz-Credential"] = "\{credentials.access_key_id}/\{scope}"
query["X-Amz-Date"] = timestamp
query["X-Amz-Expires"] = expires.to_string()
let headers = Map([])
headers["host"] = host
for key, value in signed_headers {
headers[key] = value
}
let (canonical, signed) = canonical_headers(headers)
if credentials.session_token is Some(token) {
query["X-Amz-Security-Token"] = token
}
query["X-Amz-SignedHeaders"] = signed
let canonical_request = [
meth,
path,
canonical_query(query),
canonical,
signed,
"UNSIGNED-PAYLOAD",
].join("\n")
let string_to_sign = [
"AWS4-HMAC-SHA256",
timestamp,
scope,
sha256_hex(canonical_request),
].join("\n")
let signature = hmac_sha256_hex(
signing_key(credentials.secret_access_key, date, region, service).unsafe_reinterpret_as_bytes(),
string_to_sign,
)
query["X-Amz-Signature"] = signature
query
}