letscert 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,29 @@
1
+ module LetsCert
2
+
3
+ # Account key IO plugin
4
+ # @author Sylvain Daubert
5
+ class AccountKey < IOPlugin
6
+ include FileIOPluginMixin
7
+ include JWKIOPluginMixin
8
+
9
+ # @return [Hash] always get +true+ for +:account_key+ key
10
+ def persisted
11
+ { account_key: true }
12
+ end
13
+
14
+ # @return [Hash]
15
+ def load_from_content(content)
16
+ { account_key: load_jwk(content) }
17
+ end
18
+
19
+ # Save account key.
20
+ # @param [Hash] data
21
+ # @return [void]
22
+ def save(data)
23
+ save_to_file(dump_jwk(data[:account_key]))
24
+ end
25
+
26
+ end
27
+
28
+ IOPlugin.register(AccountKey, 'account_key.json')
29
+ end
@@ -0,0 +1,29 @@
1
+ module LetsCert
2
+
3
+ # Cert file plugin
4
+ # @author Sylvain Daubert
5
+ class CertFile < OpenSSLIOPlugin
6
+ include FileIOPluginMixin
7
+
8
+ # @return [Hash] always get +true+ for +:cert+ key
9
+ def persisted
10
+ @persisted ||= { cert: true }
11
+ end
12
+
13
+ # @return [Hash]
14
+ def load_from_content(content)
15
+ { cert: load_cert(content) }
16
+ end
17
+
18
+ # Save certificate.
19
+ # @param [Hash] data
20
+ # @return [void]
21
+ def save(data)
22
+ save_to_file(dump_cert(data[:cert]))
23
+ end
24
+
25
+ end
26
+
27
+ IOPlugin.register(CertFile, 'cert.pem', :pem)
28
+ IOPlugin.register(CertFile, 'cert.der', :der)
29
+ end
@@ -0,0 +1,32 @@
1
+ module LetsCert
2
+
3
+ # Chain file plugin
4
+ # @author Sylvain Daubert
5
+ class ChainFile < OpenSSLIOPlugin
6
+ include FileIOPluginMixin
7
+
8
+ # @return [Hash] always get +true+ for +:chain+ key
9
+ def persisted
10
+ @persisted ||= { chain: true }
11
+ end
12
+
13
+ # @return [Hash]
14
+ def load_from_content(content)
15
+ chain = []
16
+ split_pems(content) do |pem|
17
+ chain << load_cert(pem)
18
+ end
19
+ { chain: chain }
20
+ end
21
+
22
+ # Save chain.
23
+ # @param [Hash] data
24
+ # @return [void]
25
+ def save(data)
26
+ save_to_file(data[:chain].map { |c| dump_cert(c) }.join)
27
+ end
28
+
29
+ end
30
+
31
+ IOPlugin.register(ChainFile, 'chain.pem', :pem)
32
+ end
@@ -0,0 +1,48 @@
1
+ module LetsCert
2
+
3
+ # Mixin for IOPmugin subclasses that handle files
4
+ # @author Sylvain Daubert
5
+ module FileIOPluginMixin
6
+
7
+ # Load data from file named +#name+
8
+ # @return [Hash]
9
+ def load
10
+ logger.debug { "Loading #{@name}" }
11
+
12
+ begin
13
+ content = File.read(@name)
14
+ rescue Errno::ENOENT => ex
15
+ logger.info { "no #{@name} file" }
16
+ return self.class.empty_data
17
+ end
18
+
19
+ load_from_content(content)
20
+ end
21
+
22
+ # @abstract
23
+ # @param [String] _content
24
+ # @return [Hash]
25
+ def load_from_content(_content)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ # Save data to file +#name+
30
+ # @param [Hash] data
31
+ # @return [void]
32
+ def save_to_file(data)
33
+ return if data.nil?
34
+
35
+ logger.info { "saving #{@name}" }
36
+ begin
37
+ File.open(name, 'w') do |f|
38
+ f.write(data)
39
+ end
40
+ rescue Errno => ex
41
+ @logger.error { ex.message }
42
+ raise Error, "Error when saving #{@name}"
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,39 @@
1
+ module LetsCert
2
+
3
+ # Fullchain file plugin
4
+ # @author Sylvain Daubert
5
+ class FullChainFile < ChainFile
6
+
7
+ # @return [Hash] always get +true+ for +:cert+ and +:chain+ keys
8
+ def persisted
9
+ @persisted ||= { cert: true, chain: true }
10
+ end
11
+
12
+ # Load full certificate chain
13
+ # @return [Hash]
14
+ def load
15
+ data = super
16
+ if data[:chain].nil? or data[:chain].empty?
17
+ cert = nil
18
+ chain = []
19
+ else
20
+ cert = data[:chain].shift
21
+ chain = data[:chain]
22
+ end
23
+
24
+ { account_key: data[:account_key], key: data[:key], cert: cert,
25
+ chain: chain }
26
+ end
27
+
28
+ # Save fullchain.
29
+ # @param [Hash] data
30
+ # @return [void]
31
+ def save(data)
32
+ super(account_key: data[:account_key], key: data[:key], cert: nil,
33
+ chain: [data[:cert]] + data[:chain])
34
+ end
35
+
36
+ end
37
+
38
+ IOPlugin.register(FullChainFile, 'fullchain.pem', :pem)
39
+ end
@@ -0,0 +1,68 @@
1
+ require 'base64'
2
+
3
+ module LetsCert
4
+
5
+ # Mixin for IOPlugin subclasses that handle JWK
6
+ # @author Sylvain Daubert
7
+ module JWKIOPluginMixin
8
+
9
+ # Encode string +data+ to base64
10
+ # @param [String] data
11
+ # @return [String]
12
+ def urlsafe_encode64(data)
13
+ Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
14
+ end
15
+
16
+ # Decode base64 string +data+
17
+ # @param [String] data
18
+ # @return [String]
19
+ def urlsafe_decode64(data)
20
+ Base64.urlsafe_decode64(data.sub(/[\s=]*\z/, ''))
21
+ end
22
+
23
+ # Load crypto data from JSON-encoded file
24
+ # @param [String] data JSON-encoded data
25
+ # @return [OpenSSL::PKey::PKey]
26
+ def load_jwk(data)
27
+ return nil if data.empty?
28
+
29
+ h = JSON.parse(data)
30
+ case h['kty']
31
+ when 'RSA'
32
+ pkey = OpenSSL::PKey::RSA.new
33
+ %w(e n d p q).collect do |key|
34
+ next if h[key].nil?
35
+ value = OpenSSL::BN.new(urlsafe_decode64(h[key]), 2)
36
+ pkey.send "#{key}=".to_sym, value
37
+ end
38
+ else
39
+ raise Error, "unknown account key type '#{k['kty']}'"
40
+ end
41
+
42
+ pkey
43
+ end
44
+
45
+ # Dump crypto data (key) to a JSON-encoded string
46
+ # @param [OpenSSL::PKey] key
47
+ # @return [String]
48
+ def dump_jwk(key)
49
+ return {}.to_json if key.nil?
50
+
51
+ h = { 'kty' => 'RSA' }
52
+ case key
53
+ when OpenSSL::PKey::RSA
54
+ h['e'] = urlsafe_encode64(key.e.to_s(2)) if key.e
55
+ h['n'] = urlsafe_encode64(key.n.to_s(2)) if key.n
56
+ if key.private?
57
+ h['d'] = urlsafe_encode64(key.d.to_s(2))
58
+ h['p'] = urlsafe_encode64(key.p.to_s(2))
59
+ h['q'] = urlsafe_encode64(key.q.to_s(2))
60
+ end
61
+ else
62
+ raise Error, 'only RSA keys are supported'
63
+ end
64
+ h.to_json
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,29 @@
1
+ module LetsCert
2
+
3
+ # Key file plugin
4
+ # @author Sylvain Daubert
5
+ class KeyFile < OpenSSLIOPlugin
6
+ include FileIOPluginMixin
7
+
8
+ # @return [Hash] always get +true+ for +:key+ key
9
+ def persisted
10
+ @persisted ||= { key: true }
11
+ end
12
+
13
+ # @return [Hash]
14
+ def load_from_content(content)
15
+ { key: load_key(content) }
16
+ end
17
+
18
+ # Save private key.
19
+ # @param [Hash] data
20
+ # @return [void]
21
+ def save(data)
22
+ save_to_file(dump_key(data[:key]))
23
+ end
24
+
25
+ end
26
+
27
+ IOPlugin.register(KeyFile, 'key.pem', :pem)
28
+ IOPlugin.register(KeyFile, 'key.der', :der)
29
+ end
@@ -0,0 +1,68 @@
1
+ module LetsCert
2
+
3
+ # OpenSSL IOPlugin
4
+ # @author Sylvain Daubert
5
+ class OpenSSLIOPlugin < IOPlugin
6
+
7
+ # @private Regular expression to discriminate PEM
8
+ PEM_RE = /^-----BEGIN CERTIFICATE-----\n.*?\n-----END CERTIFICATE-----\n/m
9
+
10
+ # @param [String] name filename
11
+ # @param [:pem,:der] type
12
+ def initialize(name, type)
13
+ case type
14
+ when :pem
15
+ when :der
16
+ else
17
+ raise ArgumentError, 'type should be :pem or :der'
18
+ end
19
+
20
+ @type = type
21
+ super(name)
22
+ end
23
+
24
+ # Load key from raw +data+
25
+ # @param [String] data
26
+ # @return [OpenSSL::PKey]
27
+ def load_key(data)
28
+ OpenSSL::PKey::RSA.new data
29
+ end
30
+
31
+ # Dump key/cert data
32
+ # @param [OpenSSL::PKey] key
33
+ # @return [String]
34
+ def dump_key(key)
35
+ case @type
36
+ when :pem
37
+ key.to_pem
38
+ when :der
39
+ key.to_der
40
+ end
41
+ end
42
+ alias dump_cert dump_key
43
+
44
+ # Load certificate from raw +data+
45
+ # @param [String] data
46
+ # @return [OpenSSL::X509::Certificate]
47
+ def load_cert(data)
48
+ OpenSSL::X509::Certificate.new data
49
+ end
50
+
51
+ private
52
+
53
+ # Split concatenated PEMs.
54
+ # @param [String] data
55
+ # @yield [String] pem
56
+ def split_pems(data)
57
+ my_data = data
58
+ m = my_data.match(PEM_RE)
59
+ while m
60
+ yield m[0]
61
+ my_data = my_data[m.end(0)..-1]
62
+ m = my_data.match(PEM_RE)
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -41,8 +41,8 @@ module LetsCert
41
41
  module ClassMethods
42
42
 
43
43
  # @private hook called when a subclass is created.
44
- # Take care of all subclasses to later properly set @logger class instance
45
- # variable.
44
+ # Take care of all subclasses to later properly set @logger class
45
+ # instance variable.
46
46
  # @param [Class] subclass
47
47
  # @return [void]
48
48
  def inherited(subclass)
@@ -69,5 +69,4 @@ module LetsCert
69
69
  end
70
70
 
71
71
  end
72
-
73
72
  end
@@ -25,86 +25,15 @@ require 'fileutils'
25
25
 
26
26
  require_relative 'io_plugin'
27
27
  require_relative 'certificate'
28
+ require_relative 'runner/logger_formatter'
29
+ require_relative 'runner/valid_time'
28
30
 
29
31
  module LetsCert
30
32
 
31
33
  # Runner class: analyse and execute CLI commands.
32
34
  # @author Sylvain Daubert
35
+ # rubocop:disable Metrics/ClassLength
33
36
  class Runner
34
- # Get options
35
- # @return [Hash]
36
- attr_reader :options
37
- # @return [Logger]
38
- attr_accessor :logger
39
-
40
- # Custom logger formatter
41
- class LoggerFormatter < Logger::Formatter
42
-
43
- # @private log format
44
- FORMAT = "[%s] %5s: %s\n"
45
-
46
- # @param [String] severity
47
- # @param [Datetime] time
48
- # @param [nil,String] progname
49
- # @param [String] msg
50
- # @return [String]
51
- def call(severity, time, progname, msg)
52
- FORMAT % [format_datetime(time), severity, msg2str(msg)]
53
- end
54
-
55
-
56
- private
57
-
58
- # @private simple datetime formatter
59
- # @param [DateTime] time
60
- # @return [String]
61
- def format_datetime(time)
62
- time.strftime("%Y-%m-%d %H:%M:%S")
63
- end
64
-
65
- end
66
-
67
- # Class used to process validation time from String.
68
- # @author Sylvain Daubert
69
- class ValidTime
70
-
71
- # @param [String] str time string. May be:
72
- # * an integer -> time in seconds
73
- # * an integer plus a letter:
74
- # * 30m: 30 minutes,
75
- # * 30h: 30 hours,
76
- # * 30d: 30 days.
77
- def initialize(str)
78
- m = str.match(/^(\d+)([mhd])?$/)
79
- if m
80
- case m[2]
81
- when nil
82
- @seconds = m[1].to_i
83
- when 'm'
84
- @seconds = m[1].to_i * 60
85
- when 'h'
86
- @seconds = m[1].to_i * 60 * 60
87
- when 'd'
88
- @seconds = m[1].to_i * 24 * 60 * 60
89
- end
90
- else
91
- raise OptionParser::InvalidArgument, "invalid argument: --valid-min #{str}"
92
- end
93
- @string = str
94
- end
95
-
96
- # Get time in seconds
97
- # @return [Integer]
98
- def to_seconds
99
- @seconds
100
- end
101
-
102
- # Get time as string
103
- # @return [String]
104
- def to_s
105
- @string
106
- end
107
- end
108
37
 
109
38
  # Exit value for OK
110
39
  RETURN_OK = 1
@@ -112,9 +41,12 @@ module LetsCert
112
41
  RETURN_OK_CERT = 0
113
42
  # Exit value for error(s)
114
43
  RETURN_ERROR = 2
115
-
44
+
45
+ # Get options
46
+ # @return [Hash]
47
+ attr_reader :options
116
48
  # @return [Logger]
117
- attr_reader :logger
49
+ attr_accessor :logger
118
50
 
119
51
  # Run LetsCert
120
52
  # @return [Integer]
@@ -125,7 +57,6 @@ module LetsCert
125
57
  runner.run
126
58
  end
127
59
 
128
-
129
60
  def initialize
130
61
  @options = {
131
62
  verbose: 0,
@@ -134,9 +65,9 @@ module LetsCert
134
65
  cert_key_size: 2048,
135
66
  valid_min: ValidTime.new('30d'),
136
67
  account_key_size: 4096,
137
- tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f35226540f',
138
- user_agent: "letscert/#{VERSION.gsub(/\..*/, '')}",
139
- server: 'https://acme-v01.api.letsencrypt.org/directory',
68
+ tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f3' \
69
+ '5226540f',
70
+ server: 'https://acme-v01.api.letsencrypt.org/directory'
140
71
  }
141
72
 
142
73
  @logger = Logger.new($stdout)
@@ -148,62 +79,19 @@ module LetsCert
148
79
  # * 1 if renewal was not necessery
149
80
  # * 2 in case of errors
150
81
  def run
151
- if @options[:print_help]
152
- puts @opt_parser
153
- exit RETURN_OK
154
- end
155
-
156
- if @options[:show_version]
157
- puts "letscert #{LetsCert::VERSION}"
158
- puts "Copyright (c) 2016 Sylvain Daubert"
159
- puts "License MIT: see http://opensource.org/licenses/MIT"
160
- exit RETURN_OK
161
- end
162
-
163
- case @options[:verbose]
164
- when 0
165
- @logger.level = Logger::Severity::WARN
166
- when 1
167
- @logger.level = Logger::Severity::INFO
168
- when 2..5
169
- @logger.level = Logger::Severity::DEBUG
170
- end
171
-
172
- @logger.debug { "options are: #{@options.inspect}" }
173
-
174
- IOPlugin.logger = @logger
175
- Certificate.logger = @logger
82
+ print_help_if_needed
83
+ show_version_if_needed
84
+ set_logger_level
85
+ set_logger
176
86
 
177
87
  begin
178
- if @options[:domains].empty?
179
- raise Error, "At leat one domain must be given with --domain option.\n" +
180
- "Try 'letscert --help' for more information."
181
- end
182
-
88
+ check_domains
183
89
  if @options[:revoke]
184
- data = load_data_from_disk(IOPlugin.registered.keys)
185
- certificate = Certificate.new(data[:cert])
186
- if certificate.revoke(data[:account_key], @options)
187
- RETURN_OK
188
- else
189
- RETURN_ERROR
190
- end
90
+ revoke
191
91
  else
192
92
  check_persisted
193
-
194
- data = load_data_from_disk(@options[:files])
195
-
196
- certificate = Certificate.new(data[:cert])
197
- if certificate.valid?(@options[:domains], @options[:valid_min].to_seconds)
198
- @logger.info { 'no need to update cert' }
199
- RETURN_OK
200
- else
201
- # update/create cert
202
- certificate.get data[:account_key], data[:key], @options
203
- RETURN_OK_CERT
204
- end
93
+ get_certificate
205
94
  end
206
-
207
95
  rescue Error, Acme::Client::Error => ex
208
96
  msg = ex.message
209
97
  msg = "[Acme] #{msg}" if ex.is_a?(Acme::Client::Error)
@@ -213,13 +101,13 @@ module LetsCert
213
101
  end
214
102
  end
215
103
 
216
-
217
104
  # Parse line command options
218
105
  # @raise [OptionParser::InvalidOption] on unrecognized or malformed option
219
106
  # @return [void]
107
+ # rubocop:disable Metrics/MethodLength
220
108
  def parse_options
221
109
  @opt_parser = OptionParser.new do |opts|
222
- opts.banner = "Usage: lestcert [options]"
110
+ opts.banner = 'Usage: lestcert [options]'
223
111
 
224
112
  opts.separator('')
225
113
 
@@ -229,8 +117,9 @@ module LetsCert
229
117
  opts.on('-V', '--version', 'Show version and exit') do |v|
230
118
  @options[:show_version] = v
231
119
  end
232
- opts.on('-v', '--verbose', 'Run verbosely') { |v| @options[:verbose] += 1 if v }
233
-
120
+ opts.on('-v', '--verbose', 'Run verbosely') do |v|
121
+ @options[:verbose] += 1 if v
122
+ end
234
123
 
235
124
  opts.separator("\nWebroot manager:")
236
125
 
@@ -252,7 +141,7 @@ module LetsCert
252
141
  @options[:revoke] = revoke
253
142
  end
254
143
 
255
- opts.on("-f", "--file FILE", 'Input/output file.',
144
+ opts.on('-f', '--file FILE', 'Input/output file.',
256
145
  'Can be specified multiple times',
257
146
  'Allowed values: account_key.json, cert.der,',
258
147
  'cert.pem, chain.pem, full.pem,',
@@ -269,9 +158,10 @@ module LetsCert
269
158
  opts.accept(ValidTime) do |valid_time|
270
159
  ValidTime.new(valid_time)
271
160
  end
272
- opts.on('--valid-min TIME', ValidTime, 'Renew existing certificate if validity',
161
+ opts.on('--valid-min TIME', ValidTime,
162
+ 'Renew existing certificate if validity',
273
163
  'is lesser than TIME',
274
- "(default: #{@options[:valid_min].to_s})") do |vt|
164
+ "(default: #{@options[:valid_min]})") do |vt|
275
165
  @options[:valid_min] = vt
276
166
  end
277
167
 
@@ -280,12 +170,13 @@ module LetsCert
280
170
  end
281
171
 
282
172
  opts.separator("\nRegistration:")
283
- opts.separator(" Automatically register an account with he ACME CA specified" +
284
- " by --server")
173
+ opts.separator(' Automatically register an account with he ACME CA' \
174
+ ' specified by --server')
285
175
  opts.separator('')
286
176
 
287
177
  opts.on('--account-key-size BITS', Integer,
288
- "Account key size (default: #{@options[:account_key_size]})") do |bits|
178
+ 'Account key size (default: ' \
179
+ "#{@options[:account_key_size]})") do |bits|
289
180
  @options[:account_key_size] = bits
290
181
  end
291
182
 
@@ -307,11 +198,6 @@ module LetsCert
307
198
  opts.separator(' Configure properties of HTTP requests and responses.')
308
199
  opts.separator('')
309
200
 
310
- opts.on('--user-agent NAME', 'User-Agent sent in all HTTP requests',
311
- "(default: #{@options[:user_agent]})") do |ua|
312
- @options[:user_agent] = ua
313
- end
314
-
315
201
  opts.on('--server URI', 'URI for the CA ACME API endpoint',
316
202
  "(default: #{@options[:server]})") do |uri|
317
203
  @options[:server] = uri
@@ -325,14 +211,8 @@ module LetsCert
325
211
  # Check all components are covered by plugins
326
212
  # @raise [Error]
327
213
  def check_persisted
328
- persisted = IOPlugin.empty_data
329
-
330
- @options[:files].each do |file|
331
- persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
332
- oldv || newv
333
- end
334
- end
335
- not_persisted = persisted.keys.find_all { |k| !persisted[k] }
214
+ persisted = persisted_data
215
+ not_persisted = persisted.keys.find_all { |k| persisted[k].nil? }
336
216
 
337
217
  unless not_persisted.empty?
338
218
  raise Error, 'Selected IO plugins do not cover following components: ' +
@@ -340,9 +220,90 @@ module LetsCert
340
220
  end
341
221
  end
342
222
 
343
-
344
223
  private
345
224
 
225
+ # Print help and exit, if +:print_help+ option is set
226
+ # @return [void]
227
+ # rubocop:disable Style/GuardClause
228
+ def print_help_if_needed
229
+ if @options[:print_help]
230
+ puts @opt_parser
231
+ exit RETURN_OK
232
+ end
233
+ end
234
+
235
+ # Show version and exit, if +:show_version+ option is set
236
+ # @return [void]
237
+ def show_version_if_needed
238
+ if @options[:show_version]
239
+ puts "letscert #{LetsCert::VERSION}"
240
+ puts 'Copyright (c) 2016 Sylvain Daubert'
241
+ puts 'License MIT: see http://opensource.org/licenses/MIT'
242
+ exit RETURN_OK
243
+ end
244
+ end
245
+
246
+ # Set logger level from +:verbose+ option
247
+ # @return [void]
248
+ def set_logger_level
249
+ case @options[:verbose]
250
+ when 0
251
+ @logger.level = Logger::Severity::WARN
252
+ when 1
253
+ @logger.level = Logger::Severity::INFO
254
+ when 2..5
255
+ @logger.level = Logger::Severity::DEBUG
256
+ end
257
+ end
258
+
259
+ # Set logger for IOPlugin and Certificate classes.
260
+ # @return [void]
261
+ def set_logger
262
+ @logger.debug { "options are: #{@options.inspect}" }
263
+ IOPlugin.logger = @logger
264
+ Certificate.logger = @logger
265
+ end
266
+
267
+ # Check at least on domain is given.
268
+ # @return [void]
269
+ # @raise [Error] no domain given
270
+ def check_domains
271
+ if @options[:domains].empty?
272
+ raise Error, 'At leat one domain must be given with --domain ' \
273
+ "option.\nTry 'letscert --help' for more information."
274
+ end
275
+ end
276
+
277
+ # Revoke a certificate
278
+ # @return [Integer] exit status
279
+ def revoke
280
+ data = load_data_from_disk(IOPlugin.registered.keys)
281
+ certificate = Certificate.new(data[:cert])
282
+ if certificate.revoke(data[:account_key], @options)
283
+ RETURN_OK
284
+ else
285
+ RETURN_ERROR
286
+ end
287
+ end
288
+
289
+ # Create/update a certificate
290
+ # @return [Integer] exit status
291
+ # rubocop:disable Style/AccessorMethodName
292
+ def get_certificate
293
+ data = load_data_from_disk(@options[:files])
294
+
295
+ certificate = Certificate.new(data[:cert])
296
+ min_time = @options[:valid_min].to_seconds
297
+ if certificate.valid?(@options[:domains], min_time)
298
+ @logger.info { 'no need to update cert' }
299
+ RETURN_OK
300
+ else
301
+ # update/create cert
302
+ certificate.get data[:account_key], data[:key], @options
303
+ RETURN_OK_CERT
304
+ end
305
+ end
306
+
346
307
  # Load existing data from disk
347
308
  # @param [Array<String>] files
348
309
  # @return [Hash]
@@ -358,9 +319,9 @@ module LetsCert
358
319
  end
359
320
  raise Error unless test
360
321
 
361
- # Merge data into all_data. New value replace old one only if old one was
362
- # not defined
363
- all_data.merge!(data) do |key, oldval, newval|
322
+ # Merge data into all_data. New value replace old one only if old
323
+ # one was not defined
324
+ all_data.merge!(data) do |_key, oldval, newval|
364
325
  oldval || newval
365
326
  end
366
327
  end
@@ -387,6 +348,18 @@ module LetsCert
387
348
  @options[:roots] = roots
388
349
  end
389
350
 
351
+ def persisted_data
352
+ persisted = IOPlugin.empty_data
353
+ @options[:files].each do |file|
354
+ ioplugin = IOPlugin.registered[file]
355
+ next if ioplugin.nil?
356
+ persisted.merge!(ioplugin.persisted) do |_k, oldv, newv|
357
+ oldv || newv
358
+ end
359
+ end
360
+ persisted
361
+ end
362
+
390
363
  end
391
364
 
392
365
  end