2
# Author:: Christopher Brown (<cb@opscode.com>)
3
# Author:: Christopher Walters (<cw@opscode.com>)
4
# Copyright:: Copyright (c) 2009, 2010 Opscode, Inc.
5
# License:: Apache License, Version 2.0
7
# Licensed under the Apache License, Version 2.0 (the "License");
8
# you may not use this file except in compliance with the License.
9
# You may obtain a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS,
15
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
# See the License for the specific language governing permissions and
17
# limitations under the License.
23
require 'mixlib/authentication'
24
require 'mixlib/authentication/digester'
29
module SignedHeaderAuth
31
SIGNING_DESCRIPTION = 'version=1.0'
33
# This is a module meant to be mixed in but can be used standalone
34
# with the simple OpenStruct extended with the auth functions
36
def signing_object(args={ })
37
SigningObject.new(args[:http_method], args[:path], args[:body], args[:host], args[:timestamp], args[:user_id], args[:file])
41
# Build the canonicalized request based on the method, other headers, etc.
42
# compute the signature from the request, using the looked-up user secret
44
# private_key<OpenSSL::PKey::RSA>:: user's RSA private key.
46
# Our multiline hash for authorization will be encoded in multiple header
47
# lines - X-Ops-Authorization-1, ... (starts at 1, not 0!)
49
"X-Ops-Sign" => SIGNING_DESCRIPTION,
50
"X-Ops-Userid" => user_id,
51
"X-Ops-Timestamp" => canonical_time,
52
"X-Ops-Content-Hash" => hashed_body,
55
string_to_sign = canonicalize_request
56
signature = Base64.encode64(private_key.private_encrypt(string_to_sign)).chomp
57
signature_lines = signature.split(/\n/)
58
signature_lines.each_index do |idx|
59
key = "X-Ops-Authorization-#{idx + 1}"
60
header_hash[key] = signature_lines[idx]
63
Mixlib::Authentication::Log.debug "String to sign: '#{string_to_sign}'\nHeader hash: #{header_hash.inspect}"
68
# Build the canonicalized time based on utc & iso8601
73
Time.parse(timestamp).utc.iso8601
76
# Build the canonicalized path, which collapses multiple slashes (/) and
77
# removes a trailing slash unless the path is only "/"
82
p = path.gsub(/\/+/,'/')
83
p.length > 1 ? p.chomp('/') : p
87
# Hash the file object if it was passed in, otherwise hash based on
89
# TODO: tim 2009-12-28: It'd be nice to just remove this special case,
90
# always sign the entire request body, using the expanded multipart
91
# body in the case of a file being include.
92
@hashed_body ||= (self.file && self.file.respond_to?(:read)) ? digester.hash_file(self.file) : digester.hash_string(self.body)
95
# Takes HTTP request method & headers and creates a canonical form
96
# to create the signature
101
def canonicalize_request
102
"Method:#{http_method.to_s.upcase}\nHashed Path:#{digester.hash_string(canonical_path)}\nX-Ops-Content-Hash:#{hashed_body}\nX-Ops-Timestamp:#{canonical_time}\nX-Ops-UserId:#{user_id}"
105
# Parses signature version information, algorithm used, etc.
109
def parse_signing_description
110
parts = signing_description.strip.split(";").inject({ }) do |memo, part|
111
field_name, field_value = part.split("=")
112
memo[field_name.to_sym] = field_value.strip
115
Mixlib::Authentication::Log.debug "Parsed signing description: #{parts.inspect}"
119
Mixlib::Authentication::Digester
122
private :canonical_time, :canonical_path, :parse_signing_description, :digester
126
class SigningObject < Struct.new(:http_method, :path, :body, :host, :timestamp, :user_id, :file)
127
include SignedHeaderAuth