Class: HTTPX::Plugins::AWSSigV4::Signer

Inherits:
Object
  • Object
show all
Defined in:
lib/httpx/plugins/aws_sigv4.rb,
sig/plugins/aws_sigv4.rbs

Overview

Signs requests using the AWS sigv4 signing.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(service:, region:, credentials: nil, username: nil, password: nil, security_token: nil, provider_prefix: "aws", header_provider_field: "amz", unsigned_headers: [], apply_checksum_header: true, algorithm: "SHA256") ⇒ Signer

Returns a new instance of Signer.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/httpx/plugins/aws_sigv4.rb', line 17

def initialize(
  service:,
  region:,
  credentials: nil,
  username: nil,
  password: nil,
  security_token: nil,
  provider_prefix: "aws",
  header_provider_field: "amz",
  unsigned_headers: [],
  apply_checksum_header: true,
  algorithm: "SHA256"
)
  @credentials = credentials || Credentials.new(username, password, security_token)
  @service = service
  @region = region

  @unsigned_headers = Set.new(unsigned_headers.map(&:downcase))
  @unsigned_headers << "authorization"
  @unsigned_headers << "x-amzn-trace-id"
  @unsigned_headers << "expect"

  @apply_checksum_header = apply_checksum_header
  @provider_prefix = provider_prefix
  @header_provider_field = header_provider_field

  @algorithm = algorithm
end

Class Method Details

.new(arg0) ⇒ instance .new(params) ⇒ instance

Overloads:

  • .new(arg0) ⇒ instance

    Parameters:

    • arg0 (instance)

    Returns:

    • (instance)
  • .new(params) ⇒ instance

    Parameters:

    • params (Object)

    Returns:

    • (instance)


31
32
# File 'sig/plugins/aws_sigv4.rbs', line 31

def self.new: (instance) -> instance
| (**untyped params) -> instance

Instance Method Details

#hexdigest(value) ⇒ String

Parameters:

Returns:

  • (String)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/httpx/plugins/aws_sigv4.rb', line 112

def hexdigest(value)
  digest = OpenSSL::Digest.new(@algorithm)

  if value.respond_to?(:read)
    if value.respond_to?(:to_path)
      # files, pathnames
      digest.file(value.to_path).hexdigest
    else
      # gzipped request bodies
      raise Error, "request body must be rewindable" unless value.respond_to?(:rewind)

      buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
      begin
        IO.copy_stream(value, buffer)
        buffer.flush

        digest.file(buffer.to_path).hexdigest
      ensure
        value.rewind
        buffer.close
        buffer.unlink
      end
    end
  else
    # error on endless generators
    raise Error, "hexdigest for endless enumerators is not supported" if value.unbounded_body?

    mb_buffer = value.each.with_object("".b) do |chunk, b|
      b << chunk
      break if b.bytesize >= 1024 * 1024
    end

    digest.hexdigest(mb_buffer)
  end
end

#hexhmac(key, value) ⇒ String

Parameters:

  • key (String)
  • value (String)

Returns:

  • (String)


152
153
154
# File 'lib/httpx/plugins/aws_sigv4.rb', line 152

def hexhmac(key, value)
  OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(@algorithm), key, value)
end

#hmac(key, value) ⇒ String

Parameters:

  • key (String)
  • value (String)

Returns:

  • (String)


148
149
150
# File 'lib/httpx/plugins/aws_sigv4.rb', line 148

def hmac(key, value)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new(@algorithm), key, value)
end

#sign!(request) ⇒ void

This method returns an undefined value.

Parameters:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/httpx/plugins/aws_sigv4.rb', line 46

def sign!(request)
  lower_provider_prefix = "#{@provider_prefix}4"
  upper_provider_prefix = lower_provider_prefix.upcase

  downcased_algorithm = @algorithm.downcase

  datetime = (request.headers["x-#{@header_provider_field}-date"] ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ"))
  date = datetime[0, 8]

  content_hashed = request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"] || hexdigest(request.body)

  request.headers["x-#{@header_provider_field}-content-#{downcased_algorithm}"] ||= content_hashed if @apply_checksum_header
  request.headers["x-#{@header_provider_field}-security-token"] ||= @credentials.security_token if @credentials.security_token

  signature_headers = request.headers.each.reject do |k, _|
    @unsigned_headers.include?(k)
  end
  # aws sigv4 needs to declare the host, regardless of protocol version
  signature_headers << ["host", request.authority] unless request.headers.key?("host")
  signature_headers.sort_by!(&:first)

  signed_headers = signature_headers.map(&:first).join(";")

  canonical_headers = signature_headers.map do |k, v|
    # eliminate whitespace between value fields, unless it's a quoted value
    "#{k}:#{v.start_with?("\"") && v.end_with?("\"") ? v : v.gsub(/\s+/, " ").strip}\n"
  end.join

  # canonical request
  creq = "#{request.verb}" \
         "\n#{request.canonical_path}" \
         "\n#{request.canonical_query}" \
         "\n#{canonical_headers}" \
         "\n#{signed_headers}" \
         "\n#{content_hashed}"

  credential_scope = "#{date}" \
                     "/#{@region}" \
                     "/#{@service}" \
                     "/#{lower_provider_prefix}_request"

  algo_line = "#{upper_provider_prefix}-HMAC-#{@algorithm}"
  # string to sign
  sts = "#{algo_line}" \
        "\n#{datetime}" \
        "\n#{credential_scope}" \
        "\n#{OpenSSL::Digest.new(@algorithm).hexdigest(creq)}"

  # signature
  k_date = hmac("#{upper_provider_prefix}#{@credentials.password}", date)
  k_region = hmac(k_date, @region)
  k_service = hmac(k_region, @service)
  k_credentials = hmac(k_service, "#{lower_provider_prefix}_request")
  sig = hexhmac(k_credentials, sts)

  credential = "#{@credentials.username}/#{credential_scope}"
  # apply signature
  request.headers["authorization"] =
    "#{algo_line} " \
    "Credential=#{credential}, " \
    "SignedHeaders=#{signed_headers}, " \
    "Signature=#{sig}"
end