mixlib-authentication 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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