summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Whiteley <mwhiteley@fastly.com>2016-10-27 10:51:06 -0700
committerMatt Whiteley <mwhiteley@fastly.com>2017-09-06 09:15:58 -0700
commit3aa343e072238abc4319901c225d293a44aeb56c (patch)
tree4733ad89c5990c586f88c57540cd3b04029c3edd
parenta2fbf74d0b2123b9cc28e3baf7ca81b061036635 (diff)
downloadmixlib-authentication-3aa343e072238abc4319901c225d293a44aeb56c.tar.gz
CHEF-2381
Signed-off-by: Matt Whiteley <mwhiteley@fastly.com>
-rw-r--r--lib/mixlib/authentication/signedheaderauth.rb33
-rw-r--r--mixlib-authentication.gemspec2
-rw-r--r--spec/mixlib/authentication/mixlib_authentication_spec.rb22
3 files changed, 51 insertions, 6 deletions
diff --git a/lib/mixlib/authentication/signedheaderauth.rb b/lib/mixlib/authentication/signedheaderauth.rb
index e870135..b6be746 100644
--- a/lib/mixlib/authentication/signedheaderauth.rb
+++ b/lib/mixlib/authentication/signedheaderauth.rb
@@ -22,6 +22,7 @@ require "base64"
require "openssl/digest"
require "mixlib/authentication"
require "mixlib/authentication/digester"
+require "net/ssh"
module Mixlib
module Authentication
@@ -96,8 +97,11 @@ module Mixlib
# 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<OpenSSL::PKey::RSA>:: user's RSA private key.
- def sign(private_key, sign_algorithm = algorithm, sign_version = proto_version)
+ # keypair<OpenSSL::PKey::RSA>:: user's RSA keypair. The OpenSSL::PKey::RSA
+ # container can either be filled with a private/public keypair or just a
+ # public key. From x-ops protocol version 1.3 on the sign method will look
+ # out to sign the request via a ssh-agent, if only a public key is present.
+ def sign(keypair, 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!)
@@ -108,7 +112,7 @@ module Mixlib
"X-Ops-Content-Hash" => hashed_body(digest),
}
- signature = Base64.encode64(do_sign(private_key, digest, sign_algorithm, sign_version)).chomp
+ signature = Base64.encode64(do_sign(keypair, digest, sign_algorithm, sign_version)).chomp
signature_lines = signature.split(/\n/)
signature_lines.each_index do |idx|
key = "X-Ops-Authorization-#{idx + 1}"
@@ -245,14 +249,31 @@ module Mixlib
end
# private
- def do_sign(private_key, digest, sign_algorithm, sign_version)
+ def do_sign(keypair, digest, sign_algorithm, sign_version)
string_to_sign = canonicalize_request(sign_algorithm, sign_version)
Mixlib::Authentication.logger.debug "String to sign: '#{string_to_sign}'"
case sign_version
when "1.3"
- private_key.sign(digest.new, string_to_sign)
+ if keypair.private?
+ keypair.sign(digest.new, string_to_sign)
+ else
+ Mixlib::Authentication::Log.debug "No private key supplied, attempt to sign with ssh-agent."
+ begin
+ agent = Net::SSH::Authentication::Agent.connect
+ rescue => e
+ raise AuthenticationError, "Could not connect to ssh-agent. Make sure the SSH_AUTH_SOCK environment variable is set properly! (#{e.class.name}: #{e.message})"
+ end
+ begin
+ ssh2_signature = agent.sign(keypair.public_key, string_to_sign)
+ rescue => e
+ raise AuthenticationError, "Ssh-agent could not sign your request. Make sure your key is loaded with ssh-add! (#{e.class.name}: #{e.message})"
+ end
+ # extract signature from SSH Agent response => skip first 15 bytes for RSA keys
+ # (see http://api.libssh.org/rfc/PROTOCOL.agent for details)
+ ssh2_signature[20..-1]
+ end
else
- private_key.private_encrypt(string_to_sign)
+ keypair.private_encrypt(string_to_sign)
end
end
diff --git a/mixlib-authentication.gemspec b/mixlib-authentication.gemspec
index 0830546..8a01f94 100644
--- a/mixlib-authentication.gemspec
+++ b/mixlib-authentication.gemspec
@@ -12,6 +12,8 @@ Gem::Specification.new do |s|
s.email = "info@chef.io"
s.homepage = "https://www.chef.io"
+ s.add_dependency "net-ssh"
+
s.require_path = "lib"
s.files = %w{LICENSE README.md Gemfile Rakefile NOTICE} + Dir.glob("*.gemspec") +
Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) }
diff --git a/spec/mixlib/authentication/mixlib_authentication_spec.rb b/spec/mixlib/authentication/mixlib_authentication_spec.rb
index 221e803..8164a99 100644
--- a/spec/mixlib/authentication/mixlib_authentication_spec.rb
+++ b/spec/mixlib/authentication/mixlib_authentication_spec.rb
@@ -25,6 +25,7 @@ require "ostruct"
require "openssl"
require "mixlib/authentication/signatureverification"
require "time"
+require "net/ssh"
# TODO: should make these regular spec-based mock objects.
class MockRequest
@@ -99,6 +100,11 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do
# 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)
+
+ agent = double("ssh-agent")
+ allow(Net::SSH::Authentication::Agent).to receive(:connect).and_return(agent)
+ allow(agent).to receive(:sign).and_return(SSH_AGENT_RESPONSE)
+ expect(V1_3_SHA256_SIGNING_OBJECT.sign(PUBLIC_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
@@ -131,6 +137,18 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do
expect { V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha_poo", "1.1") }.to raise_error(Mixlib::Authentication::AuthenticationError)
end
+ it "should choke when signing a request via ssh-agent and ssh-agent is not reachable with version 1.3" do
+ expect { Net::SSH::Authentication::Agent.connect }.to raise_error(Net::SSH::Authentication::AgentNotAvailable)
+ expect { V1_3_SHA256_SIGNING_OBJECT.sign(PUBLIC_KEY) }.to raise_error(Mixlib::Authentication::AuthenticationError)
+ end
+
+ it "should choke when signing a request via ssh-agent and the key is not loaded with version 1.3" do
+ agent = double("ssh-agent")
+ allow(Net::SSH::Authentication::Agent).to receive(:connect).and_return(agent)
+ allow(agent).to receive(:sign).and_raise(Net::SSH::Authentication::AgentError)
+ expect { V1_3_SHA256_SIGNING_OBJECT.sign(PUBLIC_KEY) }.to raise_error(Mixlib::Authentication::AuthenticationError)
+ end
+
end
describe "Mixlib::Authentication::SignatureVerification" do
@@ -366,6 +384,8 @@ X_OPS_AUTHORIZATION_LINES_V1_3_SHA256 = [
"MmbLUIm3JRYi00Yb01IUCCKdI90vUq1HHNtlTEu93YZfQaJwRxXlGkCNwIJe",
"fy49QzaCIEu1XiOx5Jn+4GmkrZch/RrK9VzQWXgs+w==",
]
+
+SSH_AGENT_RESPONSE = "\x00\x00\x00\frsa-sha2-256\x00\x00\x01\x00\x15\x93\xA6\\\f\x8E\x04\x06PW\xFB\xB0\xD7\xCF\"\x06X\xC1%s\xA6\xFAo1C\xFF\nLb\xE4\x80l\x195\xC4E\xC6Mf\xF7\x9D\xD7\x8CM\xD6Tl\xB5tT\xFB\xE8\xA7\x9A5i\x8F\b\xDBC\x9A\x9A\xDF\x1Fi\xDA\xE5FE\xB5\xF2\xC8*\xB3\xEF\xEF\x19\xBC\xD1_\xA5\xCCL\xD3w\xD5\x81\xC2\xC7\xCF\xE3gY\xF4\xDF\x95\xF4\x8ERU\xF7\v\xFEU\xAB\xAEZ]\xC9\xB7\xDCx\x90\xB9\x8C\xE7\x0F\xE6\xDC\xDF%u\x94!<\e\xE9\x9D\xC4\xAE\r\xC3Su!\x1F\xD8}\x13J\x96\x95\x81\xAA\x9A#BV\xB0g\xA5\xEE\x92\x8BX\x14\xFC\x99~\xADyQH\xD6\xCB'\x81\xA5\x02\xB0\x0F\xB8\xBF{\xEA$\xD8%<\xC42f\xCBP\x89\xB7%\x16\"\xD3F\e\xD3R\x14\b\"\x9D#\xDD/R\xADG\x1C\xDBeLK\xBD\xDD\x86_A\xA2pG\x15\xE5\x1A@\x8D\xC0\x82^\x7F.=C6\x82 K\xB5^#\xB1\xE4\x99\xFE\xE0i\xA4\xAD\x97!\xFD\x1A\xCA\xF5\\\xD0Yx,\xFB"
# We expect Mixlib::Authentication::SignedHeaderAuth#sign to return this
# if passed the BODY above, based on version
@@ -529,6 +549,8 @@ YQIDAQAB
-----END PUBLIC KEY-----
EOS
+PUBLIC_KEY = OpenSSL::PKey::RSA.new(PUBLIC_KEY_DATA)
+
PRIVATE_KEY_DATA = <<EOS
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0ueqo76MXuP6XqZBILFziH/9AI7C6PaN5W0dSvkr9yInyGHS