vcert 0.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 +7 -0
- data/lib/cloud/cloud.rb +281 -0
- data/lib/fake/fake.rb +130 -0
- data/lib/objects/objects.rb +359 -0
- data/lib/tpp/tpp.rb +338 -0
- data/lib/vcert.rb +91 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: aca37924c54553b285f4a71cb5dd665a67899ee035024acb0fdae853d0fda053
|
4
|
+
data.tar.gz: 16683bfaaf3d43b2b6b2f899fee72a13860121cf8be85975b4808fd687b0b165
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 32b44cb26e66b63fc39dd4066470e79363b3de5af723d6ad5e9a63eaf6447568041bcacbd71ebfebc5beff36174204c201d0346e47cfc25994da33f37b63cf11
|
7
|
+
data.tar.gz: 29c0bdf783fd33860c32685d9f7465b1a788acb3a60371b9e998f809c46182da36e2315f7567a07b5a073221678f411796e53db71e7d00534f57ff11d98747ce
|
data/lib/cloud/cloud.rb
ADDED
@@ -0,0 +1,281 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'utils/utils'
|
3
|
+
|
4
|
+
class Vcert::CloudConnection
|
5
|
+
def initialize(url, token)
|
6
|
+
@url = url
|
7
|
+
@token = token
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def request(zone_tag, request)
|
12
|
+
zone_id = get_zoneId_by_tag(zone_tag)
|
13
|
+
_, data = post(URL_CERTIFICATE_REQUESTS, {:zoneId => zone_id, :certificateSigningRequest => request.csr})
|
14
|
+
LOG.debug("Raw response to certificate request:")
|
15
|
+
LOG.debug(JSON.pretty_generate(data))
|
16
|
+
request.id = data['certificateRequests'][0]["id"]
|
17
|
+
request
|
18
|
+
end
|
19
|
+
|
20
|
+
def retrieve(request)
|
21
|
+
LOG.info(("Getting certificate status for ID %s" % request.id))
|
22
|
+
status, data = get(URL_CERTIFICATE_STATUS % request.id)
|
23
|
+
if [200, 409].include? status
|
24
|
+
case data['status']
|
25
|
+
when CERT_STATUS_PENDING, CERT_STATUS_REQUESTED
|
26
|
+
LOG.info(("Certificate status is: %s" % data['status']))
|
27
|
+
return nil
|
28
|
+
when CERT_STATUS_FAILED
|
29
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Certificate issue status is FAILED"
|
30
|
+
when CERT_STATUS_ISSUED
|
31
|
+
status, full_chain = get(URL_CERTIFICATE_RETRIEVE % request.id + "?chainOrder=#{CHAIN_OPTION_ROOT_LAST}&format=PEM")
|
32
|
+
if status == 200
|
33
|
+
cert = parse_full_chain full_chain
|
34
|
+
if cert.private_key == nil
|
35
|
+
cert.private_key = request.private_key
|
36
|
+
end
|
37
|
+
return cert
|
38
|
+
else
|
39
|
+
LOG.error("Can't issue certificate: #{full_chain}")
|
40
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status #{status}"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Unknown certificate status #{data['status']}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def renew(request, generate_new_key: true)
|
49
|
+
puts("Trying to renew certificate")
|
50
|
+
if request.id == nil && request.thumbprint == nil
|
51
|
+
raise Vcert::ClientBadDataError, "Either request ID or certificate thumbprint is required to renew the certificate"
|
52
|
+
end
|
53
|
+
if request.thumbprint != nil
|
54
|
+
manage_id = search_by_thumbprint(request.thumbprint)
|
55
|
+
end
|
56
|
+
if request.id != nil
|
57
|
+
prev_request = get_cert_status(request)
|
58
|
+
manage_id = prev_request[:manage_id]
|
59
|
+
zone = prev_request[:zoneId]
|
60
|
+
end
|
61
|
+
if manage_id == nil
|
62
|
+
raise Vcert::VcertError, "Can't find the existing certificate"
|
63
|
+
end
|
64
|
+
|
65
|
+
status, data = get(URL_MANAGED_CERTIFICATE_BY_ID % manage_id)
|
66
|
+
if status == 200
|
67
|
+
request.id = data['latestCertificateRequestId']
|
68
|
+
else
|
69
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status #{status}"
|
70
|
+
end
|
71
|
+
|
72
|
+
if zone == nil
|
73
|
+
prev_request = get_cert_status(request)
|
74
|
+
zone = prev_request[:zoneId]
|
75
|
+
end
|
76
|
+
|
77
|
+
d = {existingManagedCertificateId: manage_id, zoneId: zone}
|
78
|
+
if request.csr?
|
79
|
+
d.merge!(certificateSigningRequest: request.csr)
|
80
|
+
d.merge!(reuseCSR: false)
|
81
|
+
elsif generate_new_key
|
82
|
+
parsed_csr = parse_csr_fields(prev_request[:csr])
|
83
|
+
renew_request = Vcert::Request.new(
|
84
|
+
common_name: parsed_csr[:CN],
|
85
|
+
san_dns: parsed_csr[:DNS],
|
86
|
+
country: parsed_csr[:C],
|
87
|
+
province: parsed_csr[:ST],
|
88
|
+
locality: parsed_csr[:L],
|
89
|
+
organization: parsed_csr[:O],
|
90
|
+
organizational_unit: parsed_csr[:OU])
|
91
|
+
d.merge!(certificateSigningRequest: renew_request.csr)
|
92
|
+
else
|
93
|
+
d.merge!(reuseCSR: true)
|
94
|
+
end
|
95
|
+
|
96
|
+
status, data = post(URL_CERTIFICATE_REQUESTS, data = d)
|
97
|
+
if status == 201
|
98
|
+
if generate_new_key
|
99
|
+
return data['certificateRequests'][0]['id'], renew_request.private_key
|
100
|
+
else
|
101
|
+
return data['certificateRequests'][0]['id'], nil
|
102
|
+
end
|
103
|
+
|
104
|
+
else
|
105
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status: #{status} Message: #{data}"
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
def zone_configuration(tag)
|
111
|
+
if tag.to_s.strip.empty?
|
112
|
+
raise Vcert::ClientBadDataError, "Zone should not be empty"
|
113
|
+
end
|
114
|
+
LOG.info("Getting configuration for zone #{tag}")
|
115
|
+
_, data = get(URL_ZONE_BY_TAG % tag)
|
116
|
+
template_id = data['certificateIssuingTemplateId']
|
117
|
+
_, data = get(URL_TEMPLATE_BY_ID % template_id)
|
118
|
+
kt = Vcert::KeyType.new data['keyTypes'][0]["keyType"], data['keyTypes'][0]["keyLengths"][0].to_i
|
119
|
+
z = Vcert::ZoneConfiguration.new(
|
120
|
+
country: Vcert::CertField.new(""),
|
121
|
+
province: Vcert::CertField.new(""),
|
122
|
+
locality: Vcert::CertField.new(""),
|
123
|
+
organization: Vcert::CertField.new(""),
|
124
|
+
organizational_unit: Vcert::CertField.new(""),
|
125
|
+
key_type: Vcert::CertField.new(kt, locked: true),
|
126
|
+
)
|
127
|
+
return z
|
128
|
+
end
|
129
|
+
|
130
|
+
def policy(zone_id)
|
131
|
+
unless zone_id
|
132
|
+
raise Vcert::ClientBadDataError, "Zone should be not nil"
|
133
|
+
end
|
134
|
+
status, data = get(URL_PROJECT_ZONE_DETAILS % zone_id)
|
135
|
+
if status != 200
|
136
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Invalid status getting issuing template: %s for zone %s" % status, zone_id
|
137
|
+
end
|
138
|
+
template_id = data['certificateIssuingTemplateId']
|
139
|
+
status, data = get(URL_TEMPLATE_BY_ID % template_id)
|
140
|
+
if status != 200
|
141
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Invalid status getting policy: %s for issuing template %s" % status, template_id
|
142
|
+
end
|
143
|
+
parse_policy_responce_to_object(data)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
TOKEN_HEADER_NAME = "tppl-api-key"
|
149
|
+
CHAIN_OPTION_ROOT_FIRST = "ROOT_FIRST"
|
150
|
+
CHAIN_OPTION_ROOT_LAST = "EE_FIRST"
|
151
|
+
CERT_STATUS_REQUESTED = 'REQUESTED'
|
152
|
+
CERT_STATUS_PENDING = 'PENDING'
|
153
|
+
CERT_STATUS_FAILED = 'FAILED'
|
154
|
+
CERT_STATUS_ISSUED = 'ISSUED'
|
155
|
+
URL_ZONE_BY_TAG = "zones/tag/%s"
|
156
|
+
URL_PROJECT_ZONE_DETAILS = "projectzones/%s"
|
157
|
+
URL_TEMPLATE_BY_ID = "certificateissuingtemplates/%s"
|
158
|
+
URL_CERTIFICATE_REQUESTS = "certificaterequests"
|
159
|
+
URL_CERTIFICATE_STATUS = URL_CERTIFICATE_REQUESTS + "/%s"
|
160
|
+
URL_CERTIFICATE_RETRIEVE = URL_CERTIFICATE_REQUESTS + "/%s/certificate"
|
161
|
+
URL_CERTIFICATE_SEARCH = "certificatesearch"
|
162
|
+
URL_MANAGED_CERTIFICATES = "managedcertificates"
|
163
|
+
URL_MANAGED_CERTIFICATE_BY_ID = URL_MANAGED_CERTIFICATES + "/%s"
|
164
|
+
|
165
|
+
def get_zoneId_by_tag(tag)
|
166
|
+
_, data = get(URL_ZONE_BY_TAG % tag)
|
167
|
+
data['id']
|
168
|
+
end
|
169
|
+
|
170
|
+
def get(url)
|
171
|
+
uri = URI.parse(@url)
|
172
|
+
request = Net::HTTP.new(uri.host, uri.port)
|
173
|
+
request.use_ssl = true
|
174
|
+
url = uri.path + "/" + url
|
175
|
+
|
176
|
+
|
177
|
+
response = request.get(url, {TOKEN_HEADER_NAME => @token})
|
178
|
+
case response.code.to_i
|
179
|
+
when 200, 201, 202, 409
|
180
|
+
LOG.info(("HTTP status OK"))
|
181
|
+
when 403
|
182
|
+
raise Vcert::AuthenticationError
|
183
|
+
else
|
184
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Unexpected code #{response.code} for URL #{url}. Message: #{response.body}"
|
185
|
+
end
|
186
|
+
case response.header['content-type']
|
187
|
+
when "application/json"
|
188
|
+
begin
|
189
|
+
data = JSON.parse(response.body)
|
190
|
+
rescue JSON::ParserError
|
191
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Invalid JSON"
|
192
|
+
end
|
193
|
+
when "text/plain"
|
194
|
+
data = response.body
|
195
|
+
else
|
196
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Unexpected content-type #{response.header['content-type']}"
|
197
|
+
end
|
198
|
+
# rescue *ALL_NET_HTTP_ERRORS
|
199
|
+
return response.code.to_i, data
|
200
|
+
# end
|
201
|
+
end
|
202
|
+
|
203
|
+
def post(url, data)
|
204
|
+
uri = URI.parse(@url)
|
205
|
+
request = Net::HTTP.new(uri.host, uri.port)
|
206
|
+
request.use_ssl = true
|
207
|
+
url = uri.path + "/" + url
|
208
|
+
encoded_data = JSON.generate(data)
|
209
|
+
response = request.post(url, encoded_data, {TOKEN_HEADER_NAME => @token, "Content-Type" => "application/json", "Accept" => "application/json"})
|
210
|
+
case response.code.to_i
|
211
|
+
when 200, 201, 202, 409
|
212
|
+
LOG.info(("HTTP status OK"))
|
213
|
+
when 403
|
214
|
+
raise Vcert::AuthenticationError
|
215
|
+
else
|
216
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Unexpected code #{response.code} for URL #{url}. Message: #{response.body}"
|
217
|
+
end
|
218
|
+
data = JSON.parse(response.body)
|
219
|
+
return response.code.to_i, data
|
220
|
+
end
|
221
|
+
|
222
|
+
def parse_full_chain(full_chain)
|
223
|
+
pems = parse_pem_list(full_chain)
|
224
|
+
Vcert::Certificate.new(
|
225
|
+
cert: pems[0],
|
226
|
+
chain: pems[1..-1]
|
227
|
+
)
|
228
|
+
end
|
229
|
+
|
230
|
+
def parse_policy_responce_to_object(d)
|
231
|
+
key_types = []
|
232
|
+
d['keyTypes'].each { |kt| key_types.push(['keyType']) }
|
233
|
+
Vcert::Policy.new(policy_id: d['id'],
|
234
|
+
name: d['name'],
|
235
|
+
system_generated: d['systemGenerated'],
|
236
|
+
creation_date: d['creationDate'],
|
237
|
+
subject_cn_regexes: d['subjectCNRegexes'],
|
238
|
+
subject_o_regexes: d['subjectORegexes'],
|
239
|
+
subject_ou_regexes: d['subjectOURegexes'],
|
240
|
+
subject_st_regexes: d['subjectSTRegexes'],
|
241
|
+
subject_l_regexes: d['subjectLRegexes'],
|
242
|
+
subject_c_regexes: d['subjectCValues'],
|
243
|
+
san_regexes: d['sanRegexes'],
|
244
|
+
key_types: key_types)
|
245
|
+
end
|
246
|
+
|
247
|
+
def search_by_thumbprint(thumbprint)
|
248
|
+
# thumbprint = re.sub(r'[^\dabcdefABCDEF]', "", thumbprint)
|
249
|
+
thumbprint = thumbprint.upcase
|
250
|
+
status, data = post(URL_CERTIFICATE_SEARCH, data = {"expression": {operands: [
|
251
|
+
{field: "fingerprint", operator: "MATCH", value: thumbprint}]}})
|
252
|
+
# TODO: check that data have valid certificate in it
|
253
|
+
if status != 200
|
254
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status: #{status}. Message: #{data.body.to_s}"
|
255
|
+
end
|
256
|
+
# TODO: check data
|
257
|
+
manageId = data['certificates'][0]['managedCertificateId']
|
258
|
+
LOG.info("Found existing certificate with ID #{manageId}")
|
259
|
+
return manageId
|
260
|
+
end
|
261
|
+
|
262
|
+
def get_cert_status(request)
|
263
|
+
status, d = get(URL_CERTIFICATE_STATUS % request.id)
|
264
|
+
if status == 200
|
265
|
+
request_status = Hash.new
|
266
|
+
request_status[:status] = d['status']
|
267
|
+
request_status[:subject] = d['subjectDN'] or d['subjectCN'][0]
|
268
|
+
request_status[:subject_alt_names] = d['subjectAlternativeNamesByType']
|
269
|
+
request_status[:zoneId] = d['zoneId']
|
270
|
+
request_status[:manage_id] = d['managedCertificateId']
|
271
|
+
request_status[:csr] = d['certificateSigningRequest']
|
272
|
+
request_status[:key_lenght] = d['keyLength']
|
273
|
+
request_status[:key_type] = d['keyType']
|
274
|
+
return request_status
|
275
|
+
else
|
276
|
+
raise Vcert::ServerUnexpectedBehaviorError, "status: #{status}"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
data/lib/fake/fake.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
class Vcert::FakeConnection
|
7
|
+
def initialize()
|
8
|
+
@cert_cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(zone_tag, request)
|
12
|
+
request.id = Base64.encode64(request.csr)
|
13
|
+
end
|
14
|
+
|
15
|
+
def retrieve(request)
|
16
|
+
csrpem = Base64.decode64(request.id)
|
17
|
+
csr = OpenSSL::X509::Request.new(csrpem)
|
18
|
+
root_ca = OpenSSL::X509::Certificate.new ROOT_CA
|
19
|
+
root_key = OpenSSL::PKey::RSA.new ROOT_KEY
|
20
|
+
cert = OpenSSL::X509::Certificate.new
|
21
|
+
cert.version = 2
|
22
|
+
cert.serial = (Time.new.to_f() * 100).to_i
|
23
|
+
cert.subject = csr.subject
|
24
|
+
cert.issuer = root_ca.subject
|
25
|
+
cert.not_before = Time.now
|
26
|
+
cert.public_key = csr.public_key
|
27
|
+
cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60
|
28
|
+
# todo: add extensions
|
29
|
+
cert.sign(root_key, OpenSSL::Digest::SHA256.new)
|
30
|
+
c = Vcert::Certificate.new cert:cert.to_pem, chain: [ROOT_CA], private_key: request.private_key
|
31
|
+
thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der).to_s
|
32
|
+
@cert_cache[thumbprint] = cert
|
33
|
+
c
|
34
|
+
end
|
35
|
+
|
36
|
+
def policy(zone_tag)
|
37
|
+
key_types = [1024, 2048, 4096, 8192].map {|s| Vcert::KeyType.new("rsa", s) } + Vcert::SUPPORTED_CURVES.map {|c| Vcert::KeyType.new("ecdsa", c) }
|
38
|
+
Vcert::Policy.new(policy_id: zone_tag, name: zone_tag, system_generated: false, creation_date: nil,
|
39
|
+
subject_cn_regexes: [".*"], subject_o_regexes: [".*"],
|
40
|
+
subject_ou_regexes: [".*"], subject_st_regexes: [".*"],
|
41
|
+
subject_l_regexes: [".*"], subject_c_regexes: [".*"], san_regexes: [".*"],
|
42
|
+
key_types: key_types)
|
43
|
+
end
|
44
|
+
|
45
|
+
def zone_configuration(zone_tag)
|
46
|
+
Vcert::ZoneConfiguration.new(
|
47
|
+
country: Vcert::CertField.new("US"),
|
48
|
+
province: Vcert::CertField.new("Utah"),
|
49
|
+
locality: Vcert::CertField.new("Salt Lake City"),
|
50
|
+
organization: Vcert::CertField.new("Venafi"),
|
51
|
+
organizational_unit: Vcert::CertField.new("DevOps"),
|
52
|
+
key_type: Vcert::CertField.new(Vcert::KeyType.new("rsa", 2048), locked: true),
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def renew(request, generate_new_key: true)
|
57
|
+
if request.thumbprint
|
58
|
+
if generate_new_key
|
59
|
+
new_key = OpenSSL::PKey::RSA.new 2048
|
60
|
+
csr = OpenSSL::X509::Request.new
|
61
|
+
csr.subject = @cert_cache[request.thumbprint].subject
|
62
|
+
csr.public_key = new_key.public_key
|
63
|
+
csr.sign new_key, OpenSSL::Digest::SHA256.new
|
64
|
+
return Base64.encode64(csr.to_pem), new_key.to_pem
|
65
|
+
else
|
66
|
+
raise Vcert::VcertError, "can not be implemented"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
unless generate_new_key
|
70
|
+
return request.id, request.private_key
|
71
|
+
end
|
72
|
+
new_key = OpenSSL::PKey::RSA.new 2048
|
73
|
+
csr = OpenSSL::X509::Request.new Base64.decode64(request.id)
|
74
|
+
csr.public_key = new_key.public_key
|
75
|
+
csr.sign new_key, OpenSSL::Digest::SHA256.new
|
76
|
+
return Base64.encode64(csr.to_pem), new_key.to_pem
|
77
|
+
end
|
78
|
+
private
|
79
|
+
ROOT_CA = "-----BEGIN CERTIFICATE-----
|
80
|
+
MIIDYDCCAkigAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMRMwEQYKCZImiZPyLGQB
|
81
|
+
GRYDb3JnMRYwFAYKCZImiZPyLGQBGRYGVmVuYWZpMRIwEAYDVQQDDAlWZW5hZmkg
|
82
|
+
Q0EwHhcNMTkxMDIzMTIzNzExWhcNMjkxMDIwMTIzNzExWjBBMRMwEQYKCZImiZPy
|
83
|
+
LGQBGRYDb3JnMRYwFAYKCZImiZPyLGQBGRYGVmVuYWZpMRIwEAYDVQQDDAlWZW5h
|
84
|
+
ZmkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4NfBNSVzOqj8A
|
85
|
+
E6+NSuEufK5EhgE/o8KPCTtM7qvMCa0X3P+T0I5IUDMXz5Yi/TXjhTANolYEz2RS
|
86
|
+
9u5Pdv5dvBCe1hwMhXdLlcxEhLtJrjnQvBUTqzuFUBausvRZvE3GwozoZncakEEP
|
87
|
+
OqTvGEpqjbnF1uiIJf944kjIq9oWnPudatOOlCFtpA1TG1mLJg8jcCrbeiXvRo9d
|
88
|
+
/dyg7B7URgKdxMukdjCkUMqUwArlu7mnv1kN6UdzhfFRCH0MBH4pisVze9XP/QrV
|
89
|
+
MJ+gMlultrpDFuMpiruJyPeapDnGloxtWKQ/aQHlnwwaX8fcaA3ADKUAFr66nFT2
|
90
|
+
14fgmByFAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
|
91
|
+
MB0GA1UdDgQWBBSFrxYL7Fd190DFUZFJ4ahzptV7tTAfBgNVHSMEGDAWgBSFrxYL
|
92
|
+
7Fd190DFUZFJ4ahzptV7tTANBgkqhkiG9w0BAQsFAAOCAQEAZsRtfC+4j6RWbWDZ
|
93
|
+
3eRabfY4Nl7z3q3hL2cYo98ZQVb5wssYwKPpX8/DFMnmgiObe0Na5zqaB9PxDBpZ
|
94
|
+
4wkRFpRfQ2oS13dzPMDdW0/IHhWfyWZiUqdpIacWIHvyuotZ/k3IZLT7zc9Lbs2p
|
95
|
+
FPmW5/Oe7lNRu3xgqaMuhRid8i426c+fR5YPf32umZtwRnB5hFFE9IlFBPpRl5Z7
|
96
|
+
GDJKJBgZi9+sk13a3CM8Zn0A9fiCaRASDPKRVhWPjDJwzLy44WF+1GiMZRCR79MX
|
97
|
+
8B/rNxkrKpWJmjkQj3jqmQOOnp7+QwdZ5OIV7NNlc/Kx2QDV9QV+hnRPetXmsVfy
|
98
|
+
y5KjSQ==
|
99
|
+
-----END CERTIFICATE-----
|
100
|
+
"
|
101
|
+
ROOT_KEY = "-----BEGIN RSA PRIVATE KEY-----
|
102
|
+
MIIEpAIBAAKCAQEAuDXwTUlczqo/ABOvjUrhLnyuRIYBP6PCjwk7TO6rzAmtF9z/
|
103
|
+
k9COSFAzF8+WIv0144UwDaJWBM9kUvbuT3b+XbwQntYcDIV3S5XMRIS7Sa450LwV
|
104
|
+
E6s7hVAWrrL0WbxNxsKM6GZ3GpBBDzqk7xhKao25xdboiCX/eOJIyKvaFpz7nWrT
|
105
|
+
jpQhbaQNUxtZiyYPI3Aq23ol70aPXf3coOwe1EYCncTLpHYwpFDKlMAK5bu5p79Z
|
106
|
+
DelHc4XxUQh9DAR+KYrFc3vVz/0K1TCfoDJbpba6QxbjKYq7icj3mqQ5xpaMbVik
|
107
|
+
P2kB5Z8MGl/H3GgNwAylABa+upxU9teH4JgchQIDAQABAoIBAEa/YIUuUdiFhiCv
|
108
|
+
btLjGUzTUdK7bKtWZ5irwPyxBYYdiT8K/5VzmdGoC5dvgIf7m8DAHE6ANG0wgaVj
|
109
|
+
dO9MEjFJ01BNhwRAFisPYx5Fo/COW2IRej7NmtR+h9ecnz//lBdsDNYM1F19XZ9N
|
110
|
+
tJ6nQ51cxSZ4fWIcxdtVfQKlDeN0y7ZanHsltv4cpCCuVaVk8uzI6O5E8dNbDmpR
|
111
|
+
Wotefps+9HHREa6uL39SbzU+S5SkdcVofs6/g/eL6RsP4D6VcF+qdBEQ38ffDaSZ
|
112
|
+
Q1hOwfTFf6Ahv4HhpCVC6vlIpXJi/RyUu6yqbmInvcXfGHvYoMYhud/lasYZDAm2
|
113
|
+
RdGB0gECgYEA7OHnaAqOaaYnE2rQkOAn3ZzA7VRhJgMvPgKUeQoUHDhXJOD/6wWD
|
114
|
+
1/wYd4BKiQyODi5cAlOkLvdrcRlnrGOKRiLgemyNrG2GzJTzgrkJc1IOpRnl2QKw
|
115
|
+
w1k0Xrv2qDpoebKMqhxjgEnYp+ddVB7kG561vl7JfhjQidrVqx6y/0UCgYEAxxPQ
|
116
|
+
M4myF4JKHpxU4+21JKxB3bTY7CWmKM1ZBon/ZFVKd8bsq7wt0tWP83oxWq5b11o+
|
117
|
+
AnWx4CsQQyl7EWanrDoPag2SjfI/q+AySq0VUNjcAsvPLfT2Q7WQQxEMQGoabZ7j
|
118
|
+
u8uxkNvZmDy5XGDjcZVdANq2kynC++v1AtwO3EECgYEAnmeeaCuO+kU6ojhuikLr
|
119
|
+
Rb3aIZqocFP21n/BK4O62PgwBiBT4qTIerlA30CyFx2HLSKBMqkeBK49cd8sPdI+
|
120
|
+
mBIgjJ1ky+ZeGxaMFGGKWUyJMIy18D1lWOyhIayOEAcm8CKe/+6F9zbqo7UK6wLR
|
121
|
+
RUsHe+tE0IblhRoKgijASAUCgYEAra7KkXxLhRklw0kPAwBLbqBeoqf6LSS3r5dg
|
122
|
+
WUUiLQ4Adzl1GGuH6w5plbmAv6Wo+NyBhzHZq0LG4GGbPlY6aRcKhbMrrm2wQSrL
|
123
|
+
lb0mALACWuonaefyxqXsI6cG8lffkM3zz87prwEv+RLZgRACvwDZ8Dng2cmwlIuK
|
124
|
+
6iDFUkECgYBL4U3E+trPuVEQm3Nj9nyIFV1efKDZ+uehPSvglYdO+ca7UgwA0btZ
|
125
|
+
iAAu3L3yP3TSJ6SbLV3hX1VoyyNQpUr+ODZo7VWf+MdZDh9XiiuLNqr5tx7yGhOb
|
126
|
+
1JXZ1QPAZeRvALui6fdj5yCHjTvayEL2nzPAFbgFrgVYRoF8L9O0gg==
|
127
|
+
-----END RSA PRIVATE KEY-----
|
128
|
+
"
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,359 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require "logger"
|
3
|
+
LOG = Logger.new(STDOUT)
|
4
|
+
|
5
|
+
OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?)
|
6
|
+
|
7
|
+
module Vcert
|
8
|
+
SUPPORTED_CURVES = ["secp224r1", "prime256v1", "secp521r1"]
|
9
|
+
class Request
|
10
|
+
attr_accessor :id, :thumbprint
|
11
|
+
attr_reader :common_name, :country, :province, :locality, :organization, :organizational_unit, :san_dns,:key_type
|
12
|
+
|
13
|
+
def initialize(common_name: nil, private_key: nil, key_type: nil,
|
14
|
+
organization: nil, organizational_unit: nil, country: nil, province: nil, locality: nil, san_dns: nil,
|
15
|
+
friendly_name: nil, csr: nil)
|
16
|
+
@common_name = common_name
|
17
|
+
@private_key = private_key
|
18
|
+
#todo: parse private key and set public
|
19
|
+
if key_type != nil && !key_type.instance_of?(KeyType)
|
20
|
+
raise Vcert::ClientBadDataError, "key_type bad type. should be Vcert::KeyType. for example KeyType('rsa', 2048)"
|
21
|
+
end
|
22
|
+
@key_type = key_type
|
23
|
+
@organization = organization
|
24
|
+
@organizational_unit = organizational_unit
|
25
|
+
@country = country
|
26
|
+
@province = province
|
27
|
+
@locality = locality
|
28
|
+
@san_dns = san_dns
|
29
|
+
@friendly_name = friendly_name
|
30
|
+
@id = nil
|
31
|
+
@csr = csr
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate_csr
|
35
|
+
if @private_key == nil
|
36
|
+
generate_private_key
|
37
|
+
end
|
38
|
+
subject_attrs = [
|
39
|
+
['CN', @common_name]
|
40
|
+
]
|
41
|
+
if @organization != nil
|
42
|
+
subject_attrs.push(['O', @organization])
|
43
|
+
end
|
44
|
+
if @organizational_unit != nil
|
45
|
+
if @organizational_unit.kind_of?(Array)
|
46
|
+
@organizational_unit.each { |ou| subject_attrs.push(['OU', ou]) }
|
47
|
+
else
|
48
|
+
subject_attrs.push(['OU', @organizational_unit])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if @country != nil
|
52
|
+
subject_attrs.push(['C', @country])
|
53
|
+
end
|
54
|
+
if @province != nil
|
55
|
+
subject_attrs.push(['ST', @province])
|
56
|
+
end
|
57
|
+
if @locality != nil
|
58
|
+
subject_attrs.push(['L', @locality])
|
59
|
+
end
|
60
|
+
|
61
|
+
LOG.info("Making request from subject array #{subject_attrs.inspect}")
|
62
|
+
subject = OpenSSL::X509::Name.new subject_attrs
|
63
|
+
csr = OpenSSL::X509::Request.new
|
64
|
+
csr.version = 0
|
65
|
+
csr.subject = subject
|
66
|
+
csr.public_key = @public_key
|
67
|
+
if @san_dns != nil
|
68
|
+
unless @san_dns.kind_of?(Array)
|
69
|
+
@san_dns = [@san_dns]
|
70
|
+
end
|
71
|
+
#TODO: add check that san_dns is an array
|
72
|
+
san_list = @san_dns.map { |domain| "DNS:#{domain}" }
|
73
|
+
extensions = [
|
74
|
+
OpenSSL::X509::ExtensionFactory.new.create_extension('subjectAltName', san_list.join(','))
|
75
|
+
]
|
76
|
+
attribute_values = OpenSSL::ASN1::Set [OpenSSL::ASN1::Sequence(extensions)]
|
77
|
+
[
|
78
|
+
OpenSSL::X509::Attribute.new('extReq', attribute_values),
|
79
|
+
OpenSSL::X509::Attribute.new('msExtReq', attribute_values)
|
80
|
+
].each do |attribute|
|
81
|
+
csr.add_attribute attribute
|
82
|
+
end
|
83
|
+
end
|
84
|
+
csr.sign @private_key, OpenSSL::Digest::SHA256.new # todo: changable sign alg
|
85
|
+
@csr = csr.to_pem
|
86
|
+
end
|
87
|
+
|
88
|
+
def csr
|
89
|
+
# TODO: find a way to pass CSR generation if renew is requested
|
90
|
+
if @csr == nil
|
91
|
+
generate_csr
|
92
|
+
end
|
93
|
+
@csr
|
94
|
+
end
|
95
|
+
|
96
|
+
def csr?
|
97
|
+
@csr != nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def private_key
|
101
|
+
if @private_key == nil
|
102
|
+
generate_private_key
|
103
|
+
end
|
104
|
+
@private_key.to_pem
|
105
|
+
end
|
106
|
+
|
107
|
+
def friendly_name
|
108
|
+
if @friendly_name != nil
|
109
|
+
return @friendly_name
|
110
|
+
end
|
111
|
+
@common_name
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param [ZoneConfiguration] zone_config
|
115
|
+
def update_from_zone_config(zone_config)
|
116
|
+
if zone_config.country.locked || (!@country && !!zone_config.country.value)
|
117
|
+
@country = zone_config.country.value
|
118
|
+
end
|
119
|
+
if zone_config.locality.locked || (!@locality && !!zone_config.locality.value)
|
120
|
+
@locality = zone_config.locality.value
|
121
|
+
end
|
122
|
+
if zone_config.province.locked || (!@province && !!zone_config.province.value)
|
123
|
+
@province = zone_config.province.value
|
124
|
+
end
|
125
|
+
if zone_config.organization.locked || (!@organization && !!zone_config.organization.value)
|
126
|
+
@organization = zone_config.organization.value
|
127
|
+
end
|
128
|
+
if zone_config.organizational_unit.locked || (!@organizational_unit && !!zone_config.organizational_unit.value)
|
129
|
+
@organizational_unit = zone_config.organizational_unit.value
|
130
|
+
end
|
131
|
+
if zone_config.key_type.locked || (@key_type == nil && zone_config.key_type.value != nil)
|
132
|
+
@key_type = zone_config.key_type.value
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
|
139
|
+
def generate_private_key
|
140
|
+
if @key_type == nil
|
141
|
+
@key_type = DEFAULT_KEY_TYPE
|
142
|
+
end
|
143
|
+
if @key_type.type == "rsa"
|
144
|
+
@private_key = OpenSSL::PKey::RSA.new @key_type.option
|
145
|
+
@public_key = @private_key.public_key
|
146
|
+
elsif @key_type.type == "ecdsa"
|
147
|
+
@private_key, @public_key = OpenSSL::PKey::EC.new(@key_type.option), OpenSSL::PKey::EC.new(@key_type.option)
|
148
|
+
@private_key.generate_key
|
149
|
+
@public_key.public_key = @private_key.public_key
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class Certificate
|
155
|
+
attr_accessor :private_key
|
156
|
+
attr_reader :cert, :chain
|
157
|
+
|
158
|
+
def initialize(cert: nil, chain: nil, private_key: nil)
|
159
|
+
@cert = cert
|
160
|
+
@chain = chain
|
161
|
+
@private_key = private_key
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class Policy
|
166
|
+
attr_reader :policy_id, :name, :system_generated, :creation_date
|
167
|
+
|
168
|
+
def initialize(policy_id:, name:, system_generated:, creation_date:, subject_cn_regexes:, subject_o_regexes:,
|
169
|
+
subject_ou_regexes:, subject_st_regexes:, subject_l_regexes:, subject_c_regexes:, san_regexes:,
|
170
|
+
key_types:)
|
171
|
+
|
172
|
+
@policy_id = policy_id
|
173
|
+
@name = name
|
174
|
+
@system_generated = system_generated
|
175
|
+
@creation_date = creation_date
|
176
|
+
@subject_cn_regexes = subject_cn_regexes
|
177
|
+
@subject_c_regexes = subject_c_regexes
|
178
|
+
@subject_st_regexes = subject_st_regexes
|
179
|
+
@subject_l_regexes = subject_l_regexes
|
180
|
+
@subject_o_regexes = subject_o_regexes
|
181
|
+
@subject_ou_regexes = subject_ou_regexes
|
182
|
+
@san_regexes = san_regexes
|
183
|
+
@key_types = key_types
|
184
|
+
end
|
185
|
+
|
186
|
+
# @param [Request] request
|
187
|
+
def simple_check_request(request)
|
188
|
+
if request.csr?
|
189
|
+
csr = parse_csr_fields(request.csr)
|
190
|
+
unless component_is_valid?(csr[:CN], @subject_cn_regexes)
|
191
|
+
raise ValidationError, "Common name #{csr[:CN]} doesnt match #{@subject_cn_regexes}"
|
192
|
+
end
|
193
|
+
unless component_is_valid?(request.san_dns, @san_regexes, optional: true)
|
194
|
+
raise ValidationError, "SANs #{csr[:DNS]} doesnt match #{ @san_regexes }"
|
195
|
+
end
|
196
|
+
else
|
197
|
+
unless component_is_valid?(request.common_name, @subject_cn_regexes)
|
198
|
+
raise ValidationError, "Common name #{request.common_name} doesnt match #{@subject_cn_regexes}"
|
199
|
+
end
|
200
|
+
unless component_is_valid?(request.san_dns, @san_regexes, optional: true)
|
201
|
+
raise ValidationError, "SANs #{request.san_dns} doesnt match #{ @san_regexes }"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# @param [Request] request
|
207
|
+
def check_request(request)
|
208
|
+
simple_check_request(request)
|
209
|
+
if request.csr?
|
210
|
+
csr = parse_csr_fields(request.csr)
|
211
|
+
unless component_is_valid?(csr[:C], @subject_c_regexes)
|
212
|
+
raise ValidationError, "Country #{csr[:C]} doesnt match #{@subject_c_regexes}"
|
213
|
+
end
|
214
|
+
unless component_is_valid?(csr[:ST], @subject_st_regexes)
|
215
|
+
raise ValidationError, "Province #{csr[:ST]} doesnt match #{@subject_st_regexes}"
|
216
|
+
end
|
217
|
+
unless component_is_valid?(csr[:L], @subject_l_regexes)
|
218
|
+
raise ValidationError, "Locality #{csr[:L]} doesnt match #{@subject_l_regexes}"
|
219
|
+
end
|
220
|
+
unless component_is_valid?(csr[:O], @subject_o_regexes)
|
221
|
+
raise ValidationError, "Organization #{csr[:O]} doesnt match #{@subject_o_regexes}"
|
222
|
+
end
|
223
|
+
unless component_is_valid?(csr[:OU], @subject_ou_regexes)
|
224
|
+
raise ValidationError, "Organizational unit #{csr[:OU]} doesnt match #{@subject_ou_regexes}"
|
225
|
+
end
|
226
|
+
#todo: add uri, upn, ip, email
|
227
|
+
unless is_key_type_is_valid?(csr[:key_type], @key_types)
|
228
|
+
raise ValidationError, "Key Type #{csr[:key_type]} doesnt match allowed #{@key_types}"
|
229
|
+
end
|
230
|
+
else
|
231
|
+
# subject
|
232
|
+
unless component_is_valid?(request.country, @subject_c_regexes)
|
233
|
+
raise ValidationError, "Country #{request.country} doesnt match #{@subject_c_regexes}"
|
234
|
+
end
|
235
|
+
unless component_is_valid?(request.province, @subject_st_regexes)
|
236
|
+
raise ValidationError, "Province #{request.province} doesnt match #{@subject_st_regexes}"
|
237
|
+
end
|
238
|
+
unless component_is_valid?(request.locality, @subject_l_regexes)
|
239
|
+
raise ValidationError, "Locality #{request.locality} doesnt match #{@subject_l_regexes}"
|
240
|
+
end
|
241
|
+
unless component_is_valid?(request.organization, @subject_o_regexes)
|
242
|
+
raise ValidationError, "Organization #{request.organization} doesnt match #{@subject_o_regexes}"
|
243
|
+
end
|
244
|
+
unless component_is_valid?(request.organizational_unit, @subject_ou_regexes)
|
245
|
+
raise ValidationError, "Organizational unit #{request.organizational_unit} doesnt match #{@subject_ou_regexes}"
|
246
|
+
end
|
247
|
+
#todo: add uri, upn, ip, email
|
248
|
+
unless is_key_type_is_valid?(request.key_type, @key_types)
|
249
|
+
raise ValidationError, "Key Type #{request.key_type} doesnt match allowed #{@key_types}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
# todo: (!important!) parse csr if it alredy generated (!important!)
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
def is_key_type_is_valid?(key_type, allowed_key_types)
|
260
|
+
if key_type == nil
|
261
|
+
key_type = DEFAULT_KEY_TYPE
|
262
|
+
end
|
263
|
+
for i in 0 ... allowed_key_types.length
|
264
|
+
if allowed_key_types[i] == key_type
|
265
|
+
return true
|
266
|
+
end
|
267
|
+
end
|
268
|
+
false
|
269
|
+
end
|
270
|
+
|
271
|
+
def component_is_valid?(component, regexps, optional:false)
|
272
|
+
if component == nil
|
273
|
+
component = []
|
274
|
+
end
|
275
|
+
unless component.instance_of? Array
|
276
|
+
component = [component]
|
277
|
+
end
|
278
|
+
if component.length == 0 && optional
|
279
|
+
return true
|
280
|
+
end
|
281
|
+
if component.length == 0
|
282
|
+
component = [""]
|
283
|
+
end
|
284
|
+
for i in 0 ... component.length
|
285
|
+
unless match_regexps?(component[i], regexps)
|
286
|
+
return false
|
287
|
+
end
|
288
|
+
end
|
289
|
+
true
|
290
|
+
end
|
291
|
+
|
292
|
+
def match_regexps?(s, regexps)
|
293
|
+
for i in 0 ... regexps.length
|
294
|
+
if Regexp.new(regexps[i]).match(s)
|
295
|
+
return true
|
296
|
+
end
|
297
|
+
end
|
298
|
+
false
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
class ZoneConfiguration
|
303
|
+
attr_reader :country, :province, :locality, :organization, :organizational_unit, :key_type
|
304
|
+
|
305
|
+
# @param [CertField] country
|
306
|
+
# @param [CertField] province
|
307
|
+
# @param [CertField] locality
|
308
|
+
# @param [CertField] organization
|
309
|
+
# @param [CertField] organizational_unit
|
310
|
+
# @param [CertField] key_type
|
311
|
+
def initialize(country:, province:, locality:, organization:, organizational_unit:, key_type:)
|
312
|
+
@country = country
|
313
|
+
@province = province
|
314
|
+
@locality = locality
|
315
|
+
@organization = organization
|
316
|
+
@organizational_unit = organizational_unit
|
317
|
+
@key_type = key_type
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class CertField
|
322
|
+
attr_reader :value, :locked
|
323
|
+
|
324
|
+
def initialize(value, locked: false)
|
325
|
+
@value = value
|
326
|
+
@locked = locked
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
class KeyType
|
331
|
+
attr_reader :type, :option
|
332
|
+
|
333
|
+
def initialize(type, option)
|
334
|
+
@type = {"rsa" => "rsa", "ec" => "ecdsa", "ecdsa" => "ecdsa"}[type.downcase]
|
335
|
+
if @type == nil
|
336
|
+
raise Vcert::VcertError, "bad key type"
|
337
|
+
end
|
338
|
+
if @type == "rsa"
|
339
|
+
unless [512, 1024, 2048, 3072, 4096, 8192].include?(option)
|
340
|
+
raise Vcert::VcertError,"bad option for rsa key: #{option}. should be one from list 512, 1024, 2048, 3072, 4096, 8192"
|
341
|
+
end
|
342
|
+
else
|
343
|
+
unless SUPPORTED_CURVES.include?(option)
|
344
|
+
raise Vcert::VcertError, "bad option for ec key: #{option}. should be one from list #{ SUPPORTED_CURVES}"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
@option = option
|
348
|
+
end
|
349
|
+
def ==(other)
|
350
|
+
unless other.instance_of? KeyType
|
351
|
+
return false
|
352
|
+
end
|
353
|
+
self.type == other.type && self.option == other.option
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
DEFAULT_KEY_TYPE = KeyType.new("rsa", 2048)
|
358
|
+
end
|
359
|
+
|
data/lib/tpp/tpp.rb
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'date'
|
3
|
+
require 'base64'
|
4
|
+
require 'utils/utils'
|
5
|
+
|
6
|
+
class Vcert::TPPConnection
|
7
|
+
def initialize(url, user, password, trust_bundle: nil)
|
8
|
+
@url = normalize_url url
|
9
|
+
@user = user
|
10
|
+
@password = password
|
11
|
+
@token = nil
|
12
|
+
@trust_bundle = trust_bundle
|
13
|
+
end
|
14
|
+
|
15
|
+
def request(zone_tag, request)
|
16
|
+
data = {:PolicyDN => policy_dn(zone_tag),
|
17
|
+
:PKCS10 => request.csr,
|
18
|
+
:ObjectName => request.friendly_name,
|
19
|
+
:DisableAutomaticRenewal => "true"}
|
20
|
+
code, response = post URL_CERTIFICATE_REQUESTS, data
|
21
|
+
if code != 200
|
22
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status #{code}"
|
23
|
+
end
|
24
|
+
request.id = response['CertificateDN']
|
25
|
+
end
|
26
|
+
|
27
|
+
def retrieve(request)
|
28
|
+
retrieve_request = {CertificateDN: request.id, Format: "base64", IncludeChain: 'true', RootFirstOrder: "false"}
|
29
|
+
code, response = post URL_CERTIFICATE_RETRIEVE, retrieve_request
|
30
|
+
if code != 200
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
full_chain = Base64.decode64(response['CertificateData'])
|
34
|
+
cert = parse_full_chain full_chain
|
35
|
+
if cert.private_key == nil
|
36
|
+
cert.private_key = request.private_key
|
37
|
+
end
|
38
|
+
cert
|
39
|
+
end
|
40
|
+
|
41
|
+
def policy(zone_tag)
|
42
|
+
code, response = post URL_ZONE_CONFIG, {:PolicyDN => policy_dn(zone_tag)}
|
43
|
+
if code != 200
|
44
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status #{code}"
|
45
|
+
end
|
46
|
+
parse_policy_response response, zone_tag
|
47
|
+
end
|
48
|
+
|
49
|
+
def zone_configuration(zone_tag)
|
50
|
+
code, response = post URL_ZONE_CONFIG, {:PolicyDN => policy_dn(zone_tag)}
|
51
|
+
if code != 200
|
52
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status #{code}"
|
53
|
+
end
|
54
|
+
parse_zone_configuration response
|
55
|
+
end
|
56
|
+
|
57
|
+
def renew(request, generate_new_key: true)
|
58
|
+
if request.id == nil && request.thumbprint == nil
|
59
|
+
raise("Either request ID or certificate thumbprint is required to renew the certificate")
|
60
|
+
end
|
61
|
+
|
62
|
+
if request.thumbprint != nil
|
63
|
+
request.id = search_by_thumbprint(request.thumbprint)
|
64
|
+
end
|
65
|
+
renew_req_data = {"CertificateDN": request.id}
|
66
|
+
if generate_new_key
|
67
|
+
_, r = post(URL_SECRET_STORE_SEARCH, d = {"Namespace": "config", "Owner": request.id, "VaultType": 512})
|
68
|
+
vaultId = r["VaultIDs"][0]
|
69
|
+
_, r = post(URL_SECRET_STORE_RETRIEVE, d = {"VaultID": vaultId})
|
70
|
+
csr_base64_data = r['Base64Data']
|
71
|
+
csr_pem = "-----BEGIN CERTIFICATE REQUEST-----\n#{csr_base64_data}\n-----END CERTIFICATE REQUEST-----\n"
|
72
|
+
parsed_csr = parse_csr_fields(csr_pem)
|
73
|
+
renew_request = Vcert::Request.new(
|
74
|
+
common_name: parsed_csr.fetch(:CN, nil),
|
75
|
+
san_dns: parsed_csr.fetch(:DNS, nil),
|
76
|
+
country: parsed_csr.fetch(:C, nil),
|
77
|
+
province: parsed_csr.fetch(:ST, nil),
|
78
|
+
locality: parsed_csr.fetch(:L, nil),
|
79
|
+
organization: parsed_csr.fetch(:O, nil),
|
80
|
+
organizational_unit: parsed_csr.fetch(:OU, nil))
|
81
|
+
renew_req_data.merge!(PKCS10: renew_request.csr)
|
82
|
+
end
|
83
|
+
LOG.info("Trying to renew certificate %s" % request.id)
|
84
|
+
_, d = post(URL_CERTIFICATE_RENEW, renew_req_data)
|
85
|
+
if d.key?('Success')
|
86
|
+
if generate_new_key
|
87
|
+
return request.id, renew_request.private_key
|
88
|
+
else
|
89
|
+
return request.id, nil
|
90
|
+
end
|
91
|
+
else
|
92
|
+
raise "Certificate renew error"
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
URL_AUTHORIZE = "authorize/"
|
100
|
+
URL_CERTIFICATE_REQUESTS = "certificates/request"
|
101
|
+
URL_ZONE_CONFIG = "certificates/checkpolicy"
|
102
|
+
URL_CERTIFICATE_RETRIEVE = "certificates/retrieve"
|
103
|
+
URL_CERTIFICATE_SEARCH = "certificates/"
|
104
|
+
URL_CERTIFICATE_RENEW = "certificates/renew"
|
105
|
+
URL_SECRET_STORE_SEARCH = "SecretStore/LookupByOwner"
|
106
|
+
URL_SECRET_STORE_RETRIEVE = "SecretStore/Retrieve"
|
107
|
+
|
108
|
+
TOKEN_HEADER_NAME = "x-venafi-api-key"
|
109
|
+
ALL_ALLOWED_REGEX = ".*"
|
110
|
+
|
111
|
+
def auth
|
112
|
+
uri = URI.parse(@url)
|
113
|
+
request = Net::HTTP.new(uri.host, uri.port)
|
114
|
+
request.use_ssl = true
|
115
|
+
if @trust_bundle != nil
|
116
|
+
request.ca_file = @trust_bundle
|
117
|
+
end
|
118
|
+
url = uri.path + URL_AUTHORIZE
|
119
|
+
data = {:Username => @user, :Password => @password}
|
120
|
+
encoded_data = JSON.generate(data)
|
121
|
+
response = request.post(url, encoded_data, {"Content-Type" => "application/json"})
|
122
|
+
if response.code.to_i != 200
|
123
|
+
raise Vcert::AuthenticationError
|
124
|
+
end
|
125
|
+
data = JSON.parse(response.body)
|
126
|
+
token = data['APIKey']
|
127
|
+
valid_until = DateTime.strptime(data['ValidUntil'].gsub(/\D/, ''), '%Q')
|
128
|
+
@token = token, valid_until
|
129
|
+
end
|
130
|
+
|
131
|
+
def post(url, data)
|
132
|
+
if @token == nil || @token[1] < DateTime.now
|
133
|
+
auth()
|
134
|
+
end
|
135
|
+
uri = URI.parse(@url)
|
136
|
+
request = Net::HTTP.new(uri.host, uri.port)
|
137
|
+
request.use_ssl = true
|
138
|
+
if @trust_bundle != nil
|
139
|
+
request.ca_file = @trust_bundle
|
140
|
+
end
|
141
|
+
url = uri.path + url
|
142
|
+
encoded_data = JSON.generate(data)
|
143
|
+
response = request.post(url, encoded_data, {TOKEN_HEADER_NAME => @token[0], "Content-Type" => "application/json"})
|
144
|
+
data = JSON.parse(response.body)
|
145
|
+
return response.code.to_i, data
|
146
|
+
end
|
147
|
+
|
148
|
+
def get(url)
|
149
|
+
if @token == nil || @token[1] < DateTime.now
|
150
|
+
auth()
|
151
|
+
end
|
152
|
+
uri = URI.parse(@url)
|
153
|
+
request = Net::HTTP.new(uri.host, uri.port)
|
154
|
+
request.use_ssl = true
|
155
|
+
if @trust_bundle != nil
|
156
|
+
request.ca_file = @trust_bundle
|
157
|
+
end
|
158
|
+
url = uri.path + url
|
159
|
+
response = request.get(url, {TOKEN_HEADER_NAME => @token[0]})
|
160
|
+
# TODO: check valid json
|
161
|
+
data = JSON.parse(response.body)
|
162
|
+
return response.code.to_i, data
|
163
|
+
end
|
164
|
+
|
165
|
+
def policy_dn(zone)
|
166
|
+
if zone == nil || zone == ''
|
167
|
+
raise Vcert::ClientBadDataError, "Zone should not be empty"
|
168
|
+
end
|
169
|
+
if zone =~ /^\\\\VED\\\\Poplicy/
|
170
|
+
return zone
|
171
|
+
end
|
172
|
+
if zone =~ /^\\\\/
|
173
|
+
return '\\VED\\Policy' + zone
|
174
|
+
else
|
175
|
+
return '\\VED\\Policy\\' + zone
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def normalize_url(url)
|
180
|
+
if url.index('http://') == 0
|
181
|
+
url = "https://" + url[7..-1]
|
182
|
+
elsif url.index('https://') != 0
|
183
|
+
url = 'https://' + url
|
184
|
+
end
|
185
|
+
unless url.end_with?('/')
|
186
|
+
url = url + '/'
|
187
|
+
end
|
188
|
+
unless url.end_with?('/vedsdk/')
|
189
|
+
url = url + 'vedsdk/'
|
190
|
+
end
|
191
|
+
unless url =~ /^https:\/\/[a-z\d]+[-a-z\d.]+[a-z\d][:\d]*\/vedsdk\/$/
|
192
|
+
raise Vcert::ClientBadDataError, "Invalid URL for TPP"
|
193
|
+
end
|
194
|
+
url
|
195
|
+
end
|
196
|
+
|
197
|
+
def parse_full_chain(full_chain)
|
198
|
+
pems = parse_pem_list(full_chain)
|
199
|
+
Vcert::Certificate.new cert: pems[0], chain: pems[1..-1], private_key: nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def search_by_thumbprint(thumbprint)
|
203
|
+
# thumbprint = re.sub(r'[^\dabcdefABCDEF]', "", thumbprint)
|
204
|
+
thumbprint = thumbprint.upcase
|
205
|
+
status, data = get(URL_CERTIFICATE_SEARCH+"?Thumbprint=#{thumbprint}")
|
206
|
+
# TODO: check that data have valid certificate in it
|
207
|
+
if status != 200
|
208
|
+
raise Vcert::ServerUnexpectedBehaviorError, "Status: #{status}. Message:\n #{data.body.to_s}"
|
209
|
+
end
|
210
|
+
# TODO: check valid data
|
211
|
+
return data['Certificates'][0]['DN']
|
212
|
+
end
|
213
|
+
|
214
|
+
def parse_zone_configuration(data)
|
215
|
+
s = data["Policy"]["Subject"]
|
216
|
+
country = Vcert::CertField.new s["Country"]["Value"], locked: s["Country"]["Locked"]
|
217
|
+
state = Vcert::CertField.new s["State"]["Value"], locked: s["State"]["Locked"]
|
218
|
+
city = Vcert::CertField.new s["City"]["Value"], locked: s["City"]["Locked"]
|
219
|
+
organization = Vcert::CertField.new s["Organization"]["Value"], locked: s["Organization"]["Locked"]
|
220
|
+
organizational_unit = Vcert::CertField.new s["OrganizationalUnit"]["Values"], locked: s["OrganizationalUnit"]["Locked"]
|
221
|
+
key_type = Vcert::KeyType.new data["Policy"]["KeyPair"]["KeyAlgorithm"]["Value"], data["Policy"]["KeyPair"]["KeySize"]["Value"]
|
222
|
+
Vcert::ZoneConfiguration.new country: country, province: state, locality: city, organization: organization,
|
223
|
+
organizational_unit: organizational_unit, key_type: Vcert::CertField.new(key_type)
|
224
|
+
end
|
225
|
+
|
226
|
+
def parse_policy_response(response, zone_tag)
|
227
|
+
def addStartEnd(s)
|
228
|
+
unless s.index("^") == 0
|
229
|
+
s = "^" + s
|
230
|
+
end
|
231
|
+
unless s.end_with?("$")
|
232
|
+
s = s + "$"
|
233
|
+
end
|
234
|
+
s
|
235
|
+
end
|
236
|
+
|
237
|
+
def escape(value)
|
238
|
+
if value.kind_of? Array
|
239
|
+
return value.map { |v| addStartEnd(Regexp.escape(v)) }
|
240
|
+
else
|
241
|
+
return addStartEnd(Regexp.escape(value))
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
policy = response["Policy"]
|
246
|
+
s = policy["Subject"]
|
247
|
+
if policy["WhitelistedDomains"].empty?
|
248
|
+
subjectCNRegex = [ALL_ALLOWED_REGEX]
|
249
|
+
else
|
250
|
+
if policy["WildcardsAllowed"]
|
251
|
+
subjectCNRegex = policy["WhitelistedDomains"].map { |d| addStartEnd('[\w\-*]+' + Regexp.escape("." + d)) }
|
252
|
+
else
|
253
|
+
subjectCNRegex = policy["WhitelistedDomains"].map { |d| addStartEnd('[\w\-]+' + Regexp.escape("." + d)) }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
if s["OrganizationalUnit"]["Locked"]
|
257
|
+
subjectOURegexes = escape(s["OrganizationalUnit"]["Values"])
|
258
|
+
else
|
259
|
+
subjectOURegexes = [ALL_ALLOWED_REGEX]
|
260
|
+
end
|
261
|
+
if s["Organization"]["Locked"]
|
262
|
+
subjectORegexes = [escape(s["Organization"]["Value"])]
|
263
|
+
else
|
264
|
+
subjectORegexes = [ALL_ALLOWED_REGEX]
|
265
|
+
end
|
266
|
+
if s["City"]["Locked"]
|
267
|
+
subjectLRegexes = [escape(s["City"]["Value"])]
|
268
|
+
else
|
269
|
+
subjectLRegexes = [ALL_ALLOWED_REGEX]
|
270
|
+
end
|
271
|
+
if s["State"]["Locked"]
|
272
|
+
subjectSTRegexes = [escape(s["State"]["Value"])]
|
273
|
+
else
|
274
|
+
subjectSTRegexes = [ALL_ALLOWED_REGEX]
|
275
|
+
end
|
276
|
+
if s["Country"]["Locked"]
|
277
|
+
subjectCRegexes = [escape(s["Country"]["Value"])]
|
278
|
+
else
|
279
|
+
subjectCRegexes = [ALL_ALLOWED_REGEX]
|
280
|
+
end
|
281
|
+
if policy["SubjAltNameDnsAllowed"]
|
282
|
+
if policy["WhitelistedDomains"].length == 0
|
283
|
+
dnsSanRegExs = [ALL_ALLOWED_REGEX]
|
284
|
+
else
|
285
|
+
dnsSanRegExs = policy["WhitelistedDomains"].map { |d| addStartEnd('[\w-]+' + Regexp.escape("." + d)) }
|
286
|
+
end
|
287
|
+
else
|
288
|
+
dnsSanRegExs = []
|
289
|
+
end
|
290
|
+
if policy["SubjAltNameIpAllowed"]
|
291
|
+
ipSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
|
292
|
+
else
|
293
|
+
ipSanRegExs = []
|
294
|
+
end
|
295
|
+
if policy["SubjAltNameEmailAllowed"]
|
296
|
+
emailSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
|
297
|
+
else
|
298
|
+
emailSanRegExs = []
|
299
|
+
end
|
300
|
+
if policy["SubjAltNameUriAllowed"]
|
301
|
+
uriSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
|
302
|
+
else
|
303
|
+
uriSanRegExs = []
|
304
|
+
end
|
305
|
+
|
306
|
+
if policy["SubjAltNameUpnAllowed"]
|
307
|
+
upnSanRegExs = [ALL_ALLOWED_REGEX] # todo: support
|
308
|
+
else
|
309
|
+
upnSanRegExs = []
|
310
|
+
end
|
311
|
+
unless policy["KeyPair"]["KeyAlgorithm"]["Locked"]
|
312
|
+
key_types = [1024, 2048, 4096, 8192].map { |s| Vcert::KeyType.new("rsa", s) } + Vcert::SUPPORTED_CURVES.map { |c| Vcert::KeyType.new("ecdsa", c) }
|
313
|
+
else
|
314
|
+
if policy["KeyPair"]["KeyAlgorithm"]["Value"] == "RSA"
|
315
|
+
if policy["KeyPair"]["KeySize"]["Locked"]
|
316
|
+
key_types = [Vcert::KeyType.new("rsa", policy["KeyPair"]["KeySize"]["Value"])]
|
317
|
+
else
|
318
|
+
key_types = [1024, 2048, 4096, 8192].map { |s| Vcert::KeyType.new("rsa", s) }
|
319
|
+
end
|
320
|
+
elsif policy["KeyPair"]["KeyAlgorithm"]["Value"] == "EC"
|
321
|
+
if policy["KeyPair"]["EllipticCurve"]["Locked"]
|
322
|
+
curve = {"p224" => "secp224r1", "p256" => "prime256v1", "p521" => "secp521r1"}[policy["KeyPair"]["EllipticCurve"]["Value"].downcase]
|
323
|
+
key_types = [Vcert::KeyType.new("ecdsa", curve)]
|
324
|
+
else
|
325
|
+
key_types = Vcert::SUPPORTED_CURVES.map { |c| Vcert::KeyType.new("ecdsa", c) }
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
Vcert::Policy.new(policy_id: policy_dn(zone_tag), name: zone_tag, system_generated: false, creation_date: nil,
|
331
|
+
subject_cn_regexes: subjectCNRegex, subject_o_regexes: subjectORegexes,
|
332
|
+
subject_ou_regexes: subjectOURegexes, subject_st_regexes: subjectSTRegexes,
|
333
|
+
subject_l_regexes: subjectLRegexes, subject_c_regexes: subjectCRegexes, san_regexes: dnsSanRegExs,
|
334
|
+
key_types: key_types)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
|
data/lib/vcert.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
TIMEOUT = 420
|
5
|
+
|
6
|
+
module Vcert
|
7
|
+
class VcertError < StandardError ; end
|
8
|
+
class AuthenticationError < VcertError; end
|
9
|
+
class ServerUnexpectedBehaviorError < VcertError; end
|
10
|
+
class ClientBadDataError < VcertError; end
|
11
|
+
class ValidationError < VcertError; end
|
12
|
+
|
13
|
+
class Connection
|
14
|
+
def initialize(url: nil, user: nil, password: nil, cloud_token: nil, trust_bundle:nil, fake: false)
|
15
|
+
if fake
|
16
|
+
@conn = FakeConnection.new
|
17
|
+
elsif cloud_token != nil
|
18
|
+
@conn = CloudConnection.new url, cloud_token
|
19
|
+
elsif user != nil && password != nil && url != nil then
|
20
|
+
@conn = TPPConnection.new url, user, password, trust_bundle:trust_bundle
|
21
|
+
else
|
22
|
+
raise ClientBadDataError, "Invalid credentials list"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# @param [String] zone
|
28
|
+
# @param [Request] request
|
29
|
+
def request(zone, request)
|
30
|
+
@conn.request(zone, request)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param [Request] request
|
34
|
+
# @return [Certificate]
|
35
|
+
def retrieve(request)
|
36
|
+
@conn.retrieve(request)
|
37
|
+
end
|
38
|
+
|
39
|
+
def revoke(*args)
|
40
|
+
@conn.revoke(*args)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [String] zone
|
44
|
+
# @return [ZoneConfiguration]
|
45
|
+
def zone_configuration(zone)
|
46
|
+
@conn.zone_configuration(zone)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [String] zone
|
50
|
+
# @return [Policy]
|
51
|
+
def policy(zone)
|
52
|
+
@conn.policy(zone)
|
53
|
+
end
|
54
|
+
|
55
|
+
def renew(*args)
|
56
|
+
@conn.renew(*args)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param [Request] req
|
60
|
+
# @param [String] zone
|
61
|
+
# @param [Integer] timeout
|
62
|
+
# @return [Certificate]
|
63
|
+
def request_and_retrieve(req, zone, timeout: TIMEOUT)
|
64
|
+
request zone, req
|
65
|
+
cert = retrieve_loop(req, timeout: timeout)
|
66
|
+
return cert
|
67
|
+
end
|
68
|
+
|
69
|
+
def retrieve_loop(req, timeout: TIMEOUT)
|
70
|
+
t = Time.new() + timeout
|
71
|
+
loop do
|
72
|
+
if Time.new() > t
|
73
|
+
LOG.info("Waiting certificate #{req.id}")
|
74
|
+
break
|
75
|
+
end
|
76
|
+
certificate = @conn.retrieve(req)
|
77
|
+
if certificate != nil
|
78
|
+
return certificate
|
79
|
+
end
|
80
|
+
sleep 10
|
81
|
+
end
|
82
|
+
return nil
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
require 'fake/fake'
|
89
|
+
require 'cloud/cloud'
|
90
|
+
require 'tpp/tpp'
|
91
|
+
require 'objects/objects'
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vcert
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Denis Subbotin
|
8
|
+
- Alexander Rykalin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2019-09-18 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Ruby client for Venafi Cloud and Trust Protection Platform
|
15
|
+
email: opensource@venafi.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/cloud/cloud.rb
|
21
|
+
- lib/fake/fake.rb
|
22
|
+
- lib/objects/objects.rb
|
23
|
+
- lib/tpp/tpp.rb
|
24
|
+
- lib/vcert.rb
|
25
|
+
homepage: https://rubygems.org/gems/vcert
|
26
|
+
licenses:
|
27
|
+
- Apache-2.0
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 2.7.6
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Library for Venafi products
|
49
|
+
test_files: []
|