Skip to content

Commit

Permalink
WIP: signing for AWS requests
Browse files Browse the repository at this point in the history
Fixes #566
  • Loading branch information
hadley committed Oct 22, 2024
1 parent 5380f21 commit 37c61c9
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
154 changes: 154 additions & 0 deletions R/req-auth-aws.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#' @examples
#' creds <- paws.common::locate_credentials("bedrock")
#' model_id <- "anthropic.claude-3-5-sonnet-20240620-v1:0"
#' req <- request("https://bedrock-runtime.us-east-1.amazonaws.com")
#' # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
#' req <- req_url_path_append(req, "model", model_id, "converse")
#' req <- req_body_json(req, list(
#' messages = list(list(
#' role = "user",
#' content = list(list(text = "What's your name?"))
#' ))
#' ))
#' req <- req_sign_aws_v4_auth(
#' req,
#' "bedrock",
#' aws_access_key_id = creds$access_key_id,
#' aws_secret_access_key = creds$secret_access_key,
#' aws_session_token = creds$session_token,
#' aws_region = creds$region
#' )
#' resp <- req_perform_connection(req)
#' str(resp_body_json(resp))
#'
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
req_sign_aws_v4_auth <- function(req,
aws_service,
aws_access_key_id = Sys.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key = Sys.getenv("AWS_SECRET_ACCESS_KEY"),
aws_session_token = Sys.getenv("AWS_SESSION_TOKEN"),
aws_region = Sys.getenv("AWS_DEFAULT_REGION"),
current_time = Sys.time()) {

body_sha256 <- openssl::sha256(req_body_get(req))

Check warning on line 33 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L33

Added line #L33 was not covered by tests

# We begin by adding some necessary headers that must be added before
# canoncalization even thought they aren't documented until later
req <- req_aws_headers(req,
current_time = current_time,
aws_session_token = aws_session_token,
body_sha256 = body_sha256
)

Check warning on line 41 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L37-L41

Added lines #L37 - L41 were not covered by tests

authorization_header <- aws_v4_signature(
method = req_method_get(req),
url = url_parse(req$url),
headers = req$headers,
body_sha256 = body_sha256,
current_time = current_time,
aws_service = aws_service,
aws_region = aws_region,
aws_access_key_id = aws_access_key_id,
aws_secret_access_key = aws_secret_access_key
)
req_headers(req, Authorization = authorization_header)

Check warning on line 54 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L43-L54

Added lines #L43 - L54 were not covered by tests
}

req_aws_headers <- function(req, current_time, aws_session_token, body_sha256) {
RequestDateTime <- format(current_time, "%Y%m%dT%H%M%SZ", tz = "UTC")

Check warning on line 58 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L58

Added line #L58 was not covered by tests

req_headers(
req,
"x-amz-date" = RequestDateTime,
"x-amz-content-sha256" = body_sha256, # necessary for S3
"x-amz-security-token" = aws_session_token
)

Check warning on line 65 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L60-L65

Added lines #L60 - L65 were not covered by tests
}

aws_v4_signature <- function(method,
url,
headers,
body_sha256,
current_time = Sys.time(),
aws_service,
aws_region,
aws_access_key_id,
aws_secret_access_key) {
# 1. Create a canonical request
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#create-canonical-request
HTTPMethod <- method
CanonicalURI <- curl::curl_escape(url$path %||% "/")

Check warning on line 80 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L79-L80

Added lines #L79 - L80 were not covered by tests
# AWS does not want / to be encoded here
CanonicalURI <- gsub("%2F", "/", CanonicalURI, fixed = TRUE)

Check warning on line 82 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L82

Added line #L82 was not covered by tests

if (is.null(url$query)) {
CanonicalQueryString <- ""

Check warning on line 85 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L84-L85

Added lines #L84 - L85 were not covered by tests
} else {
sorted_query <- url$query[order(names(url$query))]
CanonicalQueryString <- query_build(CanonicalQueryString)

Check warning on line 88 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L87-L88

Added lines #L87 - L88 were not covered by tests
}

headers$host <- url$hostname
names(headers) <- tolower(names(headers))
headers <- headers[order(names(headers))]
headers[] <- trimws(headers)
headers[] <- gsub(" {2,}", " ", headers)
CanonicalHeaders <- paste0(names(headers), ":", headers, "\n", collapse = "")
SignedHeaders <- paste0(names(headers), collapse = ";")

Check warning on line 97 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L91-L97

Added lines #L91 - L97 were not covered by tests

CanonicalRequest <- paste0(
HTTPMethod, "\n",
CanonicalURI, "\n",
CanonicalQueryString, "\n",
CanonicalHeaders, "\n",
SignedHeaders, "\n",
body_sha256
)

Check warning on line 106 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L99-L106

Added lines #L99 - L106 were not covered by tests
# 2. Create the hash of the canonical request
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
HashedCanonicalRequest <- openssl::sha256(CanonicalRequest)

Check warning on line 109 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L109

Added line #L109 was not covered by tests

# 3. Create the string to sign
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#create-string-to-sign

Algorithm <- "AWS4-HMAC-SHA256"
RequestDateTime <- format(current_time, "%Y%m%dT%H%M%SZ", tz = "UTC")
Date <- format(current_time, "%Y%m%d", tz = "UTC")
CredentialScope <- file.path(Date, aws_region, aws_service, "aws4_request")

Check warning on line 117 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L114-L117

Added lines #L114 - L117 were not covered by tests

string_to_sign <- paste0(
Algorithm, "\n",
RequestDateTime, "\n",
CredentialScope, "\n",
HashedCanonicalRequest
)

Check warning on line 124 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L119-L124

Added lines #L119 - L124 were not covered by tests

# 4. Derive a signing key
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#derive-signing-key

DateKey <- hmac_sha256(paste0("AWS4", aws_secret_access_key), Date)
DateRegionKey <- hmac_sha256(DateKey, aws_region)
DateRegionServiceKey <- hmac_sha256(DateRegionKey, aws_service)
SigningKey <- hmac_sha256(DateRegionServiceKey, "aws4_request")

Check warning on line 132 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L129-L132

Added lines #L129 - L132 were not covered by tests

# 5. Calculate signature
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#calculate-signature

signature <- hmac_sha256(SigningKey, string_to_sign)
signature <- paste0(as.character(signature), collapse = "")

Check warning on line 138 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L137-L138

Added lines #L137 - L138 were not covered by tests

# 6. Add the signature to the request
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#calculate-signature
credential <- file.path(aws_access_key_id, CredentialScope)

Check warning on line 142 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L142

Added line #L142 was not covered by tests

paste0(
Algorithm, ",",
"Credential=", credential, ",",
"SignedHeaders=", SignedHeaders, ",",
"Signature=", signature
)

Check warning on line 149 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L144-L149

Added lines #L144 - L149 were not covered by tests
}

hmac_sha256 <- function(key, value) {
openssl::sha256(charToRaw(value), key)

Check warning on line 153 in R/req-auth-aws.R

View check run for this annotation

Codecov / codecov/patch

R/req-auth-aws.R#L153

Added line #L153 was not covered by tests
}
12 changes: 12 additions & 0 deletions R/req-body.R
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ req_body_info <- function(req) {
}
}

req_body_get <- function(req) {
if (is.null(req$body)) {
return("")

Check warning on line 224 in R/req-body.R

View check run for this annotation

Codecov / codecov/patch

R/req-body.R#L223-L224

Added lines #L223 - L224 were not covered by tests
}
switch(
req$body$type,
raw = req$body$data,
json = exec(jsonlite::toJSON, req$body$data, !!!req$body$params),
cli::cli_abort("Unsupported request body type {.str {type}}.")

Check warning on line 230 in R/req-body.R

View check run for this annotation

Codecov / codecov/patch

R/req-body.R#L227-L230

Added lines #L227 - L230 were not covered by tests
)
}

req_body_apply <- function(req) {
if (is.null(req$body)) {
return(req)
Expand Down

0 comments on commit 37c61c9

Please sign in to comment.