acmesmith 0.6.1 → 0.7.0.beta1

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
  SHA1:
3
- metadata.gz: e7fcd16986fecaf6cb855a668f261f191236f041
4
- data.tar.gz: c2995040c1e0681c9322243132e3393399fbbd07
3
+ metadata.gz: c4a9da6141839cd8a3b4850091f05ab7e024a252
4
+ data.tar.gz: 64807174e4ee28afd2a4be93d7ae8cbcdcf8cde1
5
5
  SHA512:
6
- metadata.gz: 1d2ba38ef47edf8b10a960dff3a608221dcc879a279a7713a9fa85f95141f03e67ead6aa227809951fd57b6f44fa04b6567a69c33cad53eef0f83a2f066aae51
7
- data.tar.gz: 35aefc2f03da1d5fa3c16858c9000349a2434c7f65cfea6ccffea74fdd36dc5ca6ead77d9f8acea07a94c6167b39889e92ea3fcc65b6eefe2d809d936b04cad7
6
+ metadata.gz: 1be84d5808225cf30cd6dc324a4a7924f1dacfdb310cc5872e7bdfa74d12ef93e7de5c234ba9ad4fe0679ce03ded44e832a66065544cb9dd808360fb7da4e5a3
7
+ data.tar.gz: 549065d60f3774ab1ef4be20da4b90d4f02f0e7b13af912b62128c65026ae5c1fb76880754251a3b69a929fe2d3a1581c8d09f5a8184829c57c33e317d875d4c
@@ -0,0 +1,235 @@
1
+ require 'acmesmith/account_key'
2
+ require 'acmesmith/certificate'
3
+
4
+ require 'acme-client'
5
+
6
+ module Acmesmith
7
+ class Client
8
+ def initialize(config: nil)
9
+ @config ||= config
10
+ end
11
+
12
+ def register(contact)
13
+ key = AccountKey.generate
14
+ acme = Acme::Client.new(private_key: key.private_key, endpoint: config['endpoint'])
15
+ registration = acme.register(contact: contact)
16
+ registration.agree_terms
17
+
18
+ storage.put_account_key(key, account_key_passphrase)
19
+
20
+ key
21
+ end
22
+
23
+ def authorize(*domains)
24
+ targets = domains.map do |domain|
25
+ authz = acme.authorize(domain: domain)
26
+ challenges = [authz.http01, authz.dns01, authz.tls_sni01].compact
27
+ challenge = nil
28
+ responder = config.challenge_responders.find do |x|
29
+ challenge = challenges.find { |_| x.support?(_.class::CHALLENGE_TYPE) }
30
+ end
31
+ {domain: domain, authz: authz, responder: responder, challenge: challenge}
32
+ end
33
+
34
+ begin
35
+ targets.each do |target|
36
+ target[:responder].respond(target[:domain], target[:challenge])
37
+ end
38
+
39
+ targets.each do |target|
40
+ puts "=> Requesting verifications..."
41
+ target[:challenge].request_verification
42
+ end
43
+ loop do
44
+ all_valid = true
45
+ targets.each do |target|
46
+ next if target[:valid]
47
+
48
+ status = target[:challenge].verify_status
49
+ puts " * [#{target[:domain]}] verify_status: #{status}"
50
+
51
+ if status == 'valid'
52
+ target[:valid] = true
53
+ next
54
+ end
55
+
56
+ all_valid = false
57
+ if status == "invalid"
58
+ err = target[:challenge].error
59
+ puts " ! [#{target[:domain]}] #{err["type"]}: #{err["detail"]}"
60
+ end
61
+ end
62
+ break if all_valid
63
+ sleep 3
64
+ end
65
+ puts "=> Done"
66
+ ensure
67
+ targets.each do |target|
68
+ target[:responder].cleanup(target[:domain], target[:challenge])
69
+ end
70
+ end
71
+ end
72
+
73
+ def request(common_name, *sans)
74
+ csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: sans)
75
+ retried = false
76
+ acme_cert = begin
77
+ acme.new_certificate(csr)
78
+ rescue Acme::Client::Error::Unauthorized => e
79
+ raise unless config.auto_authorize_on_request
80
+ raise if retried
81
+
82
+ puts "=> Authorizing unauthorized domain names"
83
+ # https://github.com/letsencrypt/boulder/blob/b9369a481415b3fe31e010b34e2ff570b89e42aa/ra/ra.go#L604
84
+ m = e.message.match(/authorizations for these names not found or expired: ((?:[a-zA-Z0-9_.\-]+(?:,\s+|$))+)/)
85
+ if m && m[1]
86
+ domains = m[1].split(/,\s+/)
87
+ else
88
+ warn " ! Error message on certificate request was #{e.message.inspect} and acmesmith couldn't determine which domain names are unauthorized (maybe a bug)"
89
+ warn " ! Attempting to authorize all domains in this certificate reuqest for now."
90
+ domains = [common_name, *sans]
91
+ end
92
+ puts " * #{domains.join(', ')}"
93
+ authorize(*domains)
94
+ retried = true
95
+ retry
96
+ end
97
+
98
+ cert = Certificate.from_acme_client_certificate(acme_cert)
99
+ storage.put_certificate(cert, certificate_key_passphrase)
100
+
101
+ execute_post_issue_hooks(common_name)
102
+
103
+ cert
104
+ end
105
+
106
+ def execute_post_issue_hooks(common_name)
107
+ post_issues_hooks_for_common_name = config.post_issueing_hooks(common_name)
108
+ if post_issues_hooks_for_common_name
109
+ post_issues_hooks_for_common_name.each do |hook|
110
+ hook.execute
111
+ end
112
+ end
113
+ end
114
+
115
+ def certificate_versions(common_name)
116
+ storage.list_certificate_versions(common_name).sort
117
+ end
118
+
119
+ def certificates_list
120
+ storage.list_certificates.sort
121
+ end
122
+
123
+ def current(common_name)
124
+ storage.get_current_certificate_version(common_name)
125
+ end
126
+
127
+ def get_certificate(common_name, version: 'current', type: 'text')
128
+ cert = storage.get_certificate(common_name, version: version)
129
+
130
+ certs = []
131
+ case type
132
+ when 'text'
133
+ certs << cert.certificate.to_text
134
+ certs << cert.certificate.to_pem
135
+ when 'certificate'
136
+ certs << cert.certificate.to_pem
137
+ when 'chain'
138
+ certs << cert.chain
139
+ when 'fullchain'
140
+ certs << cert.fullchain
141
+ end
142
+
143
+ certs
144
+ end
145
+
146
+ def save_certificate(common_name, version: 'current', mode: '0600', output:)
147
+ cert = storage.get_certificate(common_name, version: version)
148
+ File.open(output, 'w', mode.to_i(8)) do |f|
149
+ f.puts(cert.fullchain)
150
+ end
151
+ end
152
+
153
+ def get_private_key(common_name, version: 'current')
154
+ cert = storage.get_certificate(common_name, version: version)
155
+ cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
156
+
157
+ cert.private_key.to_pem
158
+ end
159
+
160
+ def save_private_key(common_name, version: 'current', mode: '0600', output:)
161
+ cert = storage.get_certificate(common_name, version: version)
162
+ cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
163
+ File.open(output, 'w', mode.to_i(8)) do |f|
164
+ f.puts(cert.private_key)
165
+ end
166
+ end
167
+
168
+ def save_pkcs12(common_name, version: 'current', mode: '0600', output:, passphrase:)
169
+ cert = storage.get_certificate(common_name, version: version)
170
+ cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
171
+
172
+ p12 = OpenSSL::PKCS12.create(passphrase, cert.common_name, cert.private_key, cert.certificate)
173
+ File.open(output, 'w', mode.to_i(8)) do |f|
174
+ f.puts p12.to_der
175
+ end
176
+ end
177
+
178
+ def autorenew(days)
179
+ storage.list_certificates.each do |cn|
180
+ puts "=> #{cn}"
181
+ cert = storage.get_certificate(cn)
182
+ not_after = cert.certificate.not_after.utc
183
+
184
+ puts " Not valid after: #{not_after}"
185
+ next unless (cert.certificate.not_after.utc - Time.now.utc) < (days.to_i * 86400)
186
+ puts " * Renewing: CN=#{cert.common_name}, SANs=#{cert.sans.join(',')}"
187
+ request(cert.common_name, *cert.sans)
188
+ end
189
+ end
190
+
191
+ def add_san(common_name, *add_sans)
192
+ puts "=> reissuing CN=#{common_name} with new SANs #{add_sans.join(?,)}"
193
+ cert = storage.get_certificate(common_name)
194
+ sans = cert.sans + add_sans
195
+ puts " * SANs will be: #{sans.join(?,)}"
196
+ request(cert.common_name, *sans)
197
+ end
198
+
199
+ private
200
+
201
+ def config
202
+ @config
203
+ end
204
+
205
+ def storage
206
+ config.storage
207
+ end
208
+
209
+ def account_key
210
+ @account_key ||= storage.get_account_key.tap do |x|
211
+ x.key_passphrase = account_key_passphrase if account_key_passphrase
212
+ end
213
+ end
214
+
215
+ def acme
216
+ @acme ||= Acme::Client.new(private_key: account_key.private_key, endpoint: config['endpoint'])
217
+ end
218
+
219
+ def certificate_key_passphrase
220
+ if config['passphrase_from_env']
221
+ ENV['ACMESMITH_CERTIFICATE_KEY_PASSPHRASE'] || config['certificate_key_passphrase']
222
+ else
223
+ config['certificate_key_passphrase']
224
+ end
225
+ end
226
+
227
+ def account_key_passphrase
228
+ if config['passphrase_from_env']
229
+ ENV['ACMESMITH_ACCOUNT_KEY_PASSPHRASE'] || config['account_key_passphrase']
230
+ else
231
+ config['account_key_passphrase']
232
+ end
233
+ end
234
+ end
235
+ end
@@ -1,10 +1,7 @@
1
1
  require 'thor'
2
2
 
3
3
  require 'acmesmith/config'
4
- require 'acmesmith/account_key'
5
- require 'acmesmith/certificate'
6
-
7
- require 'acme-client'
4
+ require 'acmesmith/client'
8
5
 
9
6
  module Acmesmith
10
7
  class Command < Thor
@@ -13,138 +10,48 @@ module Acmesmith
13
10
 
14
11
  desc "register CONTACT", "Create account key (contact e.g. mailto:xxx@example.org)"
15
12
  def register(contact)
16
- key = AccountKey.generate
17
- acme = Acme::Client.new(private_key: key.private_key, endpoint: config['endpoint'])
18
- registration = acme.register(contact: contact)
19
- registration.agree_terms
20
-
21
- storage.put_account_key(key, account_key_passphrase)
13
+ key = client.register(contact)
22
14
  puts "Generated:\n#{key.private_key.public_key.to_pem}"
23
15
  end
24
16
 
25
17
  desc "authorize DOMAIN [DOMAIN ...]", "Get authz for DOMAIN."
26
18
  def authorize(*domains)
27
- targets = domains.map do |domain|
28
- authz = acme.authorize(domain: domain)
29
- challenges = [authz.http01, authz.dns01, authz.tls_sni01].compact
30
- challenge = nil
31
- responder = config.challenge_responders.find do |x|
32
- challenge = challenges.find { |_| x.support?(_.class::CHALLENGE_TYPE) }
33
- end
34
- {domain: domain, authz: authz, responder: responder, challenge: challenge}
35
- end
36
-
37
- begin
38
- targets.each do |target|
39
- target[:responder].respond(target[:domain], target[:challenge])
40
- end
41
-
42
- targets.each do |target|
43
- puts "=> Requesting verifications..."
44
- target[:challenge].request_verification
45
- end
46
- loop do
47
- all_valid = true
48
- targets.each do |target|
49
- next if target[:valid]
50
-
51
- status = target[:challenge].verify_status
52
- puts " * [#{target[:domain]}] verify_status: #{status}"
53
-
54
- if status == 'valid'
55
- target[:valid] = true
56
- next
57
- end
58
-
59
- all_valid = false
60
- if status == "invalid"
61
- err = target[:challenge].error
62
- puts " ! [#{target[:domain]}] #{err["type"]}: #{err["detail"]}"
63
- end
64
- end
65
- break if all_valid
66
- sleep 3
67
- end
68
- puts "=> Done"
69
- ensure
70
- targets.each do |target|
71
- target[:responder].cleanup(target[:domain], target[:challenge])
72
- end
73
- end
19
+ client.authorize(*domains)
74
20
  end
75
21
 
76
22
  desc "request COMMON_NAME [SAN]", "request certificate for CN +COMMON_NAME+ with SANs +SAN+"
77
23
  def request(common_name, *sans)
78
- csr = Acme::Client::CertificateRequest.new(common_name: common_name, names: sans)
79
- retried = false
80
- acme_cert = begin
81
- acme.new_certificate(csr)
82
- rescue Acme::Client::Error::Unauthorized => e
83
- raise unless config.auto_authorize_on_request
84
- raise if retried
85
-
86
- puts "=> Authorizing unauthorized domain names"
87
- # https://github.com/letsencrypt/boulder/blob/b9369a481415b3fe31e010b34e2ff570b89e42aa/ra/ra.go#L604
88
- m = e.message.match(/authorizations for these names not found or expired: ((?:[a-zA-Z0-9_.\-]+(?:,\s+|$))+)/)
89
- if m && m[1]
90
- domains = m[1].split(/,\s+/)
91
- else
92
- warn " ! Error message on certificate request was #{e.message.inspect} and acmesmith couldn't determine which domain names are unauthorized (maybe a bug)"
93
- warn " ! Attempting to authorize all domains in this certificate reuqest for now."
94
- domains = [common_name, *sans]
95
- end
96
- puts " * #{domains.join(', ')}"
97
- authorize(*domains)
98
- retried = true
99
- retry
100
- end
101
-
102
- cert = Certificate.from_acme_client_certificate(acme_cert)
103
- storage.put_certificate(cert, certificate_key_passphrase)
104
-
24
+ cert = client.request(common_name, *sans)
105
25
  puts cert.certificate.to_text
106
26
  puts cert.certificate.to_pem
107
-
108
- execute_post_issue_hooks(common_name)
109
27
  end
110
28
 
111
29
  desc "post-issue-hooks COMMON_NAME", "Run all post-issueing hooks for common name. (for testing purpose)"
112
30
  def post_issue_hooks(common_name)
113
- execute_post_issue_hooks(common_name)
31
+ client.post_issue_hooks(common_name)
114
32
  end
115
33
  map 'post-issue-hooks' => :post_issue_hooks
116
34
 
117
35
  desc "list [COMMON_NAME]", "list certificates or its versions"
118
36
  def list(common_name = nil)
119
37
  if common_name
120
- puts storage.list_certificate_versions(common_name).sort
38
+ puts client.certificate_versions(common_name)
121
39
  else
122
- puts storage.list_certificates.sort
40
+ puts client.certificates_list
123
41
  end
124
42
  end
125
43
 
126
44
  desc "current COMMON_NAME", "show current version for certificate"
127
45
  def current(common_name)
128
- puts storage.get_current_certificate_version(common_name)
46
+ puts client.current(common_name)
129
47
  end
130
48
 
131
49
  desc "show-certificate COMMON_NAME", "show certificate"
132
50
  method_option :version, type: :string, default: 'current'
133
51
  method_option :type, type: :string, enum: %w(text certificate chain fullchain), default: 'text'
134
52
  def show_certificate(common_name)
135
- cert = storage.get_certificate(common_name, version: options[:version])
136
-
137
- case options[:type]
138
- when 'text'
139
- puts cert.certificate.to_text
140
- puts cert.certificate.to_pem
141
- when 'certificate'
142
- puts cert.certificate.to_pem
143
- when 'chain'
144
- puts cert.chain
145
- when 'fullchain'
146
- puts cert.fullchain
147
- end
53
+ certs = client.get_certificate(common_name, version: options[:version], type: options[:type])
54
+ puts certs
148
55
  end
149
56
  map 'show-certiticate' => :show_certificate
150
57
 
@@ -153,19 +60,13 @@ module Acmesmith
153
60
  method_option :output, type: :string, required: true, banner: 'PATH', desc: 'Path to output file'
154
61
  method_option :mode, type: :string, default: '0600', desc: 'Mode (permission) of the output file on create'
155
62
  def save_certificate(common_name)
156
- cert = storage.get_certificate(common_name, version: options[:version])
157
- File.open(options[:output], 'w', options[:mode].to_i(8)) do |f|
158
- f.puts(cert.fullchain)
159
- end
63
+ client.save_certificate(common_name, version: options[:version], mode: options[:mode], output: options[:output])
160
64
  end
161
65
 
162
66
  desc "show-private-key COMMON_NAME", "show private key"
163
67
  method_option :version, type: :string, default: 'current'
164
68
  def show_private_key(common_name)
165
- cert = storage.get_certificate(common_name, version: options[:version])
166
- cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
167
-
168
- puts cert.private_key.to_pem
69
+ puts client.get_private_key(common_name, version: options[:version])
169
70
  end
170
71
  map 'show-private-key' => :show_private_key
171
72
 
@@ -174,11 +75,7 @@ module Acmesmith
174
75
  method_option :output, type: :string, required: true, banner: 'PATH', desc: 'Path to output file'
175
76
  method_option :mode, type: :string, default: '0600', desc: 'Mode (permission) of the output file on create'
176
77
  def save_private_key(common_name)
177
- cert = storage.get_certificate(common_name, version: options[:version])
178
- cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
179
- File.open(options[:output], 'w', options[:mode].to_i(8)) do |f|
180
- f.puts(cert.private_key)
181
- end
78
+ client.save_private_key(common_name, version: options[:version], mode: options[:mode], output: options[:output])
182
79
  end
183
80
 
184
81
  desc 'save-pkcs12 COMMON_NAME', 'Save ceriticate and private key to .p12 file'
@@ -186,9 +83,6 @@ module Acmesmith
186
83
  method_option :output, type: :string, required: true, banner: 'PATH', desc: 'Path to output file'
187
84
  method_option :mode, type: :string, default: '0600', desc: 'Mode (permission) of the output file on create'
188
85
  def save_pkcs12(common_name)
189
- cert = storage.get_certificate(common_name, version: options[:version])
190
- cert.key_passphrase = certificate_key_passphrase if certificate_key_passphrase
191
-
192
86
  print 'Passphrase: '
193
87
  passphrase = $stdin.noecho { $stdin.gets }.chomp
194
88
  print "\nPassphrase (confirm): "
@@ -196,81 +90,26 @@ module Acmesmith
196
90
  puts
197
91
 
198
92
  raise ArgumentError, "Passphrase doesn't match" if passphrase != passphrase2
199
-
200
- p12 = OpenSSL::PKCS12.create(passphrase, cert.common_name, cert.private_key, cert.certificate)
201
- File.open(options[:output], 'w', options[:mode].to_i(8)) do |f|
202
- f.puts p12.to_der
203
- end
93
+ client.save_pkcs12(common_name, version: options[:version], mode: options[:mode], output: options[:output], passphrase: passphrase)
204
94
  end
205
95
 
206
96
  desc "autorenew", "request renewal of certificates which expires soon"
207
97
  method_option :days, type: :numeric, aliases: %w(-d), default: 7, desc: 'specify threshold in days to select certificates to renew'
208
98
  def autorenew
209
- storage.list_certificates.each do |cn|
210
- puts "=> #{cn}"
211
- cert = storage.get_certificate(cn)
212
- not_after = cert.certificate.not_after.utc
213
-
214
- puts " Not valid after: #{not_after}"
215
- next unless (cert.certificate.not_after.utc - Time.now.utc) < (options[:days].to_i * 86400)
216
- puts " * Renewing: CN=#{cert.common_name}, SANs=#{cert.sans.join(',')}"
217
- request(cert.common_name, *cert.sans)
218
- end
99
+ client.autorenew(options[:days])
219
100
  end
220
101
 
221
102
  desc "add-san COMMON_NAME [ADDITIONAL_SANS]", "request renewal of existing certificate with additional SANs"
222
103
  def add_san(common_name, *add_sans)
223
- puts "=> reissuing CN=#{common_name} with new SANs #{add_sans.join(?,)}"
224
- cert = storage.get_certificate(common_name)
225
- sans = cert.sans + add_sans
226
- puts " * SANs will be: #{sans.join(?,)}"
227
- request(cert.common_name, *sans)
104
+ client.add_san(common_name, *add_sans)
228
105
  end
229
106
 
230
107
  private
231
108
 
232
- def config
233
- @config ||= Config.load_yaml(options[:config])
234
- end
235
-
236
- def storage
237
- config.storage
109
+ def client
110
+ config = Config.load_yaml(options[:config])
111
+ config.merge!("passphrase_from_env" => options[:passphrase_from_env]) unless options[:passphrase_from_env].nil?
112
+ @client = Client.new(config: config)
238
113
  end
239
-
240
- def account_key
241
- @account_key ||= storage.get_account_key.tap do |x|
242
- x.key_passphrase = account_key_passphrase if account_key_passphrase
243
- end
244
- end
245
-
246
- def acme
247
- @acme ||= Acme::Client.new(private_key: account_key.private_key, endpoint: config['endpoint'])
248
- end
249
-
250
- def certificate_key_passphrase
251
- if options[:passphrase_from_env] || config['passphrase_from_env']
252
- ENV['ACMESMITH_CERTIFICATE_KEY_PASSPHRASE'] || config['certificate_key_passphrase']
253
- else
254
- config['certificate_key_passphrase']
255
- end
256
- end
257
-
258
- def account_key_passphrase
259
- if options[:passphrase_from_env] || config['passphrase_from_env']
260
- ENV['ACMESMITH_ACCOUNT_KEY_PASSPHRASE'] || config['account_key_passphrase']
261
- else
262
- config['account_key_passphrase']
263
- end
264
- end
265
-
266
- def execute_post_issue_hooks(common_name)
267
- post_issues_hooks_for_common_name = config.post_issueing_hooks(common_name)
268
- if post_issues_hooks_for_common_name
269
- post_issues_hooks_for_common_name.each do |hook|
270
- hook.execute
271
- end
272
- end
273
- end
274
-
275
114
  end
276
115
  end
@@ -28,6 +28,10 @@ module Acmesmith
28
28
  @config[key]
29
29
  end
30
30
 
31
+ def merge!(pair)
32
+ @config.merge!(pair)
33
+ end
34
+
31
35
  def account_key_passphrase
32
36
  @config['account_key_passphrase']
33
37
  end
@@ -1,3 +1,3 @@
1
1
  module Acmesmith
2
- VERSION = "0.6.1"
2
+ VERSION = "0.7.0.beta1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acmesmith
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - sorah (Shota Fukumori)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-06 00:00:00.000000000 Z
11
+ date: 2017-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acme-client
@@ -124,6 +124,7 @@ files:
124
124
  - lib/acmesmith/challenge_responders.rb
125
125
  - lib/acmesmith/challenge_responders/base.rb
126
126
  - lib/acmesmith/challenge_responders/route53.rb
127
+ - lib/acmesmith/client.rb
127
128
  - lib/acmesmith/command.rb
128
129
  - lib/acmesmith/config.rb
129
130
  - lib/acmesmith/post_issueing_hooks.rb
@@ -152,12 +153,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
153
  version: '0'
153
154
  required_rubygems_version: !ruby/object:Gem::Requirement
154
155
  requirements:
155
- - - ">="
156
+ - - ">"
156
157
  - !ruby/object:Gem::Version
157
- version: '0'
158
+ version: 1.3.1
158
159
  requirements: []
159
160
  rubyforge_project:
160
- rubygems_version: 2.6.8
161
+ rubygems_version: 2.6.11
161
162
  signing_key:
162
163
  specification_version: 4
163
164
  summary: ACME client (Let's encrypt client) to manage certificate in multi server