conjur-api 6.2.0.pre.802 → 6.2.0.pre.806

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: fc34dbf11e2c9189c0b8583c7442b11ef2f5afb47c160570d2873bebf8e4caa5
4
- data.tar.gz: 5dc92b0ce02f936ee1c44ec09dd8a44bb86cbe13da116f0679aaf63d86f8e660
3
+ metadata.gz: 4eb698cae0bb3c7abf0e8f8283ef11c7c2ef5790fdb144d3aa6ccb8a75cb2146
4
+ data.tar.gz: 4dcb5fca7170062cfa751d52f830fcaaa9244d218064d1e064988a69ab8912c4
5
5
  SHA512:
6
- metadata.gz: bc1c204249a7c8345bf6142c3c3279f59346568cedca381a5cd52c7d472f3d667f174eae78b84690bd4063605aaa3b5adff6348d4b3ab840a1bbb36a5b296f44
7
- data.tar.gz: 4a565eb2017e1fe8b824d899580b5a6509149a5ed1be172cbdbfea9fc33f13bdd9e81c244030a9454e05a57550e9b9a60791137dd4984aa61cfb3eade99ba038
6
+ metadata.gz: bbe620e6fcb2653eb04f7a8355a3155e1e98f32f9f5c52818c5b208aae9cb121ccc9e23e575755135a8a91729af08af3ab2a5444584ea10422dea3d3c8d9bedd
7
+ data.tar.gz: 445c664bf985458b7139c14af8552f603778fe9c31f400fe3e4513b5e7aaa92c5182a7128427f4303171ea4f66ac6d5df3c426fa3a29f3efa89688edabf886c9
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.2.0-802
1
+ 6.2.0-806
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'json'
5
+
6
+ module Conjur
7
+ class API
8
+ # Authenticator that uses mutual TLS (authn-cert) to obtain Conjur access tokens.
9
+ # The client certificate is presented during the TLS handshake; an empty POST
10
+ # to the authn-cert endpoint returns a Conjur access token.
11
+ #
12
+ # Two modes:
13
+ # - Request mode: +host_id+ is provided; Conjur validates the cert matches the host.
14
+ # - SPIFFE mode: +host_id+ is nil; Conjur derives the host from the cert's SPIFFE SAN URI.
15
+ class CertAuthenticator
16
+ include TokenExpiration
17
+
18
+ attr_reader :account, :service_id, :cert, :key, :host_id
19
+
20
+ def initialize(account, service_id, cert, key, host_id = nil)
21
+ @account = account
22
+ @service_id = service_id
23
+ @cert = cert
24
+ @key = key
25
+ @host_id = host_id
26
+ update_token_born
27
+ end
28
+
29
+ def refresh_token
30
+ Conjur::API.authenticate_cert(service_id, cert, key, account: account, host_id: host_id).tap do
31
+ update_token_born
32
+ end
33
+ end
34
+ end
35
+
36
+ class << self
37
+ # Create a new {Conjur::API} instance authenticated via client certificate (authn-cert / mTLS).
38
+ #
39
+ # The +cert+ and +key+ arguments may each be:
40
+ # * An already-loaded OpenSSL object (+OpenSSL::X509::Certificate+ / +OpenSSL::PKey::PKey+)
41
+ # * A file path (String) — the file will be read and parsed
42
+ # * A PEM-encoded string — parsed directly
43
+ #
44
+ # @param [String] service_id the authn-cert service ID configured in Conjur
45
+ # @param [OpenSSL::X509::Certificate, String] cert client certificate
46
+ # @param [OpenSSL::PKey::PKey, String] key client private key
47
+ # @param [String] account The organization account.
48
+ # @param [String, nil] host_id Conjur host path for request mode (e.g. "host/workloads/vm-01").
49
+ # Pass +nil+ or omit for SPIFFE mode.
50
+ # @param [String, nil] remote_ip optional IP address recorded in the audit log.
51
+ # @return [Conjur::API]
52
+ def new_from_cert(service_id, cert, key,
53
+ account: Conjur.configuration.account,
54
+ host_id: nil,
55
+ remote_ip: nil)
56
+ cert = load_cert(cert)
57
+ key = load_key(key)
58
+ self.new.init_from_cert(service_id, cert, key,
59
+ account: account, host_id: host_id, remote_ip: remote_ip)
60
+ end
61
+
62
+ # Authenticate using authn-cert and return a parsed Conjur access token.
63
+ #
64
+ # +cert+ and +key+ must already be OpenSSL objects (use {.new_from_cert} for automatic loading).
65
+ #
66
+ # @param [String] service_id the authn-cert service ID
67
+ # @param [OpenSSL::X509::Certificate] cert client certificate
68
+ # @param [OpenSSL::PKey::PKey] key client private key
69
+ # @param [String] account The organization account.
70
+ # @param [String, nil] host_id Conjur host path for request mode; nil for SPIFFE mode.
71
+ # @return [Hash] parsed access token
72
+ def authenticate_cert(service_id, cert, key,
73
+ account: Conjur.configuration.account,
74
+ host_id: nil)
75
+ raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
76
+
77
+ if Conjur.log
78
+ Conjur.log << "Authenticating via authn-cert/#{service_id} to account #{account}\n"
79
+ end
80
+ cert_options = { ssl_client_cert: cert, ssl_client_key: key }
81
+ JSON.parse(url_for(:authn_cert_authenticate, account, service_id, host_id, cert_options).post(''))
82
+ end
83
+
84
+ private
85
+
86
+ def load_cert(cert)
87
+ return cert if cert.is_a?(OpenSSL::X509::Certificate)
88
+ pem = File.file?(cert.to_s) ? File.read(cert.to_s) : cert.to_s
89
+ OpenSSL::X509::Certificate.new(pem)
90
+ end
91
+
92
+ def load_key(key)
93
+ return key if key.is_a?(OpenSSL::PKey::PKey)
94
+ pem = File.file?(key.to_s) ? File.read(key.to_s) : key.to_s
95
+ OpenSSL::PKey.read(pem)
96
+ end
97
+ end
98
+
99
+ def init_from_cert(service_id, cert, key,
100
+ account: Conjur.configuration.account,
101
+ host_id: nil,
102
+ remote_ip: nil)
103
+ @remote_ip = remote_ip
104
+ @authenticator = CertAuthenticator.new(account, service_id, cert, key, host_id)
105
+ self
106
+ end
107
+ end
108
+ end
@@ -42,6 +42,18 @@ module Conjur
42
42
  )[fully_escape account][fully_escape username]['authenticate']
43
43
  end
44
44
 
45
+ # Builds the RestClient::Resource for an authn-cert authentication request.
46
+ # cert_options must include :ssl_client_cert and :ssl_client_key so that
47
+ # the client certificate is presented during the TLS handshake.
48
+ def authn_cert_authenticate account, service_id, host_id, cert_options
49
+ options = Conjur.configuration.create_rest_client_options(cert_options)
50
+ resource = RestClient::Resource.new(
51
+ Conjur.configuration.core_url,
52
+ options
53
+ )['authn-cert'][fully_escape service_id][fully_escape account]
54
+ host_id ? resource[fully_escape host_id]['authenticate'] : resource['authenticate']
55
+ end
56
+
45
57
  def authenticator_authenticate(account, service_id, authenticator, options)
46
58
  RestClient::Resource.new(
47
59
  Conjur.configuration.core_url,
data/lib/conjur/api.rb CHANGED
@@ -36,6 +36,7 @@ require 'conjur/log_source'
36
36
  require 'conjur/has_attributes'
37
37
  require 'conjur/api/authenticators'
38
38
  require 'conjur/api/authn'
39
+ require 'conjur/api/authn_cert'
39
40
  require 'conjur/api/roles'
40
41
  require 'conjur/api/resources'
41
42
  require 'conjur/api/pubkeys'
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'openssl'
5
+ require 'tempfile'
6
+ require 'conjur/api/router'
7
+
8
+ # Generate once for the entire spec file — RSA key generation is expensive.
9
+ TEST_KEY = OpenSSL::PKey::RSA.new(512)
10
+ TEST_CERT = OpenSSL::X509::Certificate.new.tap do |c|
11
+ c.subject = c.issuer = OpenSSL::X509::Name.parse('/CN=test')
12
+ c.not_before = Time.now - 1
13
+ c.not_after = Time.now + 3600
14
+ c.public_key = TEST_KEY.public_key
15
+ c.serial = 1
16
+ c.sign(TEST_KEY, OpenSSL::Digest::SHA256.new)
17
+ end
18
+
19
+ describe Conjur::API, api: :dummy do
20
+ let(:service_id) { 'my-service' }
21
+ let(:host_id) { 'host/workloads/vm-01' }
22
+
23
+ let(:key) { TEST_KEY }
24
+ let(:cert) { TEST_CERT }
25
+
26
+ let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
27
+
28
+ describe '.new_from_cert' do
29
+ it 'returns an API instance with a CertAuthenticator' do
30
+ api_instance = Conjur::API.new_from_cert(service_id, cert, key, account: account)
31
+ expect(api_instance).to be_a(Conjur::API)
32
+ expect(api_instance.authenticator).to be_a(Conjur::API::CertAuthenticator)
33
+ end
34
+
35
+ it 'accepts file paths for cert and key' do
36
+ cert_file = Tempfile.new(['cert', '.pem'])
37
+ key_file = Tempfile.new(['key', '.pem'])
38
+ begin
39
+ cert_file.write(cert.to_pem)
40
+ key_file.write(key.to_pem)
41
+ cert_file.close
42
+ key_file.close
43
+
44
+ api_instance = Conjur::API.new_from_cert(service_id, cert_file.path, key_file.path,
45
+ account: account)
46
+ expect(api_instance.authenticator).to be_a(Conjur::API::CertAuthenticator)
47
+ ensure
48
+ cert_file.unlink
49
+ key_file.unlink
50
+ end
51
+ end
52
+
53
+ it 'accepts PEM strings for cert and key' do
54
+ api_instance = Conjur::API.new_from_cert(service_id, cert.to_pem, key.to_pem,
55
+ account: account)
56
+ expect(api_instance.authenticator).to be_a(Conjur::API::CertAuthenticator)
57
+ end
58
+ end
59
+
60
+ describe '.authenticate_cert' do
61
+ let(:resource) { double('resource') }
62
+
63
+ before do
64
+ allow(Conjur::API).to receive(:url_for)
65
+ .with(:authn_cert_authenticate, account, service_id, host_id, anything)
66
+ .and_return(resource)
67
+ allow(resource).to receive(:post).with('').and_return(raw_token.to_json)
68
+ end
69
+
70
+ it 'returns a parsed token' do
71
+ token = Conjur::API.authenticate_cert(service_id, cert, key,
72
+ account: account, host_id: host_id)
73
+ expect(token).to eq(raw_token)
74
+ end
75
+ end
76
+
77
+ describe '.authenticate_cert (SPIFFE mode)' do
78
+ let(:resource) { double('resource') }
79
+
80
+ before do
81
+ allow(Conjur::API).to receive(:url_for)
82
+ .with(:authn_cert_authenticate, account, service_id, nil, anything)
83
+ .and_return(resource)
84
+ allow(resource).to receive(:post).with('').and_return(raw_token.to_json)
85
+ end
86
+
87
+ it 'returns a parsed token without host_id' do
88
+ token = Conjur::API.authenticate_cert(service_id, cert, key, account: account)
89
+ expect(token).to eq(raw_token)
90
+ end
91
+ end
92
+
93
+ describe 'CertAuthenticator#refresh_token' do
94
+ subject(:authenticator) do
95
+ Conjur::API::CertAuthenticator.new(account, service_id, cert, key, host_id)
96
+ end
97
+
98
+ it 'calls authenticate_cert with the stored credentials' do
99
+ expect(Conjur::API).to receive(:authenticate_cert)
100
+ .with(service_id, cert, key, account: account, host_id: host_id)
101
+ .and_return(raw_token)
102
+ expect(authenticator.refresh_token).to eq(raw_token)
103
+ end
104
+ end
105
+
106
+ describe 'CertAuthenticator (SPIFFE mode)' do
107
+ subject(:authenticator) do
108
+ Conjur::API::CertAuthenticator.new(account, service_id, cert, key)
109
+ end
110
+
111
+ it 'calls authenticate_cert without host_id' do
112
+ expect(Conjur::API).to receive(:authenticate_cert)
113
+ .with(service_id, cert, key, account: account, host_id: nil)
114
+ .and_return(raw_token)
115
+ expect(authenticator.refresh_token).to eq(raw_token)
116
+ end
117
+ end
118
+ end
119
+
120
+ describe Conjur::API::Router do
121
+ let(:account) { 'myaccount' }
122
+ let(:service_id) { 'my-service' }
123
+ let(:cert) { double('cert') }
124
+ let(:key) { double('key') }
125
+ let(:cert_opts) { { ssl_client_cert: cert, ssl_client_key: key } }
126
+
127
+ before do
128
+ allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
129
+ allow(Conjur.configuration).to receive(:create_rest_client_options) do |opts|
130
+ { ssl_cert_store: OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE }.merge(opts)
131
+ end
132
+ end
133
+
134
+ describe '#authn_cert_authenticate' do
135
+ context 'with host_id (request mode)' do
136
+ subject { described_class.authn_cert_authenticate(account, service_id, 'host/vm-01', cert_opts) }
137
+
138
+ it 'returns a RestClient::Resource' do
139
+ expect(subject).to be_a(RestClient::Resource)
140
+ end
141
+
142
+ it 'builds the correct URL' do
143
+ expect(subject.url).to end_with('/authn-cert/my-service/myaccount/host%2Fvm-01/authenticate')
144
+ end
145
+ end
146
+
147
+ context 'without host_id (SPIFFE mode)' do
148
+ subject { described_class.authn_cert_authenticate(account, service_id, nil, cert_opts) }
149
+
150
+ it 'builds the correct URL without host segment' do
151
+ expect(subject.url).to end_with('/authn-cert/my-service/myaccount/authenticate')
152
+ end
153
+ end
154
+
155
+ context 'with a service_id containing special characters' do
156
+ subject { described_class.authn_cert_authenticate(account, 'svc/my service', nil, cert_opts) }
157
+
158
+ it 'URL-encodes the service_id' do
159
+ expect(subject.url).to end_with('/authn-cert/svc%2Fmy%20service/myaccount/authenticate')
160
+ end
161
+ end
162
+ end
163
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: conjur-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.2.0.pre.802
4
+ version: 6.2.0.pre.806
5
5
  platform: ruby
6
6
  authors:
7
7
  - CyberArk Maintainers
@@ -342,6 +342,7 @@ files:
342
342
  - lib/conjur/api.rb
343
343
  - lib/conjur/api/authenticators.rb
344
344
  - lib/conjur/api/authn.rb
345
+ - lib/conjur/api/authn_cert.rb
345
346
  - lib/conjur/api/host_factories.rb
346
347
  - lib/conjur/api/ldap_sync.rb
347
348
  - lib/conjur/api/policies.rb
@@ -384,6 +385,7 @@ files:
384
385
  - spec/.conjurrc
385
386
  - spec/api/host_factories_spec.rb
386
387
  - spec/api_spec.rb
388
+ - spec/authn_cert_spec.rb
387
389
  - spec/base_object_spec.rb
388
390
  - spec/cert_utils_spec.rb
389
391
  - spec/cidr_spec.rb
@@ -457,6 +459,7 @@ test_files:
457
459
  - spec/.conjurrc
458
460
  - spec/api/host_factories_spec.rb
459
461
  - spec/api_spec.rb
462
+ - spec/authn_cert_spec.rb
460
463
  - spec/base_object_spec.rb
461
464
  - spec/cert_utils_spec.rb
462
465
  - spec/cidr_spec.rb