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 +4 -4
- data/VERSION +1 -1
- data/lib/conjur/api/authn_cert.rb +108 -0
- data/lib/conjur/api/router.rb +12 -0
- data/lib/conjur/api.rb +1 -0
- data/spec/authn_cert_spec.rb +163 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4eb698cae0bb3c7abf0e8f8283ef11c7c2ef5790fdb144d3aa6ccb8a75cb2146
|
|
4
|
+
data.tar.gz: 4dcb5fca7170062cfa751d52f830fcaaa9244d218064d1e064988a69ab8912c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bbe620e6fcb2653eb04f7a8355a3155e1e98f32f9f5c52818c5b208aae9cb121ccc9e23e575755135a8a91729af08af3ab2a5444584ea10422dea3d3c8d9bedd
|
|
7
|
+
data.tar.gz: 445c664bf985458b7139c14af8552f603778fe9c31f400fe3e4513b5e7aaa92c5182a7128427f4303171ea4f66ac6d5df3c426fa3a29f3efa89688edabf886c9
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.2.0-
|
|
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
|
data/lib/conjur/api/router.rb
CHANGED
|
@@ -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.
|
|
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
|