mixlib-authentication 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30042f8e3ac0f6ca2d793fa73556e19d41146554c6673788e2c0d7225e9f3584
4
- data.tar.gz: bed096ad15f32dca36dddfe79deb4f7b9f54a8656849b68393ce900944f3cc5b
3
+ metadata.gz: 8d5824d5178ca85fc7eb82bbd0699c21d0a3ddbbc97c9b41d78c24c6dd78af11
4
+ data.tar.gz: 522ee0cc91c67a8c824dc25d7d605cfd833f9c5c636435787cdfe8f0bc67d7b2
5
5
  SHA512:
6
- metadata.gz: f2bc4eff96e238290c9d3ec9cfc490b62372b9f4b0668dae56055f88844c8b8b971183000e4189d5ecfd8a4cc8b902aee753f103fecb839ad1bf9b33716d1840
7
- data.tar.gz: 0a03fd4b0a2016b93b681ba5a174dcc49089bd2aab2c2601bad114970a0680fe091bd562a6de4a0918f24e52b3eb512159287a239fb0cf1654ca91c452e34a4f
6
+ metadata.gz: c1843f8908adb974f06173dc2a9bfc97a5b18fe11be4bbf8769c9e2af716e0cc259e275b5b2b00aa41b9d442f9e2e1f2fc8e111d5f46645e8e7544a3e8b44933
7
+ data.tar.gz: c8b6d61be5b2e9c8db0d6527f94c8ef347fbcc8aa08735dc0f00b10f82a90687a71a4058f1489a805b19ce296c1ae8f9bd7b1981e24fbac1c4ef6e9fe04ea742
data/Gemfile CHANGED
@@ -3,5 +3,6 @@ gemspec
3
3
 
4
4
  group(:development) do
5
5
  gem "pry"
6
- gem "mixlib-log", '~> 2'
6
+ gem "mixlib-log", "~> 2"
7
+ gem "net-ssh"
7
8
  end
@@ -95,9 +95,28 @@ module Mixlib
95
95
 
96
96
  # Build the canonicalized request based on the method, other headers, etc.
97
97
  # compute the signature from the request, using the looked-up user secret
98
- # ====Parameters
99
- # private_key<OpenSSL::PKey::RSA>:: user's RSA private key.
100
- def sign(private_key, sign_algorithm = algorithm, sign_version = proto_version)
98
+ #
99
+ # @param rsa_key [OpenSSL::PKey::RSA] User's RSA key. If `use_ssh_agent` is
100
+ # true, this must have the public key portion populated. If `use_ssh_agent`
101
+ # is false, this must have the private key portion populated.
102
+ # @param use_ssh_agent [Boolean] If true, use ssh-agent for request signing.
103
+ def sign(rsa_key, sign_algorithm = algorithm, sign_version = proto_version, **opts)
104
+ # Backwards compat stuff.
105
+ if sign_algorithm.is_a?(Hash)
106
+ # Was called like sign(key, sign_algorithm: 'foo', other: 'bar')
107
+ opts.update(sign_algorithm)
108
+ opts[:sign_algorithm] ||= algorithm
109
+ opts[:sign_version] ||= sign_version
110
+ else
111
+ # Was called like sign(key, 'foo', '1.3', other: 'bar')
112
+ Mixlib::Authentication.logger.warn("Using deprecated positional arguments for sign(), please update to keyword arguments (from #{caller[1][/^(.*:\d+):in /, 1]})")
113
+ opts[:sign_algorithm] ||= sign_algorithm
114
+ opts[:sign_version] ||= sign_version
115
+ end
116
+ sign_algorithm = opts[:sign_algorithm]
117
+ sign_version = opts[:sign_version]
118
+ use_ssh_agent = opts[:use_ssh_agent]
119
+
101
120
  digest = validate_sign_version_digest!(sign_algorithm, sign_version)
102
121
  # Our multiline hash for authorization will be encoded in multiple header
103
122
  # lines - X-Ops-Authorization-1, ... (starts at 1, not 0!)
@@ -108,7 +127,7 @@ module Mixlib
108
127
  "X-Ops-Content-Hash" => hashed_body(digest),
109
128
  }
110
129
 
111
- signature = Base64.encode64(do_sign(private_key, digest, sign_algorithm, sign_version)).chomp
130
+ signature = Base64.encode64(do_sign(rsa_key, digest, sign_algorithm, sign_version, use_ssh_agent)).chomp
112
131
  signature_lines = signature.split(/\n/)
113
132
  signature_lines.each_index do |idx|
114
133
  key = "X-Ops-Authorization-#{idx + 1}"
@@ -244,18 +263,73 @@ module Mixlib
244
263
  Mixlib::Authentication::Digester
245
264
  end
246
265
 
247
- # private
248
- def do_sign(private_key, digest, sign_algorithm, sign_version)
266
+ # Low-level RSA signature implementation used in {#sign}.
267
+ #
268
+ # @api private
269
+ # @param rsa_key [OpenSSL::PKey::RSA] User's RSA key. If `use_ssh_agent` is
270
+ # true, this must have the public key portion populated. If `use_ssh_agent`
271
+ # is false, this must have the private key portion populated.
272
+ # @param digest [Class] Sublcass of OpenSSL::Digest to use while signing.
273
+ # @param sign_algorithm [String] Hash algorithm to use while signing.
274
+ # @param sign_version [String] Version number of the signing protocol to use.
275
+ # @param use_ssh_agent [Boolean] If true, use ssh-agent for request signing.
276
+ # @return [String]
277
+ def do_sign(rsa_key, digest, sign_algorithm, sign_version, use_ssh_agent)
249
278
  string_to_sign = canonicalize_request(sign_algorithm, sign_version)
250
279
  Mixlib::Authentication.logger.trace "String to sign: '#{string_to_sign}'"
251
280
  case sign_version
252
281
  when "1.3"
253
- private_key.sign(digest.new, string_to_sign)
282
+ if use_ssh_agent
283
+ do_sign_ssh_agent(rsa_key, string_to_sign)
284
+ else
285
+ raise AuthenticationError, "RSA private key is required to sign requests, but a public key was provided" unless rsa_key.private?
286
+ rsa_key.sign(digest.new, string_to_sign)
287
+ end
254
288
  else
255
- private_key.private_encrypt(string_to_sign)
289
+ raise AuthenticationError, "Agent signing mode requires signing protocol version 1.3 or newer" if use_ssh_agent
290
+ raise AuthenticationError, "RSA private key is required to sign requests, but a public key was provided" unless rsa_key.private?
291
+ rsa_key.private_encrypt(string_to_sign)
256
292
  end
257
293
  end
258
294
 
295
+ # Low-level signing logic for using ssh-agent. This requires the user has
296
+ # already set up ssh-agent and used ssh-add to load in a (possibly encrypted)
297
+ # RSA private key. ssh-agent supports keys other than RSA, however they
298
+ # are not supported as Chef's protocol explicitly requires RSA keys/sigs.
299
+ #
300
+ # @api private
301
+ # @param rsa_key [OpenSSL::PKey::RSA] User's RSA public key.
302
+ # @param string_to_sign [String] String data to sign with the requested key.
303
+ # @return [String]
304
+ def do_sign_ssh_agent(rsa_key, string_to_sign)
305
+ # First try loading net-ssh as it is an optional dependency.
306
+ begin
307
+ require "net/ssh"
308
+ rescue LoadError => e
309
+ # ???: Since agent mode is explicitly enabled, should we even catch
310
+ # this in the first place? Might be cleaner to let the LoadError bubble.
311
+ raise AuthenticationError, "net-ssh gem is not available, unable to use ssh-agent signing: #{e.message}"
312
+ end
313
+
314
+ # Try to connect to ssh-agent.
315
+ begin
316
+ agent = Net::SSH::Authentication::Agent.connect
317
+ rescue Net::SSH::Authentication::AgentNotAvailable => e
318
+ raise AuthenticationError, "Could not connect to ssh-agent. Make sure the SSH_AUTH_SOCK environment variable is set and ssh-agent is running: #{e.message}"
319
+ end
320
+
321
+ begin
322
+ ssh2_signature = agent.sign(rsa_key.public_key, string_to_sign, Net::SSH::Authentication::Agent::SSH_AGENT_RSA_SHA2_256)
323
+ rescue Net::SSH::Authentication::AgentError => e
324
+ raise AuthenticationError, "Unable to sign request with ssh-agent. Make sure your key is loaded with ssh-add: #{e.class.name} #{e.message})"
325
+ end
326
+
327
+ # extract signature from SSH Agent response => skip first 20 bytes for RSA keys
328
+ # "\x00\x00\x00\frsa-sha2-256\x00\x00\x01\x00"
329
+ # (see http://api.libssh.org/rfc/PROTOCOL.agent for details)
330
+ ssh2_signature[20..-1]
331
+ end
332
+
259
333
  private :canonical_time, :canonical_path, :parse_signing_description, :digester, :canonicalize_user_id
260
334
 
261
335
  end
@@ -15,6 +15,6 @@
15
15
 
16
16
  module Mixlib
17
17
  module Authentication
18
- VERSION = "2.0.0"
18
+ VERSION = "2.1.0"
19
19
  end
20
20
  end
@@ -25,6 +25,7 @@ require "ostruct"
25
25
  require "openssl"
26
26
  require "mixlib/authentication/signatureverification"
27
27
  require "time"
28
+ require "net/ssh"
28
29
 
29
30
  # TODO: should make these regular spec-based mock objects.
30
31
  class MockRequest
@@ -101,6 +102,13 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do
101
102
  expect(V1_3_SHA256_SIGNING_OBJECT.sign(PRIVATE_KEY)).to eq(EXPECTED_SIGN_RESULT_V1_3_SHA256)
102
103
  end
103
104
 
105
+ it "should generate the correct string to sign and signature for version 1.3 with SHA256 via ssh-agent" do
106
+ agent = double("ssh-agent")
107
+ expect(Net::SSH::Authentication::Agent).to receive(:connect).and_return(agent)
108
+ expect(agent).to receive(:sign).and_return(SSH_AGENT_RESPONSE)
109
+ expect(V1_3_SHA256_SIGNING_OBJECT.sign(PUBLIC_KEY, use_ssh_agent: true)).to eq(EXPECTED_SIGN_RESULT_V1_3_SHA256)
110
+ end
111
+
104
112
  it "should generate the correct string to sign and signature for non-default proto version when used as a mixin" do
105
113
  algorithm = "sha1"
106
114
  version = "1.1"
@@ -113,14 +121,17 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do
113
121
  # the results of res.inspect and copy them as appropriate into the
114
122
  # the constants in this file.
115
123
  expect(V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, algorithm, version)).to eq(EXPECTED_SIGN_RESULT_V1_1)
124
+ expect(V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, sign_algorithm: algorithm, sign_version: version)).to eq(EXPECTED_SIGN_RESULT_V1_1)
116
125
  end
117
126
 
118
127
  it "should not choke when signing a request for a long user id with version 1.1" do
119
128
  expect { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha1", "1.1") }.not_to raise_error
129
+ expect { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, sign_algorithm: "sha1", sign_version: "1.1") }.not_to raise_error
120
130
  end
121
131
 
122
132
  it "should choke when signing a request for a long user id with version 1.0" do
123
133
  expect { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha1", "1.0") }.to raise_error(OpenSSL::PKey::RSAError)
134
+ expect { LONG_SIGNING_OBJECT.sign(PRIVATE_KEY, sign_algorithm: "sha1", sign_version: "1.0") }.to raise_error(OpenSSL::PKey::RSAError)
124
135
  end
125
136
 
126
137
  it "should choke when signing a request with a bad version" do
@@ -131,6 +142,18 @@ describe "Mixlib::Authentication::SignedHeaderAuth" do
131
142
  expect { V1_1_SIGNING_OBJECT.sign(PRIVATE_KEY, "sha_poo", "1.1") }.to raise_error(Mixlib::Authentication::AuthenticationError)
132
143
  end
133
144
 
145
+ it "should choke when signing a request via ssh-agent and ssh-agent is not reachable with version 1.3" do
146
+ expect(Net::SSH::Authentication::Agent).to receive(:connect).and_raise(Net::SSH::Authentication::AgentNotAvailable)
147
+ expect { V1_3_SHA256_SIGNING_OBJECT.sign(PUBLIC_KEY, use_ssh_agent: true) }.to raise_error(Mixlib::Authentication::AuthenticationError)
148
+ end
149
+
150
+ it "should choke when signing a request via ssh-agent and the key is not loaded with version 1.3" do
151
+ agent = double("ssh-agent")
152
+ expect(Net::SSH::Authentication::Agent).to receive(:connect).and_return(agent)
153
+ expect(agent).to receive(:sign).and_raise(Net::SSH::Authentication::AgentError)
154
+ expect { V1_3_SHA256_SIGNING_OBJECT.sign(PUBLIC_KEY, use_ssh_agent: true) }.to raise_error(Mixlib::Authentication::AuthenticationError)
155
+ end
156
+
134
157
  end
135
158
 
136
159
  describe "Mixlib::Authentication::SignatureVerification" do
@@ -366,6 +389,8 @@ X_OPS_AUTHORIZATION_LINES_V1_3_SHA256 = [
366
389
  "MmbLUIm3JRYi00Yb01IUCCKdI90vUq1HHNtlTEu93YZfQaJwRxXlGkCNwIJe",
367
390
  "fy49QzaCIEu1XiOx5Jn+4GmkrZch/RrK9VzQWXgs+w==",
368
391
  ]
392
+
393
+ 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"
369
394
  # We expect Mixlib::Authentication::SignedHeaderAuth#sign to return this
370
395
  # if passed the BODY above, based on version
371
396
 
@@ -529,6 +554,8 @@ YQIDAQAB
529
554
  -----END PUBLIC KEY-----
530
555
  EOS
531
556
 
557
+ PUBLIC_KEY = OpenSSL::PKey::RSA.new(PUBLIC_KEY_DATA)
558
+
532
559
  PRIVATE_KEY_DATA = <<EOS
533
560
  -----BEGIN RSA PRIVATE KEY-----
534
561
  MIIEpAIBAAKCAQEA0ueqo76MXuP6XqZBILFziH/9AI7C6PaN5W0dSvkr9yInyGHS
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixlib-authentication
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chef Software, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-12 00:00:00.000000000 Z
11
+ date: 2018-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core
@@ -124,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
124
  version: '0'
125
125
  requirements: []
126
126
  rubyforge_project:
127
- rubygems_version: 2.7.3
127
+ rubygems_version: 2.7.6
128
128
  signing_key:
129
129
  specification_version: 4
130
130
  summary: Mixes in simple per-request authentication