Module: Mixlib::Authentication::SignedHeaderAuth
- Included in:
- SignatureVerification, SigningObject
- Defined in:
- lib/mixlib/authentication/signedheaderauth.rb
Constant Summary collapse
- NULL_ARG =
Object.new
- ALGORITHM_FOR_VERSION =
{ "1.0" => "sha1", "1.1" => "sha1", "1.3" => "sha256", }.freeze
- SUPPORTED_ALGORITHMS =
Use of SUPPORTED_ALGORITHMS and SUPPORTED_VERSIONS is deprecated. Use ALGORITHM_FOR_VERSION instead
["sha1"].freeze
- SUPPORTED_VERSIONS =
["1.0", "1.1"].freeze
- DEFAULT_SIGN_ALGORITHM =
"sha1".freeze
- DEFAULT_PROTO_VERSION =
"1.0".freeze
Class Method Summary collapse
-
.signing_object(args = {}) ⇒ Object
signing_object This is the intended interface for signing requests with the Opscode/Chef signed header protocol.
Instance Method Summary collapse
- #algorithm ⇒ Object
-
#canonicalize_request(sign_algorithm = algorithm, sign_version = proto_version) ⇒ Object
Takes HTTP request method & headers and creates a canonical form to create the signature.
-
#do_sign(rsa_key, digest, sign_algorithm, sign_version, use_ssh_agent) ⇒ String
private
Low-level RSA signature implementation used in #sign.
-
#do_sign_ssh_agent(rsa_key, string_to_sign) ⇒ String
private
Low-level signing logic for using ssh-agent.
- #hashed_body(digest = OpenSSL::Digest::SHA1) ⇒ Object
- #proto_version ⇒ Object
-
#sign(rsa_key, sign_algorithm = algorithm, sign_version = proto_version, **opts) ⇒ Object
Build the canonicalized request based on the method, other headers, etc.
- #validate_sign_version_digest!(sign_algorithm, sign_version) ⇒ Object
Class Method Details
.signing_object(args = {}) ⇒ Object
signing_object
This is the intended interface for signing requests with the Opscode/Chef signed header protocol. This wraps the constructor for a Struct that contains the relevant information about your request.
Signature Parameters:
These parameters are used to generate the canonical representation of the request, which is then hashed and encrypted to generate the request’s signature. These options are all required, with the exception of ‘:body` and `:file`, which are alternate ways to specify the request body (you must specify one of these).
-
‘:http_method`: HTTP method as a lowercase symbol, e.g., `:get | :put | :post | :delete`
-
‘:path`: The path part of the URI, e.g., `URI.parse(uri).path`
-
‘:body`: An object representing the body of the request. Use an empty String for bodiless requests.
-
‘:timestamp`: A String representing the time in any format understood by `Time.parse`. The server may reject the request if the timestamp is not close to the server’s current time.
-
‘:user_id`: The user or client name. This is used by the server to lookup the public key necessary to verify the signature.
-
‘:file`: An IO object (must respond to `:read`) to be used as the request body.
Protocol Versioning Parameters:
-
‘:proto_version`: The version of the signing protocol to use. Currently defaults to 1.0, but version 1.1 is also available.
Other Parameters:
These parameters are accepted but not used in the computation of the signature.
-
‘:host`: The host part of the URI
75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 75 def self.signing_object(args = {}) SigningObject.new(args[:http_method], args[:path], args[:body], args[:host], args[:timestamp], args[:user_id], args[:file], args[:proto_version], args[:headers]) end |
Instance Method Details
#algorithm ⇒ Object
87 88 89 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 87 def algorithm ALGORITHM_FOR_VERSION[proto_version] || DEFAULT_SIGN_ALGORITHM end |
#canonicalize_request(sign_algorithm = algorithm, sign_version = proto_version) ⇒ Object
Takes HTTP request method & headers and creates a canonical form to create the signature
Parameters
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 212 def canonicalize_request(sign_algorithm = algorithm, sign_version = proto_version) digest = validate_sign_version_digest!(sign_algorithm, sign_version) canonical_x_ops_user_id = canonicalize_user_id(user_id, sign_version, digest) case sign_version when "1.3" [ "Method:#{http_method.to_s.upcase}", "Path:#{canonical_path}", "X-Ops-Content-Hash:#{hashed_body(digest)}", "X-Ops-Sign:version=#{sign_version}", "X-Ops-Timestamp:#{canonical_time}", "X-Ops-UserId:#{canonical_x_ops_user_id}", "X-Ops-Server-API-Version:#{server_api_version}", ].join("\n") else [ "Method:#{http_method.to_s.upcase}", "Hashed Path:#{digester.hash_string(canonical_path, digest)}", "X-Ops-Content-Hash:#{hashed_body(digest)}", "X-Ops-Timestamp:#{canonical_time}", "X-Ops-UserId:#{canonical_x_ops_user_id}", ].join("\n") end end |
#do_sign(rsa_key, digest, sign_algorithm, sign_version, use_ssh_agent) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Low-level RSA signature implementation used in #sign.
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 277 def do_sign(rsa_key, digest, sign_algorithm, sign_version, use_ssh_agent) string_to_sign = canonicalize_request(sign_algorithm, sign_version) Mixlib::Authentication.logger.trace "String to sign: '#{string_to_sign}'" case sign_version when "1.3" if use_ssh_agent do_sign_ssh_agent(rsa_key, string_to_sign) else raise AuthenticationError, "RSA private key is required to sign requests, but a public key was provided" unless rsa_key.private? rsa_key.sign(digest.new, string_to_sign) end else raise AuthenticationError, "Agent signing mode requires signing protocol version 1.3 or newer" if use_ssh_agent raise AuthenticationError, "RSA private key is required to sign requests, but a public key was provided" unless rsa_key.private? rsa_key.private_encrypt(string_to_sign) end end |
#do_sign_ssh_agent(rsa_key, string_to_sign) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Low-level signing logic for using ssh-agent. This requires the user has already set up ssh-agent and used ssh-add to load in a (possibly encrypted) RSA private key. ssh-agent supports keys other than RSA, however they are not supported as Chef’s protocol explicitly requires RSA keys/sigs.
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 306 def do_sign_ssh_agent(rsa_key, string_to_sign) # First try loading net-ssh as it is an optional dependency. begin require "net/ssh" unless defined?(Net::SSH) rescue LoadError => e # ???: Since agent mode is explicitly enabled, should we even catch # this in the first place? Might be cleaner to let the LoadError bubble. raise AuthenticationError, "net-ssh gem is not available, unable to use ssh-agent signing: #{e.}" end # Try to connect to ssh-agent. begin agent = Net::SSH::Authentication::Agent.connect rescue Net::SSH::Authentication::AgentNotAvailable => e raise AuthenticationError, "Could not connect to ssh-agent. Make sure the SSH_AUTH_SOCK environment variable is set and ssh-agent is running: #{e.}" end begin ssh2_signature = agent.sign(rsa_key.public_key, string_to_sign, Net::SSH::Authentication::Agent::SSH_AGENT_RSA_SHA2_256) rescue Net::SSH::Authentication::AgentError => e raise AuthenticationError, "Unable to sign request with ssh-agent. Make sure your key is loaded with ssh-add: #{e.class.name} #{e.})" end # extract signature from SSH Agent response => skip first 20 bytes for RSA keys # "\x00\x00\x00\frsa-sha2-256\x00\x00\x01\x00" # (see http://api.libssh.org/rfc/PROTOCOL.agent for details) ssh2_signature[20..-1] end |
#hashed_body(digest = OpenSSL::Digest::SHA1) ⇒ Object
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 181 def hashed_body(digest = OpenSSL::Digest::SHA1) # This is weird. sign() is called with the digest type and signing # version. These are also expected to be properties of the object. # Hence, we're going to assume the one that is passed to sign is # the correct one and needs to passed through all the functions # that do any sort of digest. @hashed_body_digest = nil unless defined?(@hashed_body_digest) if !@hashed_body_digest.nil? && @hashed_body_digest != digest raise "hashed_body must always be called with the same digest" else @hashed_body_digest = digest end # Hash the file object if it was passed in, otherwise hash based on # the body. # TODO: tim 2009-12-28: It'd be nice to just remove this special case, # always sign the entire request body, using the expanded multipart # body in the case of a file being include. @hashed_body ||= if file&.respond_to?(:read) digester.hash_file(file, digest) else digester.hash_string(body, digest) end end |
#proto_version ⇒ Object
91 92 93 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 91 def proto_version DEFAULT_PROTO_VERSION end |
#sign(rsa_key, sign_algorithm = algorithm, sign_version = proto_version, **opts) ⇒ Object
Build the canonicalized request based on the method, other headers, etc. compute the signature from the request, using the looked-up user secret
102 103 104 105 106 107 108 109 110 111 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 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 102 def sign(rsa_key, sign_algorithm = algorithm, sign_version = proto_version, **opts) # Backwards compat stuff. if sign_algorithm.is_a?(Hash) # Was called like sign(key, sign_algorithm: 'foo', other: 'bar') opts.update(sign_algorithm) opts[:sign_algorithm] ||= algorithm opts[:sign_version] ||= sign_version else # Was called like sign(key, 'foo', '1.3', other: 'bar') Mixlib::Authentication.logger.warn("Using deprecated positional arguments for sign(), please update to keyword arguments (from #{caller[1][/^(.*:\d+):in /, 1]})") unless sign_algorithm == algorithm opts[:sign_algorithm] ||= sign_algorithm opts[:sign_version] ||= sign_version end sign_algorithm = opts[:sign_algorithm] sign_version = opts[:sign_version] use_ssh_agent = opts[:use_ssh_agent] digest = validate_sign_version_digest!(sign_algorithm, sign_version) # Our multiline hash for authorization will be encoded in multiple header # lines - X-Ops-Authorization-1, ... (starts at 1, not 0!) header_hash = { "X-Ops-Sign" => "algorithm=#{sign_algorithm};version=#{sign_version};", "X-Ops-Userid" => user_id, "X-Ops-Timestamp" => canonical_time, "X-Ops-Content-Hash" => hashed_body(digest), } signature = Base64.encode64(do_sign(rsa_key, digest, sign_algorithm, sign_version, use_ssh_agent)).chomp signature_lines = signature.split("\n") signature_lines.each_index do |idx| key = "X-Ops-Authorization-#{idx + 1}" header_hash[key] = signature_lines[idx] end Mixlib::Authentication.logger.trace "Header hash: #{header_hash.inspect}" header_hash end |
#validate_sign_version_digest!(sign_algorithm, sign_version) ⇒ Object
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/mixlib/authentication/signedheaderauth.rb', line 141 def validate_sign_version_digest!(sign_algorithm, sign_version) if ALGORITHM_FOR_VERSION[sign_version].nil? raise AuthenticationError, "Unsupported version '#{sign_version}'" end if ALGORITHM_FOR_VERSION[sign_version] != sign_algorithm raise AuthenticationError, "Unsupported algorithm #{sign_algorithm} for version '#{sign_version}'" end case sign_algorithm when "sha1" OpenSSL::Digest::SHA1 when "sha256" OpenSSL::Digest::SHA256 else # This case should never happen raise "Unknown algorithm #{sign_algorithm}" end end |