mixlib-authentication-1.3.0/0000755000004100000410000000000012250223763016046 5ustar www-datawww-datamixlib-authentication-1.3.0/Rakefile0000644000004100000410000000174012250223763017515 0ustar www-datawww-datarequire 'rubygems' require 'rake/gempackagetask' require 'rubygems/specification' require 'date' require 'rspec/core/rake_task' GEM = "mixlib-authentication" GEM_VERSION = "1.2.1" AUTHOR = "Opscode, Inc." EMAIL = "info@opscode.com" HOMEPAGE = "http://www.opscode.com" SUMMARY = "Mixes in simple per-request authentication" task :default => :spec desc "Run specs" RSpec::Core::RakeTask.new do |t| t.pattern = 'spec/**/*_spec.rb' t.rspec_opts = %w(-fs --color) end gem_spec = eval(File.read("mixlib-authentication.gemspec")) Rake::GemPackageTask.new(gem_spec) do |pkg| pkg.gem_spec = gem_spec end desc "install the gem locally" task :install => [:package] do sh %{gem install pkg/#{GEM}-#{GEM_VERSION}} end desc "create a gemspec file" task :make_spec do File.open("#{GEM}.gemspec", "w") do |file| file.puts spec.to_ruby end end desc "remove build files" task :clean do sh %Q{ rm -f pkg/*.gem } end desc "Run the spec and features" task :test => [ :features, :spec ] mixlib-authentication-1.3.0/spec/0000755000004100000410000000000012250223763017000 5ustar www-datawww-datamixlib-authentication-1.3.0/spec/spec_helper.rb0000644000004100000410000000172712250223763021625 0ustar www-datawww-data# # Author:: Tim Hinderliter () # Author:: Christopher Walters () # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")) # lib in mixlib-authentication $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "mixlib-log", "lib")) # mixlib-log/log require 'rubygems' mixlib-authentication-1.3.0/spec/mixlib/0000755000004100000410000000000012250223763020264 5ustar www-datawww-datamixlib-authentication-1.3.0/spec/mixlib/authentication/0000755000004100000410000000000012250223763023303 5ustar www-datawww-datamixlib-authentication-1.3.0/spec/mixlib/authentication/http_authentication_request_spec.rb0000644000004100000410000001300312250223763032465 0ustar www-datawww-data# Author:: Daniel DeLeo () # Copyright:: Copyright (c) 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require File.expand_path(File.join(File.dirname(__FILE__), '..','..','spec_helper')) require 'mixlib/authentication' require 'mixlib/authentication/http_authentication_request' require 'ostruct' require 'pp' describe Mixlib::Authentication::HTTPAuthenticationRequest do before do request = Struct.new(:env, :method, :path) @timestamp_iso8601 = "2009-01-01T12:00:00Z" @x_ops_content_hash = "DFteJZPVv6WKdQmMqZUQUumUyRs=" @user_id = "spec-user" @http_x_ops_lines = [ "jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4", "NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", "3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O", "IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy", "9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", "utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w=="] @merb_headers = { # These are used by signatureverification. An arbitrary sampling of non-HTTP_* # headers are in here to exercise that code path. "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"version=1.0", "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", "HTTP_X_OPS_TIMESTAMP"=>@timestamp_iso8601, "HTTP_X_OPS_CONTENT_HASH"=>@x_ops_content_hash, "HTTP_X_OPS_USERID"=>@user_id, "HTTP_X_OPS_AUTHORIZATION_1"=>@http_x_ops_lines[0], "HTTP_X_OPS_AUTHORIZATION_2"=>@http_x_ops_lines[1], "HTTP_X_OPS_AUTHORIZATION_3"=>@http_x_ops_lines[2], "HTTP_X_OPS_AUTHORIZATION_4"=>@http_x_ops_lines[3], "HTTP_X_OPS_AUTHORIZATION_5"=>@http_x_ops_lines[4], "HTTP_X_OPS_AUTHORIZATION_6"=>@http_x_ops_lines[5], # Random sampling "REMOTE_ADDR"=>"127.0.0.1", "PATH_INFO"=>"/organizations/local-test-org/cookbooks", "REQUEST_PATH"=>"/organizations/local-test-org/cookbooks", "CONTENT_TYPE"=>"multipart/form-data; boundary=----RubyMultipartClient6792ZZZZZ", "CONTENT_LENGTH"=>"394", } @request = request.new(@merb_headers, "POST", '/nodes') @http_authentication_request = Mixlib::Authentication::HTTPAuthenticationRequest.new(@request) end it "normalizes the headers to lowercase symbols" do expected = {:host=>"127.0.0.1", :x_ops_sign=>"version=1.0", :x_ops_requestid=>"127.0.0.1 1258566194.85386", :x_ops_timestamp=>"2009-01-01T12:00:00Z", :x_ops_content_hash=>"DFteJZPVv6WKdQmMqZUQUumUyRs=", :x_ops_userid=>"spec-user", :x_ops_authorization_1=>"jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4", :x_ops_authorization_2=>"NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", :x_ops_authorization_3=>"3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O", :x_ops_authorization_4=>"IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy", :x_ops_authorization_5=>"9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", :x_ops_authorization_6=>"utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w=="} @http_authentication_request.headers.should == expected end it "raises an error when not all required headers are given" do @merb_headers.delete("HTTP_X_OPS_SIGN") exception = Mixlib::Authentication::MissingAuthenticationHeader lambda{ Mixlib::Authentication::HTTPAuthenticationRequest.new(@request) }.should raise_error(exception) end it "extracts the path from the request" do @http_authentication_request.path.should == '/nodes' end it "extracts the request method from the request" do @http_authentication_request.http_method.should == 'POST' end it "extracts the signing description from the request headers" do @http_authentication_request.signing_description.should == 'version=1.0' end it "extracts the user_id from the request headers" do @http_authentication_request.user_id.should == 'spec-user' end it "extracts the timestamp from the request headers" do @http_authentication_request.timestamp.should == "2009-01-01T12:00:00Z" end it "extracts the host from the request headers" do @http_authentication_request.host.should == "127.0.0.1" end it "extracts the content hash from the request headers" do @http_authentication_request.content_hash.should == "DFteJZPVv6WKdQmMqZUQUumUyRs=" end it "rebuilds the request signature from the headers" do expected=<<-SIG jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4 NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc 3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy 9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0 utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w== SIG @http_authentication_request.request_signature.should == expected.chomp end end mixlib-authentication-1.3.0/spec/mixlib/authentication/mixlib_authentication_spec.rb0000644000004100000410000004520712250223763031235 0ustar www-datawww-data# # Author:: Tim Hinderliter () # Author:: Christopher Walters () # Author:: Christopher Brown () # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require File.expand_path(File.join(File.dirname(__FILE__), '..','..','spec_helper')) require 'rubygems' require 'ostruct' require 'openssl' require 'mixlib/authentication/signatureverification' require 'time' # TODO: should make these regular spec-based mock objects. class MockRequest attr_accessor :env, :params, :path, :raw_post def initialize(path, params, headers, raw_post) @path = path @params = params @env = headers @raw_post = raw_post end def method "POST" end end class MockFile def initialize @have_read = nil end def self.length BODY.length end def read(len, out_str) if @have_read.nil? @have_read = 1 out_str[0..-1] = BODY BODY else nil end end end # Uncomment this to get some more info from the methods we're testing. #Mixlib::Authentication::Log.logger = Logger.new(STDERR) #Mixlib::Authentication::Log.level :debug describe "Mixlib::Authentication::SignedHeaderAuth" do # NOTE: Version 1.0 will be the default until Chef 11 is released. it "should generate the correct string to sign and signature, version 1.0 (default)" do V1_0_SIGNING_OBJECT.canonicalize_request.should == V1_0_CANONICAL_REQUEST # If you need to regenerate the constants in this test spec, print out # the results of res.inspect and copy them as appropriate into the # the constants in this file. V1_0_SIGNING_OBJECT.sign(PRIVATE_KEY).should == EXPECTED_SIGN_RESULT_V1_0 end it "should generate the correct string to sign and signature, version 1.1" do V1_1_SIGNING_OBJECT.proto_version.should == "1.1" V1_1_SIGNING_OBJECT.canonicalize_request.should == V1_1_CANONICAL_REQUEST # If you need to regenerate the constants in this test spec, print out # the results of res.inspect and copy them as appropriate into the # the constants in this file. V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY).should == EXPECTED_SIGN_RESULT_V1_1 end it "should generate the correct string to sign and signature for non-default proto version when used as a mixin" do algorithm = 'sha1' version = '1.1' V1_1_SIGNING_OBJECT.proto_version = "1.0" V1_1_SIGNING_OBJECT.proto_version.should == "1.0" V1_1_SIGNING_OBJECT.canonicalize_request(algorithm, version).should == V1_1_CANONICAL_REQUEST # If you need to regenerate the constants in this test spec, print out # the results of res.inspect and copy them as appropriate into the # the constants in this file. V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, algorithm, version).should == EXPECTED_SIGN_RESULT_V1_1 end it "should not choke when signing a request for a long user id with version 1.1" do lambda { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, 'sha1', '1.1') }.should_not raise_error end it "should choke when signing a request for a long user id with version 1.0" do lambda { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, 'sha1', '1.0') }.should raise_error end it "should choke when signing a request with a bad version" do lambda { V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, 'sha1', 'poo') }.should raise_error end it "should choke when signing a request with a bad algorithm" do lambda { V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, 'sha_poo', '1.1') }.should raise_error end end describe "Mixlib::Authentication::SignatureVerification" do before(:each) do @user_private_key = PRIVATE_KEY end it "should authenticate a File-containing request - Merb" do request_params = MERB_REQUEST_PARAMS.clone request_params["file"] = { "size"=>MockFile.length, "content_type"=>"application/octet-stream", "filename"=>"zsh.tar.gz", "tempfile"=>MockFile.new } mock_request = MockRequest.new(PATH, request_params, MERB_HEADERS_V1_1, "") Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) res.should_not be_nil end it "should authenticate a File-containing request from a v1.0 client - Passenger" do request_params = PASSENGER_REQUEST_PARAMS.clone request_params["tarball"] = MockFile.new mock_request = MockRequest.new(PATH, request_params, PASSENGER_HEADERS_V1_0, "") Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) auth_req = Mixlib::Authentication::SignatureVerification.new res = auth_req.authenticate_user_request(mock_request, @user_private_key) res.should_not be_nil end it "should authenticate a normal (post body) request - Merb" do mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, MERB_HEADERS_V1_1, BODY) Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) res.should_not be_nil end it "should authenticate a normal (post body) request from a v1.0 client - Merb" do mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, MERB_HEADERS_V1_0, BODY) Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) res.should_not be_nil end it "shouldn't authenticate if an Authorization header is missing" do headers = MERB_HEADERS_V1_1.clone headers.delete("HTTP_X_OPS_SIGN") mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY) Time.stub!(:now).and_return(TIMESTAMP_OBJ) auth_req = Mixlib::Authentication::SignatureVerification.new lambda {auth_req.authenticate_user_request(mock_request, @user_private_key)}.should raise_error(Mixlib::Authentication::AuthenticationError) auth_req.should_not be_a_valid_request auth_req.should_not be_a_valid_timestamp auth_req.should_not be_a_valid_signature auth_req.should_not be_a_valid_content_hash end it "shouldn't authenticate if Authorization header is wrong" do headers = MERB_HEADERS_V1_1.clone headers["HTTP_X_OPS_CONTENT_HASH"] += "_" mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY) Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) auth_req = Mixlib::Authentication::SignatureVerification.new res = auth_req.authenticate_user_request(mock_request, @user_private_key) res.should be_nil auth_req.should_not be_a_valid_request auth_req.should be_a_valid_timestamp auth_req.should be_a_valid_signature auth_req.should_not be_a_valid_content_hash end it "shouldn't authenticate if the timestamp is not within bounds" do mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, MERB_HEADERS_V1_1, BODY) Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ - 1000) auth_req = Mixlib::Authentication::SignatureVerification.new res = auth_req.authenticate_user_request(mock_request, @user_private_key) res.should be_nil auth_req.should_not be_a_valid_request auth_req.should_not be_a_valid_timestamp auth_req.should be_a_valid_signature auth_req.should be_a_valid_content_hash end it "shouldn't authenticate if the signature is wrong" do headers = MERB_HEADERS_V1_1.dup headers["HTTP_X_OPS_AUTHORIZATION_1"] = "epicfail" mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY) Time.should_receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) auth_req = Mixlib::Authentication::SignatureVerification.new res = auth_req.authenticate_user_request(mock_request, @user_private_key) res.should be_nil auth_req.should_not be_a_valid_request auth_req.should_not be_a_valid_signature auth_req.should be_a_valid_timestamp auth_req.should be_a_valid_content_hash end end USER_ID = "spec-user" DIGESTED_USER_ID = Base64.encode64(Digest::SHA1.new.digest(USER_ID)).chomp BODY = "Spec Body" HASHED_BODY = "DFteJZPVv6WKdQmMqZUQUumUyRs=" # Base64.encode64(Digest::SHA1.digest("Spec Body")).chomp TIMESTAMP_ISO8601 = "2009-01-01T12:00:00Z" TIMESTAMP_OBJ = Time.parse("Thu Jan 01 12:00:00 -0000 2009") PATH = "/organizations/clownco" HASHED_CANONICAL_PATH = "YtBWDn1blGGuFIuKksdwXzHU9oE=" # Base64.encode64(Digest::SHA1.digest("/organizations/clownco")).chomp V1_0_ARGS = { :body => BODY, :user_id => USER_ID, :http_method => :post, :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. :file => MockFile.new, :path => PATH } V1_1_ARGS = { :body => BODY, :user_id => USER_ID, :http_method => :post, :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. :file => MockFile.new, :path => PATH, :proto_version => 1.1 } LONG_PATH_LONG_USER_ARGS = { :body => BODY, :user_id => "A" * 200, :http_method => :put, :timestamp => TIMESTAMP_ISO8601, # fixed timestamp so we get back the same answer each time. :file => MockFile.new, :path => PATH + "/nodes/#{"A" * 250}" } REQUESTING_ACTOR_ID = "c0f8a68c52bffa1020222a56b23cccfa" # Content hash is ???TODO X_OPS_CONTENT_HASH = "DFteJZPVv6WKdQmMqZUQUumUyRs=" X_OPS_AUTHORIZATION_LINES_V1_0 = [ "jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4", "NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc", "3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O", "IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy", "9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", "utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w==" ] X_OPS_AUTHORIZATION_LINES = [ "UfZD9dRz6rFu6LbP5Mo1oNHcWYxpNIcUfFCffJS1FQa0GtfU/vkt3/O5HuCM", "1wIFl/U0f5faH9EWpXWY5NwKR031Myxcabw4t4ZLO69CIh/3qx1XnjcZvt2w", "c2R9bx/43IWA/r8w8Q6decuu0f6ZlNheJeJhaYPI8piX/aH+uHBH8zTACZu8", "vMnl5MF3/OIlsZc8cemq6eKYstp8a8KYq9OmkB5IXIX6qVMJHA6fRvQEB/7j", "281Q7oI/O+lE8AmVyBbwruPb7Mp6s4839eYiOdjbDwFjYtbS3XgAjrHlaD7W", "FDlbAG7H8Dmvo+wBxmtNkszhzbBnEYtuwQqT8nM/8A==" ] # We expect Mixlib::Authentication::SignedHeaderAuth#sign to return this # if passed the BODY above, based on version EXPECTED_SIGN_RESULT_V1_0 = { "X-Ops-Content-Hash"=>X_OPS_CONTENT_HASH, "X-Ops-Userid"=>USER_ID, "X-Ops-Sign"=>"algorithm=sha1;version=1.0;", "X-Ops-Authorization-1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], "X-Ops-Authorization-2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], "X-Ops-Authorization-3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], "X-Ops-Authorization-4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], "X-Ops-Authorization-5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], "X-Ops-Authorization-6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], "X-Ops-Timestamp"=>TIMESTAMP_ISO8601 } EXPECTED_SIGN_RESULT_V1_1 = { "X-Ops-Content-Hash"=>X_OPS_CONTENT_HASH, "X-Ops-Userid"=>USER_ID, "X-Ops-Sign"=>"algorithm=sha1;version=1.1;", "X-Ops-Authorization-1"=>X_OPS_AUTHORIZATION_LINES[0], "X-Ops-Authorization-2"=>X_OPS_AUTHORIZATION_LINES[1], "X-Ops-Authorization-3"=>X_OPS_AUTHORIZATION_LINES[2], "X-Ops-Authorization-4"=>X_OPS_AUTHORIZATION_LINES[3], "X-Ops-Authorization-5"=>X_OPS_AUTHORIZATION_LINES[4], "X-Ops-Authorization-6"=>X_OPS_AUTHORIZATION_LINES[5], "X-Ops-Timestamp"=>TIMESTAMP_ISO8601 } OTHER_HEADERS = { # An arbitrary sampling of non-HTTP_* headers are in here to # exercise that code path. "REMOTE_ADDR"=>"127.0.0.1", "PATH_INFO"=>"/organizations/local-test-org/cookbooks", "REQUEST_PATH"=>"/organizations/local-test-org/cookbooks", "CONTENT_TYPE"=>"multipart/form-data; boundary=----RubyMultipartClient6792ZZZZZ", "CONTENT_LENGTH"=>"394", } # This is what will be in request.params for the Merb case. MERB_REQUEST_PARAMS = { "name"=>"zsh", "action"=>"create", "controller"=>"chef_server_api/cookbooks", "organization_id"=>"local-test-org", "requesting_actor_id"=>REQUESTING_ACTOR_ID, } # Tis is what will be in request.env for the Merb case. MERB_HEADERS_V1_1 = { # These are used by signatureverification. "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"algorithm=sha1;version=1.1;", "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, "HTTP_X_OPS_USERID"=>USER_ID, "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES[0], "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES[1], "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES[2], "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES[3], "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES[4], "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES[5], }.merge(OTHER_HEADERS) # Tis is what will be in request.env for the Merb case. MERB_HEADERS_V1_0 = { # These are used by signatureverification. "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"version=1.0", "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, "HTTP_X_OPS_USERID"=>USER_ID, "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], }.merge(OTHER_HEADERS) PASSENGER_REQUEST_PARAMS = { "action"=>"create", #"tarball"=>#, "controller"=>"api/v1/cookbooks", "cookbook"=>"{\"category\":\"databases\"}", } PASSENGER_HEADERS_V1_1 = { # These are used by signatureverification. "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"algorithm=sha1;version=1.1;", "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, "HTTP_X_OPS_USERID"=>USER_ID, "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES[0], "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES[1], "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES[2], "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES[3], "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES[4], "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES[5], }.merge(OTHER_HEADERS) PASSENGER_HEADERS_V1_0 = { # These are used by signatureverification. "HTTP_HOST"=>"127.0.0.1", "HTTP_X_OPS_SIGN"=>"version=1.0", "HTTP_X_OPS_REQUESTID"=>"127.0.0.1 1258566194.85386", "HTTP_X_OPS_TIMESTAMP"=>TIMESTAMP_ISO8601, "HTTP_X_OPS_CONTENT_HASH"=>X_OPS_CONTENT_HASH, "HTTP_X_OPS_USERID"=>USER_ID, "HTTP_X_OPS_AUTHORIZATION_1"=>X_OPS_AUTHORIZATION_LINES_V1_0[0], "HTTP_X_OPS_AUTHORIZATION_2"=>X_OPS_AUTHORIZATION_LINES_V1_0[1], "HTTP_X_OPS_AUTHORIZATION_3"=>X_OPS_AUTHORIZATION_LINES_V1_0[2], "HTTP_X_OPS_AUTHORIZATION_4"=>X_OPS_AUTHORIZATION_LINES_V1_0[3], "HTTP_X_OPS_AUTHORIZATION_5"=>X_OPS_AUTHORIZATION_LINES_V1_0[4], "HTTP_X_OPS_AUTHORIZATION_6"=>X_OPS_AUTHORIZATION_LINES_V1_0[5], }.merge(OTHER_HEADERS) # generated with # openssl genrsa -out private.pem 2048 # openssl rsa -in private.pem -out public.pem -pubout PUBLIC_KEY_DATA = <) Copyright:: Copyright (c) 2009 Opscode, Inc. License:: Apache License, Version 2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. == Issue Tracker Report any issues via Opscode Jira instance at http://tickets.opscode.com against Chef project. mixlib-authentication-1.3.0/lib/0000755000004100000410000000000012250223763016614 5ustar www-datawww-datamixlib-authentication-1.3.0/lib/mixlib/0000755000004100000410000000000012250223763020100 5ustar www-datawww-datamixlib-authentication-1.3.0/lib/mixlib/authentication.rb0000644000004100000410000000171312250223763023446 0ustar www-datawww-data# # Author:: Christopher Brown () # Copyright:: Copyright (c) 2009 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'mixlib/log' module Mixlib module Authentication class AuthenticationError < StandardError end class MissingAuthenticationHeader < AuthenticationError end class Log extend Mixlib::Log end Log.level = :error end end mixlib-authentication-1.3.0/lib/mixlib/authentication/0000755000004100000410000000000012250223763023117 5ustar www-datawww-datamixlib-authentication-1.3.0/lib/mixlib/authentication/http_authentication_request.rb0000644000004100000410000000454612250223763031303 0ustar www-datawww-data# # Author:: Daniel DeLeo () # Copyright:: Copyright (c) 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'mixlib/authentication' module Mixlib module Authentication class HTTPAuthenticationRequest MANDATORY_HEADERS = [:x_ops_sign, :x_ops_userid, :x_ops_timestamp, :host, :x_ops_content_hash] attr_reader :request def initialize(request) @request = request @request_signature = nil validate_headers! end def headers @headers ||= @request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo } end def http_method @request.method.to_s end def path @request.path.to_s end def signing_description headers[:x_ops_sign].chomp end def user_id headers[:x_ops_userid].chomp end def timestamp headers[:x_ops_timestamp].chomp end def host headers[:host].chomp end def content_hash headers[:x_ops_content_hash].chomp end def request_signature unless @request_signature @request_signature = headers.find_all { |h| h[0].to_s =~ /^x_ops_authorization_/ }.sort { |x,y| x.to_s <=> y.to_s}.map { |i| i[1] }.join("\n") Mixlib::Authentication::Log.debug "Reconstituted (user-supplied) request signature: #{@request_signature}" end @request_signature end def validate_headers! missing_headers = MANDATORY_HEADERS - headers.keys unless missing_headers.empty? missing_headers.map! { |h| h.to_s.upcase } raise MissingAuthenticationHeader, "missing required authentication header(s) '#{missing_headers.join("', '")}'" end end end end end mixlib-authentication-1.3.0/lib/mixlib/authentication/signedheaderauth.rb0000644000004100000410000001673012250223763026757 0ustar www-datawww-data# # Author:: Christopher Brown () # Author:: Christopher Walters () # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'time' require 'base64' require 'digest/sha1' require 'mixlib/authentication' require 'mixlib/authentication/digester' module Mixlib module Authentication module SignedHeaderAuth NULL_ARG = Object.new SUPPORTED_ALGORITHMS = ['sha1'].freeze SUPPORTED_VERSIONS = ['1.0', '1.1'].freeze DEFAULT_SIGN_ALGORITHM = 'sha1'.freeze DEFAULT_PROTO_VERSION = '1.0'.freeze # === 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 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]) end def algorithm DEFAULT_SIGN_ALGORITHM end def proto_version DEFAULT_PROTO_VERSION end # Build the canonicalized request based on the method, other headers, etc. # compute the signature from the request, using the looked-up user secret # ====Parameters # private_key:: user's RSA private key. def sign(private_key, sign_algorithm=algorithm, sign_version=proto_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, } string_to_sign = canonicalize_request(sign_algorithm, sign_version) signature = Base64.encode64(private_key.private_encrypt(string_to_sign)).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::Log.debug "String to sign: '#{string_to_sign}'\nHeader hash: #{header_hash.inspect}" header_hash end # Build the canonicalized time based on utc & iso8601 # # ====Parameters # def canonical_time Time.parse(timestamp).utc.iso8601 end # Build the canonicalized path, which collapses multiple slashes (/) and # removes a trailing slash unless the path is only "/" # # ====Parameters # def canonical_path p = path.gsub(/\/+/,'/') p.length > 1 ? p.chomp('/') : p end def hashed_body # 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 ||= (self.file && self.file.respond_to?(:read)) ? digester.hash_file(self.file) : digester.hash_string(self.body) end # Takes HTTP request method & headers and creates a canonical form # to create the signature # # ====Parameters # # def canonicalize_request(sign_algorithm=algorithm, sign_version=proto_version) unless SUPPORTED_ALGORITHMS.include?(sign_algorithm) && SUPPORTED_VERSIONS.include?(sign_version) raise AuthenticationError, "Bad algorithm '#{sign_algorithm}' (allowed: #{SUPPORTED_ALGORITHMS.inspect}) or version '#{sign_version}' (allowed: #{SUPPORTED_VERSIONS.inspect})" end canonical_x_ops_user_id = canonicalize_user_id(user_id, sign_version) "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:#{canonical_x_ops_user_id}" end def canonicalize_user_id(user_id, proto_version) case proto_version when "1.1" digester.hash_string(user_id) when "1.0" user_id else user_id end end # Parses signature version information, algorithm used, etc. # # ====Parameters # def parse_signing_description parts = signing_description.strip.split(";").inject({ }) do |memo, part| field_name, field_value = part.split("=") memo[field_name.to_sym] = field_value.strip memo end Mixlib::Authentication::Log.debug "Parsed signing description: #{parts.inspect}" parts end def digester Mixlib::Authentication::Digester end private :canonical_time, :canonical_path, :parse_signing_description, :digester, :canonicalize_user_id end # === SigningObject # A Struct-based value object that contains the necessary information to # generate a request signature. `SignedHeaderAuth.signing_object()` # provides a more convenient interface to the constructor. class SigningObject < Struct.new(:http_method, :path, :body, :host, :timestamp, :user_id, :file, :proto_version) include SignedHeaderAuth def proto_version (self[:proto_version] or DEFAULT_PROTO_VERSION).to_s end end end end mixlib-authentication-1.3.0/lib/mixlib/authentication/digester.rb0000644000004100000410000000241112250223763025250 0ustar www-datawww-data# # Author:: Christopher Brown () # Copyright:: Copyright (c) 2009 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'mixlib/authentication' module Mixlib module Authentication class Digester class << self def hash_file(f) digester = Digest::SHA1.new buf = "" while f.read(16384, buf) digester.update buf end ::Base64.encode64(digester.digest).chomp end # Digests a string, base64's and chomps the end # # ====Parameters # def hash_string(str) ::Base64.encode64(Digest::SHA1.digest(str)).chomp end end end end end mixlib-authentication-1.3.0/lib/mixlib/authentication/signatureverification.rb0000644000004100000410000002060612250223763030054 0ustar www-datawww-data# # Author:: Christopher Brown () # Author:: Christopher Walters () # Copyright:: Copyright (c) 2009, 2010 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'net/http' require 'forwardable' require 'mixlib/authentication' require 'mixlib/authentication/http_authentication_request' require 'mixlib/authentication/signedheaderauth' module Mixlib module Authentication SignatureResponse = Struct.new(:name) class SignatureVerification extend Forwardable def_delegator :@auth_request, :http_method def_delegator :@auth_request, :path def_delegator :@auth_request, :signing_description def_delegator :@auth_request, :user_id def_delegator :@auth_request, :timestamp def_delegator :@auth_request, :host def_delegator :@auth_request, :request_signature def_delegator :@auth_request, :content_hash def_delegator :@auth_request, :request include Mixlib::Authentication::SignedHeaderAuth def initialize(request=nil) @auth_request = HTTPAuthenticationRequest.new(request) if request @valid_signature, @valid_timestamp, @valid_content_hash = false, false, false @hashed_body = nil end def authenticate_user_request(request, user_lookup, time_skew=(15*60)) @auth_request = HTTPAuthenticationRequest.new(request) authenticate_request(user_lookup, time_skew) end # Takes the request, boils down the pieces we are interested in, # looks up the user, generates a signature, and compares to # the signature in the request # ====Headers # # X-Ops-Sign: algorithm=sha1;version=1.0; # X-Ops-UserId: # X-Ops-Timestamp: # X-Ops-Content-Hash: # X-Ops-Authorization-#{line_number} def authenticate_request(user_secret, time_skew=(15*60)) Mixlib::Authentication::Log.debug "Initializing header auth : #{request.inspect}" @user_secret = user_secret @allowed_time_skew = time_skew # in seconds begin parts = parse_signing_description # version 1.0 clients don't include their algorithm in the # signing description, so default to sha1 parts[:algorithm] ||= 'sha1' verify_signature(parts[:algorithm], parts[:version]) verify_timestamp verify_content_hash rescue StandardError=>se raise AuthenticationError,"Failed to authenticate user request. Check your client key and clock: #{se.message}", se.backtrace end if valid_request? SignatureResponse.new(user_id) else nil end end def valid_signature? @valid_signature end def valid_timestamp? @valid_timestamp end def valid_content_hash? @valid_content_hash end def valid_request? valid_signature? && valid_timestamp? && valid_content_hash? end # The authorization header is a Base64-encoded version of an RSA signature. # The client sent it on multiple header lines, starting at index 1 - # X-Ops-Authorization-1, X-Ops-Authorization-2, etc. Pull them out and # concatenate. def headers @headers ||= request.env.inject({ }) { |memo, kv| memo[$2.gsub(/\-/,"_").downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo } end private def assert_required_headers_present MANDATORY_HEADERS.each do |header| unless headers.key?(header) raise MissingAuthenticationHeader, "required authentication header #{header.to_s.upcase} missing" end end end def verify_signature(algorithm, version) candidate_block = canonicalize_request(algorithm, version) request_decrypted_block = @user_secret.public_decrypt(Base64.decode64(request_signature)) @valid_signature = (request_decrypted_block == candidate_block) # Keep the debug messages lined up so it's easy to scan them Mixlib::Authentication::Log.debug("Verifying request signature:") Mixlib::Authentication::Log.debug(" Expected Block is: '#{candidate_block}'") Mixlib::Authentication::Log.debug("Decrypted block is: '#{request_decrypted_block}'") Mixlib::Authentication::Log.debug("Signatures match? : '#{@valid_signature}'") @valid_signature rescue => e Mixlib::Authentication::Log.debug("Failed to verify request signature: #{e.class.name}: #{e.message}") @valid_signature = false end def verify_timestamp @valid_timestamp = timestamp_within_bounds?(Time.parse(timestamp), Time.now) end def verify_content_hash @valid_content_hash = (content_hash == hashed_body) # Keep the debug messages lined up so it's easy to scan them Mixlib::Authentication::Log.debug("Expected content hash is: '#{hashed_body}'") Mixlib::Authentication::Log.debug(" Request Content Hash is: '#{content_hash}'") Mixlib::Authentication::Log.debug(" Hashes match?: #{@valid_content_hash}") @valid_content_hash end # The request signature is based on any file attached, if any. Otherwise # it's based on the body of the request. def hashed_body unless @hashed_body # TODO: tim: 2009-112-28: It'd be nice to remove this special case, and # always hash the entire request body. In the file case it would just be # expanded multipart text - the entire body of the POST. # # Pull out any file that was attached to this request, using multipart # form uploads. # Depending on the server we're running in, multipart form uploads are # handed to us differently. # - In Passenger (Cookbooks Community Site), the File is handed to us # directly in the params hash. The name is whatever the client used, # its value is therefore a File or Tempfile. # e.g. request['file_param'] = File # # - In Merb (Chef server), the File is wrapped. The original parameter # name used for the file is used, but its value is a Hash. Within # the hash is a name/value pair named 'file' which actually # contains the Tempfile instance. # e.g. request['file_param'] = { :file => Tempfile } file_param = request.params.values.find { |value| value.respond_to?(:read) } # No file_param; we're running in Merb, or it's just not there.. if file_param.nil? hash_param = request.params.values.find { |value| value.respond_to?(:has_key?) } # Hash responds to :has_key? . if !hash_param.nil? file_param = hash_param.values.find { |value| value.respond_to?(:read) } # File/Tempfile responds to :read. end end # Any file that's included in the request is hashed if it's there. Otherwise, # we hash the body. if file_param Mixlib::Authentication::Log.debug "Digesting file_param: '#{file_param.inspect}'" @hashed_body = digester.hash_file(file_param) else body = request.raw_post Mixlib::Authentication::Log.debug "Digesting body: '#{body}'" @hashed_body = digester.hash_string(body) end end @hashed_body end # Compare the request timestamp with boundary time # # # ====Parameters # time1