mixlib-authentication-1.4.1/0000755000004100000410000000000012733464065016060 5ustar www-datawww-datamixlib-authentication-1.4.1/Rakefile0000644000004100000410000000055712733464065017534 0ustar www-datawww-datarequire "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task :default => :spec begin require "chefstyle" require "rubocop/rake_task" RuboCop::RakeTask.new(:style) do |task| task.options += ["--display-cop-names", "--no-color"] end rescue puts "chefstyle/rubocop is not available." end task :ci => [:style, :spec] mixlib-authentication-1.4.1/Gemfile0000644000004100000410000000011612733464065017351 0ustar www-datawww-datasource "https://rubygems.org" gemspec group(:development) do gem "pry" end mixlib-authentication-1.4.1/spec/0000755000004100000410000000000012733464065017012 5ustar www-datawww-datamixlib-authentication-1.4.1/spec/spec_helper.rb0000644000004100000410000000172512733464065021635 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.4.1/spec/mixlib/0000755000004100000410000000000012733464065020276 5ustar www-datawww-datamixlib-authentication-1.4.1/spec/mixlib/authentication/0000755000004100000410000000000012733464065023315 5ustar www-datawww-datamixlib-authentication-1.4.1/spec/mixlib/authentication/http_authentication_request_spec.rb0000644000004100000410000001334012733464065032503 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==" } expect(@http_authentication_request.headers).to eq(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 expect { Mixlib::Authentication::HTTPAuthenticationRequest.new(@request) }.to raise_error(exception) end it "extracts the path from the request" do expect(@http_authentication_request.path).to eq("/nodes") end it "extracts the request method from the request" do expect(@http_authentication_request.http_method).to eq("POST") end it "extracts the signing description from the request headers" do expect(@http_authentication_request.signing_description).to eq("version=1.0") end it "extracts the user_id from the request headers" do expect(@http_authentication_request.user_id).to eq("spec-user") end it "extracts the timestamp from the request headers" do expect(@http_authentication_request.timestamp).to eq("2009-01-01T12:00:00Z") end it "extracts the host from the request headers" do expect(@http_authentication_request.host).to eq("127.0.0.1") end it "extracts the content hash from the request headers" do expect(@http_authentication_request.content_hash).to eq("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 expect(@http_authentication_request.request_signature).to eq(expected.chomp) end it "defaults to server api version 0" do expect(@http_authentication_request.server_api_version).to eq("0") end end mixlib-authentication-1.4.1/spec/mixlib/authentication/mixlib_authentication_spec.rb0000644000004100000410000006040612733464065031245 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 expect(V1_0_SIGNING_OBJECT.canonicalize_request).to eq(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. expect(V1_0_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_0) end it "should generate the correct string to sign and signature, version 1.1" do expect(V1_1_SIGNING_OBJECT.proto_version).to eq("1.1") expect(V1_1_SIGNING_OBJECT.canonicalize_request).to eq(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. expect(V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_1) end it "should generate the correct string to sign and signature for version 1.3 with SHA256" do expect(V1_3_SHA256_SIGNING_OBJECT.proto_version).to eq("1.3") expect(V1_3_SHA256_SIGNING_OBJECT.algorithm).to eq("sha256") expect(V1_3_SHA256_SIGNING_OBJECT.server_api_version).to eq("1") expect(V1_3_SHA256_SIGNING_OBJECT.canonicalize_request).to eq(V1_3_SHA256_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. expect(V1_3_SHA256_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_3_SHA256) 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" expect(V1_1_SIGNING_OBJECT.proto_version).to eq("1.0") expect(V1_1_SIGNING_OBJECT.canonicalize_request(algorithm, version)).to eq(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. expect(V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, algorithm, version)).to eq(EXPECTED_SIGN_RESULT_V1_1) end it "should not choke when signing a request for a long user id with version 1.1" do expect { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha1", "1.1") }.not_to raise_error end it "should choke when signing a request for a long user id with version 1.0" do expect { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha1", "1.0") }.to raise_error(OpenSSL::PKey::RSAError) end it "should choke when signing a request with a bad version" do expect { V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha1", "poo") }.to raise_error(Mixlib::Authentication::AuthenticationError) end it "should choke when signing a request with a bad algorithm" do expect { V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha_poo", "1.1") }.to raise_error(Mixlib::Authentication::AuthenticationError) end end describe "Mixlib::Authentication::SignatureVerification" do before(:each) do @user_private_key = PRIVATE_KEY end it "should authenticate a File-containing request V1.1 - 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, "") expect(Time).to receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) expect(res).not_to be_nil end it "should authenticate a File-containing request V1.3 SHA256 - 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_3_SHA256, "") expect(Time).to receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) expect(res).not_to 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, "") expect(Time).to 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) expect(res).not_to be_nil end it "should authenticate a normal (post body) request v1.3 SHA256 - Merb" do mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, MERB_HEADERS_V1_3_SHA256, BODY) expect(Time).to receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) expect(res).not_to be_nil end it "should authenticate a normal (post body) request v1.1 - Merb" do mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, MERB_HEADERS_V1_1, BODY) expect(Time).to receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) expect(res).not_to 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) expect(Time).to receive(:now).at_least(:once).and_return(TIMESTAMP_OBJ) service = Mixlib::Authentication::SignatureVerification.new res = service.authenticate_user_request(mock_request, @user_private_key) expect(res).not_to 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) allow(Time).to receive(:now).and_return(TIMESTAMP_OBJ) #Time.stub!(:now).and_return(TIMESTAMP_OBJ) auth_req = Mixlib::Authentication::SignatureVerification.new expect { auth_req.authenticate_user_request(mock_request, @user_private_key) }.to raise_error(Mixlib::Authentication::AuthenticationError) expect(auth_req).not_to be_a_valid_request expect(auth_req).not_to be_a_valid_timestamp expect(auth_req).not_to be_a_valid_signature expect(auth_req).not_to 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) expect(Time).to 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) expect(res).to be_nil expect(auth_req).not_to be_a_valid_request expect(auth_req).to be_a_valid_timestamp expect(auth_req).to be_a_valid_signature expect(auth_req).not_to 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) expect(Time).to 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) expect(res).to be_nil expect(auth_req).not_to be_a_valid_request expect(auth_req).not_to be_a_valid_timestamp expect(auth_req).to be_a_valid_signature expect(auth_req).to 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) expect(Time).to 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) expect(res).to be_nil expect(auth_req).not_to be_a_valid_request expect(auth_req).not_to be_a_valid_signature expect(auth_req).to be_a_valid_timestamp expect(auth_req).to be_a_valid_content_hash end it "shouldn't authenticate if the signature is wrong for v1.3 SHA256" do headers = MERB_HEADERS_V1_3_SHA256.dup headers["HTTP_X_OPS_AUTHORIZATION_1"] = "epicfail" mock_request = MockRequest.new(PATH, MERB_REQUEST_PARAMS, headers, BODY) expect(Time).to 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) expect(res).to be_nil expect(auth_req).not_to be_a_valid_request expect(auth_req).not_to be_a_valid_signature expect(auth_req).to be_a_valid_timestamp expect(auth_req).to 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 HASHED_BODY_SHA256 = "hDlKNZhIhgso3Fs0S0pZwJ0xyBWtR1RBaeHs1DrzOho=" 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, } V1_3_ARGS_SHA256 = { :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.3", :headers => { "X-OpS-SeRvEr-ApI-VerSiOn" => "1", } # This defaults to sha256 } 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_CONTENT_HASH_SHA256 = "hDlKNZhIhgso3Fs0S0pZwJ0xyBWtR1RBaeHs1DrzOho=" 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==", ] X_OPS_AUTHORIZATION_LINES_V1_3_SHA256 = [ "FZOmXAyOBAZQV/uw188iBljBJXOm+m8xQ/8KTGLkgGwZNcRFxk1m953XjE3W", "VGy1dFT76KeaNWmPCNtDmprfH2na5UZFtfLIKrPv7xm80V+lzEzTd9WBwsfP", "42dZ9N+V9I5SVfcL/lWrrlpdybfceJC5jOcP5tzfJXWUITwb6Z3Erg3DU3Uh", "H9h9E0qWlYGqmiNCVrBnpe6Si1gU/Jl+rXlRSNbLJ4GlArAPuL976iTYJTzE", "MmbLUIm3JRYi00Yb01IUCCKdI90vUq1HHNtlTEu93YZfQaJwRxXlGkCNwIJe", "fy49QzaCIEu1XiOx5Jn+4GmkrZch/RrK9VzQWXgs+w==", ] # 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, } EXPECTED_SIGN_RESULT_V1_3_SHA256 = { "X-Ops-Content-Hash" => X_OPS_CONTENT_HASH_SHA256, "X-Ops-Userid" => USER_ID, "X-Ops-Sign" => "algorithm=sha256;version=1.3;", "X-Ops-Authorization-1" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[0], "X-Ops-Authorization-2" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[1], "X-Ops-Authorization-3" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[2], "X-Ops-Authorization-4" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[3], "X-Ops-Authorization-5" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[4], "X-Ops-Authorization-6" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[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 } MERB_HEADERS_V1_3_SHA256 = { # These are used by signatureverification. "HTTP_HOST" => "127.0.0.1", "HTTP_X_OPS_SIGN" => "algorithm=sha256;version=1.3;", "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_SHA256, "HTTP_X_OPS_USERID" => USER_ID, "HTTP_X_OPS_SERVER_API_VERSION" => "1", "HTTP_X_OPS_AUTHORIZATION_1" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[0], "HTTP_X_OPS_AUTHORIZATION_2" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[1], "HTTP_X_OPS_AUTHORIZATION_3" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[2], "HTTP_X_OPS_AUTHORIZATION_4" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[3], "HTTP_X_OPS_AUTHORIZATION_5" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[4], "HTTP_X_OPS_AUTHORIZATION_6" => X_OPS_AUTHORIZATION_LINES_V1_3_SHA256[5], }.merge(OTHER_HEADERS) # 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. # require "mixlib/log" module Mixlib module Authentication DEFAULT_SERVER_API_VERSION = "0" class AuthenticationError < StandardError end class MissingAuthenticationHeader < AuthenticationError end class Log extend Mixlib::Log end Log.level = :error end end mixlib-authentication-1.4.1/lib/mixlib/authentication/0000755000004100000410000000000012733464065023131 5ustar www-datawww-datamixlib-authentication-1.4.1/lib/mixlib/authentication/http_authentication_request.rb0000644000004100000410000000473112733464065031311 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.tr("-", "_").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 server_api_version (headers[:x_ops_server_api_version] || DEFAULT_SERVER_API_VERSION).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.4.1/lib/mixlib/authentication/signedheaderauth.rb0000644000004100000410000002504712733464065026772 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 "openssl/digest" require "mixlib/authentication" require "mixlib/authentication/digester" module Mixlib module Authentication module SignedHeaderAuth NULL_ARG = Object.new ALGORITHM_FOR_VERSION = { "1.0" => "sha1", "1.1" => "sha1", "1.3" => "sha256", }.freeze() # Use of SUPPORTED_ALGORITHMS and SUPPORTED_VERSIONS is deprecated. Use # ALGORITHM_FOR_VERSION instead 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], args[:headers] ) end def algorithm ALGORITHM_FOR_VERSION[proto_version] || 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) 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(private_key, digest, sign_algorithm, sign_version)).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 "Header hash: #{header_hash.inspect}" header_hash end 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 # 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(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. 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 self.file && self.file.respond_to?(:read) digester.hash_file(self.file, digest) else digester.hash_string(self.body, digest) end 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) 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 def canonicalize_user_id(user_id, proto_version, digest = OpenSSL::Digest::SHA1) case proto_version when "1.1" # and 1.2 if that ever gets implemented digester.hash_string(user_id, digest) else # versions 1.0 and 1.3 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 def do_sign(private_key, digest, sign_algorithm, sign_version) string_to_sign = canonicalize_request(sign_algorithm, sign_version) Mixlib::Authentication::Log.debug "String to sign: '#{string_to_sign}'" case sign_version when "1.3" private_key.sign(digest.new, string_to_sign) else private_key.private_encrypt(string_to_sign) end 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, :headers) include SignedHeaderAuth def proto_version (self[:proto_version] || DEFAULT_PROTO_VERSION).to_s end def server_api_version key = (self[:headers] || {}).keys.select do |k| k.downcase == "x-ops-server-api-version" end.first if key self[:headers][key] else DEFAULT_SERVER_API_VERSION end end end end end mixlib-authentication-1.4.1/lib/mixlib/authentication/digester.rb0000644000004100000410000000245612733464065025273 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" require "openssl" module Mixlib module Authentication class Digester class << self def hash_file(f, digest = OpenSSL::Digest::SHA1) digester = digest.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, digest = OpenSSL::Digest::SHA1) ::Base64.encode64(digest.digest(str)).chomp end end end end end mixlib-authentication-1.4.1/lib/mixlib/authentication/version.rb0000644000004100000410000000131312733464065025141 0ustar www-datawww-data# Copyright:: Copyright (c) 2010-2015 Chef Software, 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. module Mixlib module Authentication VERSION = "1.4.1" end end mixlib-authentication-1.4.1/lib/mixlib/authentication/signatureverification.rb0000644000004100000410000002151212733464065030063 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 def_delegator :@auth_request, :server_api_version 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.tr("-", "_").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) signature = Base64.decode64(request_signature) @valid_signature = case version when "1.3" digest = validate_sign_version_digest!(algorithm, version) @user_secret.verify(digest.new, signature, candidate_block) else request_decrypted_block = @user_secret.public_decrypt(signature) (request_decrypted_block == candidate_block) end # 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(digest = Digest::SHA1) 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, digest) else body = request.raw_post Mixlib::Authentication::Log.debug "Digesting body: '#{body}'" @hashed_body = digester.hash_string(body, digest) end end @hashed_body end # Compare the request timestamp with boundary time # # # ====Parameters # time1