
cognitect.aws.signers.clj Maven / Gradle / Ivy
The newest version!
;; Copyright (c) Cognitect, Inc.
;; All rights reserved.
(ns ^:skip-wiki cognitect.aws.signers
"Impl, don't call directly."
(:require [clojure.string :as str]
[cognitect.aws.service :as service]
[cognitect.aws.util :as util])
(:import [java.net URI]
[java.net URLDecoder]))
(set! *warn-on-reflection* true)
(defmulti sign-http-request
"Sign the HTTP request."
(fn [service _endpoint _credentials _http-request]
(get-in service [:metadata :signatureVersion])))
(def ^:private safe-chars
"ASCII codes for characters that are never encoded."
(set (mapv int "_-~.")))
(defn uri-encode
"Escape (%XX) special characters in the string `s`.
Letters, digits, and the characters `_-~.` are never encoded.
The optional `extra-chars` specifies extra characters to not encode."
([^String s]
(when s
(uri-encode s "")))
([^String s extra-chars]
(when s
(let [extra-chars (set (mapv int extra-chars))
builder (StringBuilder.)]
(doseq [b (.getBytes s "UTF-8")]
(.append builder
(if (or (Character/isLetterOrDigit ^int b)
(contains? safe-chars b)
(contains? extra-chars b))
(char b)
(format "%%%02X" b))))
(.toString builder)))))
(defn- request->x-amz-date-only
"Given a request map, reads the x-amz-date header,
parses it according to x-amz-date-format, and returns
the date-only formatted string.
e.g. given {:headers {\"x-amz-date\" \"20241127T021030Z\"}}
it returns \"20241127\"
"
[request]
(-> (get-in request [:headers "x-amz-date"])
(str/split #"T")
first))
(defn credential-scope
[{:keys [region service]} request]
(str/join "/" [(request->x-amz-date-only request)
region
service
"aws4_request"]))
(defn- canonical-method
[{:keys [request-method]}]
(-> request-method name str/upper-case))
(defn- canonical-uri
[{:keys [uri]} {:keys [double-encode? normalize-uri?]}]
(let [[path _query] (str/split uri #"\?")
^String encoded-path (-> path
(cond-> double-encode? (uri-encode "/"))
(str/replace #"^//+" "/") ; (URI.) throws Exception on '//' at beginning of string.
(str/replace #"\s" "%20"); (URI.) throws Exception on space.
(URI.)
(cond-> normalize-uri? (.normalize))
(.getPath)
(uri-encode "/"))]
(cond
(.isEmpty encoded-path)
"/"
;; https://github.com/aws/aws-sdk-java-v2/blob/61d16e0/core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java#L546-L555
;; Normalization can leave a trailing slash at the end of the resource path,
;; even if the input path doesn't end with one. Example input: /foo/bar/.
;; Remove the trailing slash if the input path doesn't end with one.
(and (not= encoded-path "/")
(str/ends-with? encoded-path "/")
(not (str/ends-with? path "/")))
(.substring encoded-path 0 (dec (.length encoded-path)))
:else encoded-path)))
(defn- canonical-query-string
[{:keys [uri query-string]}]
(let [qs (or query-string (second (str/split uri #"\?")))]
(when-not (str/blank? qs)
(->> (str/split qs #"&")
(map #(str/split % #"=" 2))
;; TODO (dchelimsky 2019-01-30) decoding first because sometimes
;; it's already been encoding. Look into avoiding that!
(map (fn [kv] (map #(uri-encode (URLDecoder/decode %)) kv)))
(sort (fn [[k1 v1] [k2 v2]]
(if (= k1 k2)
(compare v1 v2)
(compare k1 k2))))
(map (fn [[k v]] (str k "=" v)))
(str/join "&")))))
(defn- canonical-headers
[{:keys [headers]}]
(reduce-kv (fn [m k v]
(assoc m (str/lower-case k) (-> v str/trim (str/replace #"\s+" " "))))
(sorted-map)
headers))
(defn- canonical-headers-string
[request]
(->> (canonical-headers request)
(map (fn [[k v]] (str k ":" v "\n")))
(str/join "")))
(defn signed-headers
[request]
(->> (canonical-headers request)
keys
(str/join ";")))
(defn hashed-body
[request]
(util/hex-encode (util/sha-256 (:body request))))
(defn canonical-request
[{:keys [headers] :as request} opts]
(str/join "\n" [(canonical-method request)
(canonical-uri request opts)
(canonical-query-string request)
(canonical-headers-string request)
(signed-headers request)
(or (get headers "x-amz-content-sha256")
(hashed-body request))]))
(defn string-to-sign
[request auth-info opts]
(let [bytes (.getBytes ^String (canonical-request request opts))]
(str/join "\n" ["AWS4-HMAC-SHA256"
(get-in request [:headers "x-amz-date"])
(credential-scope auth-info request)
(util/hex-encode (util/sha-256 bytes))])))
(defn signing-key
[request {:keys [secret-access-key region service]}]
(-> (.getBytes (str "AWS4" secret-access-key) "UTF-8")
(util/hmac-sha-256 (request->x-amz-date-only request))
(util/hmac-sha-256 region)
(util/hmac-sha-256 service)
(util/hmac-sha-256 "aws4_request")))
(defn signature
[auth-info request opts]
(util/hex-encode
(util/hmac-sha-256 (signing-key request auth-info)
(string-to-sign request auth-info opts))))
(defn v4-sign-http-request
[service endpoint credentials http-request & {:keys [content-sha256-header? double-url-encode? normalize-uri-paths?]}]
(let [{:keys [:aws/access-key-id :aws/secret-access-key :aws/session-token]} credentials
auth-info {:access-key-id access-key-id
:secret-access-key secret-access-key
:service (or (service/signing-name service)
(service/endpoint-prefix service))
:region (or (get-in endpoint [:credentialScope :region])
(:region endpoint))}
req (cond-> http-request
session-token (assoc-in [:headers "x-amz-security-token"] session-token)
content-sha256-header? (assoc-in [:headers "x-amz-content-sha256"] (hashed-body http-request)))]
(assoc-in req
[:headers "authorization"]
(format "AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s"
(:access-key-id auth-info)
(credential-scope auth-info req)
(signed-headers req)
(signature auth-info req {:double-encode? double-url-encode?
:normalize-uri? normalize-uri-paths?})))))
;; https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
;;
;; Each path segment must be URI-encoded twice (except for Amazon S3 which only gets URI-encoded once).
;;
;; Normalize URI paths according to RFC 3986.
;; In exception to this, you do not normalize URI paths for requests to Amazon S3
(defmethod sign-http-request "v4"
[service endpoint credentials http-request]
(v4-sign-http-request service endpoint credentials http-request
:double-url-encode? true
:normalize-uri-paths? true))
(defmethod sign-http-request "s3"
[service endpoint credentials http-request]
(v4-sign-http-request service endpoint credentials http-request
:content-sha256-header? true))
(defmethod sign-http-request "s3v4"
[service endpoint credentials http-request]
(v4-sign-http-request service endpoint credentials http-request
:content-sha256-header? true))
© 2015 - 2025 Weber Informatics LLC | Privacy Policy