letscert 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +21 -4
- data/bin/letscert +22 -0
- data/lib/letscert/certificate.rb +209 -0
- data/lib/letscert/certificate.rb~ +2 -8
- data/lib/letscert/io_plugin.rb +80 -13
- data/lib/letscert/loggable.rb +72 -0
- data/lib/letscert/loggable.rb~ +24 -0
- data/lib/letscert/runner.rb +46 -163
- data/lib/letscert.rb +23 -1
- data/tasks/gem.rake +2 -2
- data/tasks/yard.rake +1 -1
- metadata +9 -8
- data/lib/letscert/io_plugin.rb~ +0 -9
- data/lib/letscert/runner.rb~ +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7794f56c134e1437cbb2d89cf9bcf3b2da6f5bc1
|
4
|
+
data.tar.gz: 735bb555113c95eb110500c3fcd43b435c7e41aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d1369f91acfe830d02461a442eeac66d14995944996dac69c1d6120c42077566701eb0aca6bdc34ec9a0a17e82d2765a315086ba5ad0d97b7c58772701e0102
|
7
|
+
data.tar.gz: 5cc07fe0b35bab4b652ac29a1807ae39b62cae562fcbb73dd8fc9320f2bbef3be338471094baca01bdc41c9309fb6c8b375080b01013c91538467ecaddbae05b
|
data/README.md
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
# letscert
|
2
2
|
A simple `Let's Encrypt` client in ruby.
|
3
3
|
|
4
|
-
I think `simp_le` do it the right way: it is simple, it is safe as it does not
|
5
|
-
but it is Python (no one is perfect :-)) So I started to create a clone, but
|
6
|
-
|
7
|
-
Work in progress.
|
4
|
+
I think `simp_le` do it the right way: it is simple, it is safe as it does not need to be
|
5
|
+
run as root, but it is Python (no one is perfect :-)) So I started to create a clone, but
|
6
|
+
in Ruby.
|
8
7
|
|
9
8
|
# Usage
|
10
9
|
|
@@ -14,3 +13,21 @@ letscert -d example.com:/var/www/example.com/html -f key.pem -f cert.pem -f full
|
|
14
13
|
```
|
15
14
|
|
16
15
|
The command is the same for certificate renewal.
|
16
|
+
|
17
|
+
# What `letscert` do
|
18
|
+
|
19
|
+
* Automagically a new ACME account if needed.
|
20
|
+
* Issue new certificate if no previous one found.
|
21
|
+
* Renew certificate only if needed.
|
22
|
+
* Only `http-01` challenge supported. An existing web server must be alreay running. `letscert` should have write access to `${webroot}/.well-known/acme-challenge`.
|
23
|
+
* Crontab friendly: no promts.
|
24
|
+
* No configuration file.
|
25
|
+
* Support multiple domains with multiple roots. Always create a single certificate per un
|
26
|
+
(ie a certificate may have multiple SANs).
|
27
|
+
* As `simp_le`, check the exit code to known if a renewal has happened:
|
28
|
+
* 0 if certificate data was created or updated;
|
29
|
+
* 1 if renewal not necessary;
|
30
|
+
* 2 in case of errors.
|
31
|
+
|
32
|
+
# Todo
|
33
|
+
Add support to revocation.
|
data/bin/letscert
CHANGED
@@ -1,4 +1,26 @@
|
|
1
1
|
#!/usr/bin/ruby
|
2
|
+
#
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2016 Sylvain Daubert
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
12
|
+
# furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included in all
|
15
|
+
# copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
# SOFTWARE.
|
2
24
|
|
3
25
|
require 'acme-client'
|
4
26
|
require 'letscert'
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2016 Sylvain Daubert
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
require_relative 'loggable'
|
23
|
+
|
24
|
+
module LetsCert
|
25
|
+
|
26
|
+
# Class to handle ACME operations on certificates
|
27
|
+
class Certificate
|
28
|
+
include Loggable
|
29
|
+
|
30
|
+
# Revoke certificates
|
31
|
+
# @param [Array<String>] files
|
32
|
+
def self.revoke(files)
|
33
|
+
logger.warn "revoke not yet implemented"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get a new certificate, or renew an existing one
|
37
|
+
# @param [Hash] options
|
38
|
+
# @param [Hash] data
|
39
|
+
def self.get(options, data)
|
40
|
+
new.get options, data
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(options, data)
|
44
|
+
logger.info {"create key/cert/chain..." }
|
45
|
+
roots = compute_roots(options)
|
46
|
+
logger.debug { "webroots are: #{roots.inspect}" }
|
47
|
+
|
48
|
+
client = get_acme_client(data[:account_key], options)
|
49
|
+
|
50
|
+
do_challenges client, roots
|
51
|
+
|
52
|
+
if options[:reuse_key] and !data[:key].nil?
|
53
|
+
logger.info { 'Reuse existing private key' }
|
54
|
+
key = data[:key]
|
55
|
+
else
|
56
|
+
logger.info { 'Generate new private key' }
|
57
|
+
key = OpenSSL::PKey::RSA.generate(@options[:cert_key_size])
|
58
|
+
end
|
59
|
+
|
60
|
+
csr = Acme::Client::CertificateRequest.new(names: roots.keys, private_key: key)
|
61
|
+
cert = client.new_certificate(csr)
|
62
|
+
|
63
|
+
IOPlugin.registered.each do |name, plugin|
|
64
|
+
plugin.save( account_key: client.private_key, key: key, cert: cert.x509,
|
65
|
+
chain: cert.x509_chain)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Compute webroots
|
73
|
+
# @return [Hash] whre key are domains and value are their webroot path
|
74
|
+
def compute_roots(options)
|
75
|
+
roots = {}
|
76
|
+
no_roots = []
|
77
|
+
|
78
|
+
options[:domains].each do |domain|
|
79
|
+
match = domain.match(/([\w+\.]+):(.*)/)
|
80
|
+
if match
|
81
|
+
roots[match[1]] = match[2]
|
82
|
+
elsif options[:default_root]
|
83
|
+
roots[domain] = options[:default_root]
|
84
|
+
else
|
85
|
+
no_roots << domain
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if !no_roots.empty?
|
90
|
+
raise Error, 'root for the following domain(s) are not specified: ' +
|
91
|
+
no_roots.join(', ') + ".\nTry --default_root or use " +
|
92
|
+
'-d example.com:/var/www/html syntax.'
|
93
|
+
end
|
94
|
+
|
95
|
+
roots
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get ACME client.
|
99
|
+
#
|
100
|
+
# Client is only created on first call, then it is cached.
|
101
|
+
# @param [Hash] account_key
|
102
|
+
# @param [Hash] options
|
103
|
+
def get_acme_client(account_key, options)
|
104
|
+
return @client if @client
|
105
|
+
|
106
|
+
key = get_account_key(account_key, options[:account_key_size])
|
107
|
+
|
108
|
+
logger.debug { "connect to #{options[:server]}" }
|
109
|
+
@client = Acme::Client.new(private_key: key, endpoint: options[:server])
|
110
|
+
|
111
|
+
if options[:email].nil?
|
112
|
+
logger.warn { '--email was not provided. ACME CA will have no way to ' +
|
113
|
+
'contact you!' }
|
114
|
+
end
|
115
|
+
|
116
|
+
begin
|
117
|
+
logger.debug { "register with #{options[:email]}" }
|
118
|
+
registration = @client.register(contact: "mailto:#{options[:email]}")
|
119
|
+
rescue Acme::Client::Error::Malformed => ex
|
120
|
+
if ex.message != 'Registration key is already in use'
|
121
|
+
raise
|
122
|
+
end
|
123
|
+
else
|
124
|
+
# Requesting ToS make acme-client throw an exception: Connection reset by peer
|
125
|
+
# (Faraday::ConnectionFailed). To investigate...
|
126
|
+
#if registration.term_of_service_uri
|
127
|
+
# @logger.debug { "get terms of service" }
|
128
|
+
# terms = registration.get_terms
|
129
|
+
# if !terms.nil?
|
130
|
+
# tos_digest = OpenSSL::Digest::SHA256.digest(terms)
|
131
|
+
# if tos_digest != @options[:tos_sha256]
|
132
|
+
# raise Error, 'Terms Of Service mismatch'
|
133
|
+
# end
|
134
|
+
|
135
|
+
@logger.debug { "agree terms of service" }
|
136
|
+
registration.agree_terms
|
137
|
+
# end
|
138
|
+
#end
|
139
|
+
end
|
140
|
+
|
141
|
+
@client
|
142
|
+
end
|
143
|
+
|
144
|
+
# Generate a new account key if no one is given in +data+
|
145
|
+
# @param [OpenSSL::PKey,nil] key
|
146
|
+
# @param [Hash] options
|
147
|
+
def get_account_key(key, key_size)
|
148
|
+
if key.nil?
|
149
|
+
logger.info { 'No account key. Generate a new one...' }
|
150
|
+
OpenSSL::PKey::RSA.new(key_size)
|
151
|
+
else
|
152
|
+
key
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Do ACME challenges for each requested domain.
|
157
|
+
# @param [Acme::Client] client
|
158
|
+
# @param [Hash] roots
|
159
|
+
def do_challenges(client, roots)
|
160
|
+
logger.debug { 'Get authorization for all domains' }
|
161
|
+
challenges = {}
|
162
|
+
|
163
|
+
roots.keys.each do |domain|
|
164
|
+
authorization = client.authorize(domain: domain)
|
165
|
+
if authorization
|
166
|
+
challenges[domain] = authorization.http01
|
167
|
+
else
|
168
|
+
challenges[domain] = nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
logger.debug { 'Check all challenges are HTTP-01' }
|
173
|
+
if challenges.values.any? { |chall| chall.nil? }
|
174
|
+
raise Error, 'CA did not offer http-01-only challenge. ' +
|
175
|
+
'This client is unable to solve any other challenges.'
|
176
|
+
end
|
177
|
+
|
178
|
+
challenges.each do |domain, challenge|
|
179
|
+
begin
|
180
|
+
FileUtils.mkdir_p(File.join(roots[domain], File.dirname(challenge.filename)))
|
181
|
+
rescue SystemCallError => ex
|
182
|
+
raise Error, ex.message
|
183
|
+
end
|
184
|
+
|
185
|
+
path = File.join(roots[domain], challenge.filename)
|
186
|
+
logger.debug { "Save validation #{challenge.file_content} to #{path}" }
|
187
|
+
File.write path, challenge.file_content
|
188
|
+
|
189
|
+
challenge.request_verification
|
190
|
+
|
191
|
+
status = 'pending'
|
192
|
+
while(status == 'pending') do
|
193
|
+
sleep(1)
|
194
|
+
status = challenge.verify_status
|
195
|
+
end
|
196
|
+
|
197
|
+
if status != 'valid'
|
198
|
+
logger.warn { "#{domain} was not successfully verified!" }
|
199
|
+
else
|
200
|
+
logger.info { "#{domain} was successfully verified." }
|
201
|
+
end
|
202
|
+
|
203
|
+
File.unlink path
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
@@ -1,14 +1,8 @@
|
|
1
|
+
|
1
2
|
module LetsCert
|
2
3
|
|
3
|
-
# Class to handle certificates
|
4
|
+
# Class to handle ACME operations on certificates
|
4
5
|
class Certificate
|
5
|
-
|
6
|
-
# Load all certificates passed as arguments
|
7
|
-
# @param [Array<String>] files
|
8
|
-
# @return [Array<Certificate>]
|
9
|
-
def self.load(*files)
|
10
|
-
end
|
11
|
-
|
12
6
|
end
|
13
7
|
|
14
8
|
end
|
data/lib/letscert/io_plugin.rb
CHANGED
@@ -1,10 +1,34 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2016 Sylvain Daubert
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
1
22
|
require 'json'
|
2
23
|
require 'base64'
|
24
|
+
require_relative 'loggable'
|
3
25
|
|
4
26
|
module LetsCert
|
5
27
|
|
6
28
|
# Input/output plugin
|
29
|
+
# @author Sylvain Daubert
|
7
30
|
class IOPlugin
|
31
|
+
include Loggable
|
8
32
|
|
9
33
|
# Plugin name
|
10
34
|
# @return [String]
|
@@ -15,18 +39,23 @@ module LetsCert
|
|
15
39
|
%w(fullchain.pem key.der key.pem)
|
16
40
|
|
17
41
|
|
42
|
+
# Registered plugins
|
18
43
|
@@registered = {}
|
19
44
|
|
20
45
|
# Get empty data
|
46
|
+
# @return [Hash] +{ account_key: nil, key: nil, cert: nil, chain: nil }+
|
21
47
|
def self.empty_data
|
22
48
|
{ account_key: nil, key: nil, cert: nil, chain: nil }
|
23
49
|
end
|
24
50
|
|
25
51
|
# Register a plugin
|
52
|
+
# @param [Class] klass
|
53
|
+
# @param [Array] args args to pass to +klass+ constructor
|
54
|
+
# @return [IOPlugin]
|
26
55
|
def self.register(klass, *args)
|
27
56
|
plugin = klass.new(*args)
|
28
57
|
if plugin.name =~ /[\/\\]/ or ['.', '..'].include?(plugin.name)
|
29
|
-
raise Error, "plugin name should just
|
58
|
+
raise Error, "plugin name should just be a file name, without path"
|
30
59
|
end
|
31
60
|
|
32
61
|
@@registered[plugin.name] = plugin
|
@@ -35,26 +64,16 @@ module LetsCert
|
|
35
64
|
end
|
36
65
|
|
37
66
|
# Get registered plugins
|
67
|
+
# @return [Hash] keys are filenames and keys are instances of IOPlugin subclasses.
|
38
68
|
def self.registered
|
39
69
|
@@registered
|
40
70
|
end
|
41
71
|
|
42
|
-
# Set logger
|
43
|
-
def self.logger=(logger)
|
44
|
-
@@logger = logger
|
45
|
-
end
|
46
|
-
|
47
72
|
# @param [String] name
|
48
73
|
def initialize(name)
|
49
74
|
@name = name
|
50
75
|
end
|
51
76
|
|
52
|
-
# Get logger instance
|
53
|
-
# @return [Logger]
|
54
|
-
def logger
|
55
|
-
@logger ||= self.class.class_variable_get(:@@logger)
|
56
|
-
end
|
57
|
-
|
58
77
|
# @abstract This method must be overriden in subclasses
|
59
78
|
def load
|
60
79
|
raise NotImplementedError
|
@@ -69,6 +88,7 @@ module LetsCert
|
|
69
88
|
|
70
89
|
|
71
90
|
# Mixin for IOPmugin subclasses that handle files
|
91
|
+
# @author Sylvain Daubert
|
72
92
|
module FileIOPluginMixin
|
73
93
|
|
74
94
|
# Load data from file named {#name}
|
@@ -116,6 +136,7 @@ module LetsCert
|
|
116
136
|
|
117
137
|
|
118
138
|
# Mixin for IOPlugin subclasses that handle JWK
|
139
|
+
# @author Sylvain Daubert
|
119
140
|
module JWKIOPluginMixin
|
120
141
|
|
121
142
|
# Load crypto data from JSON-encoded file
|
@@ -166,18 +187,24 @@ module LetsCert
|
|
166
187
|
|
167
188
|
|
168
189
|
# Account key IO plugin
|
190
|
+
# @author Sylvain Daubert
|
169
191
|
class AccountKey < IOPlugin
|
170
192
|
include FileIOPluginMixin
|
171
193
|
include JWKIOPluginMixin
|
172
194
|
|
195
|
+
# @return [Hash] always get +true+ for +:account_key+ key
|
173
196
|
def persisted
|
174
197
|
{ account_key: true }
|
175
198
|
end
|
176
199
|
|
200
|
+
# @return [Hash]
|
177
201
|
def load_from_content(content)
|
178
202
|
{ account_key: load_jwk(content) }
|
179
203
|
end
|
180
204
|
|
205
|
+
# Save account key.
|
206
|
+
# @param [Hash] data
|
207
|
+
# @return [void]
|
181
208
|
def save(data)
|
182
209
|
save_to_file(dump_jwk(data[:account_key]))
|
183
210
|
end
|
@@ -187,15 +214,18 @@ module LetsCert
|
|
187
214
|
|
188
215
|
|
189
216
|
# OpenSSL IOPlugin
|
217
|
+
# @author Sylvain Daubert
|
190
218
|
class OpenSSLIOPlugin < IOPlugin
|
191
219
|
|
192
|
-
# @private
|
220
|
+
# @private Regular expression to discriminate PEM
|
193
221
|
PEM_RE = /
|
194
222
|
^-----BEGIN ((?:[\x21-\x2c\x2e-\x7e](?:[- ]?[\x21-\x2c\x2e-\x7e])*)?)\s*-----$
|
195
223
|
.*?
|
196
224
|
^-----END \1-----\s*
|
197
225
|
/x
|
198
226
|
|
227
|
+
# @param [String] name filename
|
228
|
+
# @param [:pem,:der] type
|
199
229
|
def initialize(name, type)
|
200
230
|
case type
|
201
231
|
when :pem
|
@@ -208,10 +238,16 @@ module LetsCert
|
|
208
238
|
super(name)
|
209
239
|
end
|
210
240
|
|
241
|
+
# Load key from raw +data+
|
242
|
+
# @param [String] data
|
243
|
+
# @return [OpenSSL::PKey]
|
211
244
|
def load_key(data)
|
212
245
|
OpenSSL::PKey::RSA.new data
|
213
246
|
end
|
214
247
|
|
248
|
+
# Dump key/cert data
|
249
|
+
# @param [OpenSSL::PKey] key
|
250
|
+
# @return [String]
|
215
251
|
def dump_key(key)
|
216
252
|
case @type
|
217
253
|
when :pem
|
@@ -222,6 +258,9 @@ module LetsCert
|
|
222
258
|
end
|
223
259
|
alias :dump_cert :dump_key
|
224
260
|
|
261
|
+
# Load certificate from raw +data+
|
262
|
+
# @param [String] data
|
263
|
+
# @return [OpenSSL::X509::Certificate]
|
225
264
|
def load_cert(data)
|
226
265
|
OpenSSL::X509::Certificate.new data
|
227
266
|
end
|
@@ -229,6 +268,9 @@ module LetsCert
|
|
229
268
|
|
230
269
|
private
|
231
270
|
|
271
|
+
# Split concatenated PEMs.
|
272
|
+
# @param [String] data
|
273
|
+
# @yield [String] pem
|
232
274
|
def split_pems(data)
|
233
275
|
m = data.match(PEM_RE)
|
234
276
|
while (m) do
|
@@ -241,17 +283,23 @@ module LetsCert
|
|
241
283
|
|
242
284
|
|
243
285
|
# Key file plugin
|
286
|
+
# @author Sylvain Daubert
|
244
287
|
class KeyFile < OpenSSLIOPlugin
|
245
288
|
include FileIOPluginMixin
|
246
289
|
|
290
|
+
# @return [Hash] always get +true+ for +:key+ key
|
247
291
|
def persisted
|
248
292
|
@persisted ||= { key: true }
|
249
293
|
end
|
250
294
|
|
295
|
+
# @return [Hash]
|
251
296
|
def load_from_content(content)
|
252
297
|
{ key: load_key(content) }
|
253
298
|
end
|
254
299
|
|
300
|
+
# Save private key.
|
301
|
+
# @param [Hash] data
|
302
|
+
# @return [void]
|
255
303
|
def save(data)
|
256
304
|
save_to_file(dump_key(data[:key]))
|
257
305
|
end
|
@@ -262,13 +310,16 @@ module LetsCert
|
|
262
310
|
|
263
311
|
|
264
312
|
# Chain file plugin
|
313
|
+
# @author Sylvain Daubert
|
265
314
|
class ChainFile < OpenSSLIOPlugin
|
266
315
|
include FileIOPluginMixin
|
267
316
|
|
317
|
+
# @return [Hash] always get +true+ for +:chain+ key
|
268
318
|
def persisted
|
269
319
|
@persisted ||= { chain: true }
|
270
320
|
end
|
271
321
|
|
322
|
+
# @return [Hash]
|
272
323
|
def load_from_content(content)
|
273
324
|
chain = []
|
274
325
|
split_pems(content) do |pem|
|
@@ -277,6 +328,9 @@ module LetsCert
|
|
277
328
|
{ chain: chain }
|
278
329
|
end
|
279
330
|
|
331
|
+
# Save chain.
|
332
|
+
# @param [Hash] data
|
333
|
+
# @return [void]
|
280
334
|
def save(data)
|
281
335
|
save_to_file(data[:chain].map { |c| dump_cert(c) }.join)
|
282
336
|
end
|
@@ -286,12 +340,16 @@ module LetsCert
|
|
286
340
|
|
287
341
|
|
288
342
|
# Fullchain file plugin
|
343
|
+
# @author Sylvain Daubert
|
289
344
|
class FullChainFile < ChainFile
|
290
345
|
|
346
|
+
# @return [Hash] always get +true+ for +:cert+ and +:chain+ keys
|
291
347
|
def persisted
|
292
348
|
@persisted ||= { cert: true, chain: true }
|
293
349
|
end
|
294
350
|
|
351
|
+
# Load full certificate chain
|
352
|
+
# @return [Hash]
|
295
353
|
def load
|
296
354
|
data = super
|
297
355
|
if data[:chain].nil? or data[:chain].empty?
|
@@ -303,6 +361,9 @@ module LetsCert
|
|
303
361
|
{ account_key: data[:account_key], key: data[:key], cert: cert, chain: chain }
|
304
362
|
end
|
305
363
|
|
364
|
+
# Save fullchain.
|
365
|
+
# @param [Hash] data
|
366
|
+
# @return [void]
|
306
367
|
def save(data)
|
307
368
|
super(account_key: data[:account_key], key: data[:key], cert: nil,
|
308
369
|
chain: [data[:cert]] + data[:chain])
|
@@ -313,17 +374,23 @@ module LetsCert
|
|
313
374
|
|
314
375
|
|
315
376
|
# Cert file plugin
|
377
|
+
# @author Sylvain Daubert
|
316
378
|
class CertFile < OpenSSLIOPlugin
|
317
379
|
include FileIOPluginMixin
|
318
380
|
|
381
|
+
# @return [Hash] always get +true+ for +:cert+ key
|
319
382
|
def persisted
|
320
383
|
@persisted ||= { cert: true }
|
321
384
|
end
|
322
385
|
|
386
|
+
# @return [Hash]
|
323
387
|
def load_from_content(content)
|
324
388
|
{ cert: load_cert(content) }
|
325
389
|
end
|
326
390
|
|
391
|
+
# Save certificate.
|
392
|
+
# @param [Hash] data
|
393
|
+
# @return [void]
|
327
394
|
def save(data)
|
328
395
|
save_to_file(dump_cert(data[:cert]))
|
329
396
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2016 Sylvain Daubert
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
# Namespace for all letcert's classes.
|
24
|
+
# @author Sylvain Daubert
|
25
|
+
module LetsCert
|
26
|
+
|
27
|
+
# Mixin module to add loggability to a class.
|
28
|
+
# @author Sylvain Daubert
|
29
|
+
module Loggable
|
30
|
+
|
31
|
+
# Hook called when {Loggable} is included in a class or a module.
|
32
|
+
# This hook adds methods from {ClassMethods} as class methods to +mod+.
|
33
|
+
# @param [Module] mod
|
34
|
+
# @return [void]
|
35
|
+
def self.included(mod)
|
36
|
+
mod.extend(ClassMethods)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Class methods from {Loggable} module to include in target classes.
|
40
|
+
# @author Sylvain Daubert
|
41
|
+
module ClassMethods
|
42
|
+
|
43
|
+
# @private hook called a subclass is created.
|
44
|
+
# Take care of all subclasses to later properly set @logger class instance variable.
|
45
|
+
# @param [Class] subclass
|
46
|
+
# @return [void]
|
47
|
+
def inherited(subclass)
|
48
|
+
@@subclasses ||= []
|
49
|
+
@@subclasses << subclass
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set logger
|
53
|
+
# @param [Logger] logger
|
54
|
+
# @return [void]
|
55
|
+
def logger=(logger)
|
56
|
+
@logger = logger
|
57
|
+
@@subclasses.each do |subclass|
|
58
|
+
subclass.instance_variable_set(:@logger, logger)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get logger instance
|
65
|
+
# @return [Logger]
|
66
|
+
def logger
|
67
|
+
@logger ||= self.class.instance_variable_get(:@logger)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
module Loggable
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
# Set logger
|
8
|
+
# @param [Logger] logger
|
9
|
+
def self.logger=(logger)
|
10
|
+
@@logger = logger
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# Get logger instance
|
17
|
+
# @return [Logger]
|
18
|
+
def logger
|
19
|
+
@logger ||= self.class.class_variable_get(:@@logger)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/letscert/runner.rb
CHANGED
@@ -1,17 +1,41 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2016 Sylvain Daubert
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
1
22
|
require 'optparse'
|
2
23
|
require 'logger'
|
3
24
|
require 'fileutils'
|
4
25
|
|
5
26
|
require_relative 'io_plugin'
|
27
|
+
require_relative 'certificate'
|
6
28
|
|
7
29
|
module LetsCert
|
8
30
|
|
31
|
+
# Runner class: analyse and execute CLI commands.
|
32
|
+
# @author Sylvain Daubert
|
9
33
|
class Runner
|
10
34
|
|
11
35
|
# Custom logger formatter
|
12
36
|
class LoggerFormatter < Logger::Formatter
|
13
37
|
|
14
|
-
# @private
|
38
|
+
# @private log format
|
15
39
|
FORMAT = "[%s] %5s: %s\n"
|
16
40
|
|
17
41
|
# @param [String] severity
|
@@ -26,8 +50,11 @@ module LetsCert
|
|
26
50
|
|
27
51
|
private
|
28
52
|
|
53
|
+
# @private simple datetime formatter
|
54
|
+
# @param [DateTime] time
|
55
|
+
# @return [String]
|
29
56
|
def format_datetime(time)
|
30
|
-
time.strftime("%Y-%
|
57
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
31
58
|
end
|
32
59
|
|
33
60
|
end
|
@@ -100,13 +127,15 @@ module LetsCert
|
|
100
127
|
@logger.debug { "options are: #{@options.inspect}" }
|
101
128
|
|
102
129
|
IOPlugin.logger = @logger
|
130
|
+
Certificate.logger = @logger
|
103
131
|
|
104
132
|
begin
|
105
133
|
if @options[:revoke]
|
106
|
-
revoke
|
134
|
+
Certificate.revoke @options[:files]
|
107
135
|
RETURN_OK
|
108
136
|
elsif @options[:domains].empty?
|
109
|
-
raise Error,
|
137
|
+
raise Error, "At leat one domain must be given with --domain option.\n" +
|
138
|
+
"Try 'letscert --help' for more information."
|
110
139
|
else
|
111
140
|
# Check all components are covered by plugins
|
112
141
|
persisted = IOPlugin.empty_data
|
@@ -128,7 +157,7 @@ module LetsCert
|
|
128
157
|
RETURN_OK
|
129
158
|
else
|
130
159
|
# update/create cert
|
131
|
-
|
160
|
+
Certificate.get @options, data
|
132
161
|
RETURN_OK_CERT
|
133
162
|
end
|
134
163
|
end
|
@@ -141,6 +170,9 @@ module LetsCert
|
|
141
170
|
end
|
142
171
|
|
143
172
|
|
173
|
+
# Parse line command options
|
174
|
+
# @raise [OptionParser::InvalidOption] on unrecognized or malformed option
|
175
|
+
# @return [void]
|
144
176
|
def parse_options
|
145
177
|
@opt_parser = OptionParser.new do |opts|
|
146
178
|
opts.banner = "Usage: lestcert [options]"
|
@@ -245,72 +277,12 @@ module LetsCert
|
|
245
277
|
@opt_parser.parse!
|
246
278
|
end
|
247
279
|
|
248
|
-
def revoke
|
249
|
-
@logger.info { "load certificates: #{@options[:files].join(', ')}" }
|
250
|
-
if @options[:files].empty?
|
251
|
-
raise Error, 'no certificate to revoke. Pass at least one with '+
|
252
|
-
' -f option.'
|
253
|
-
end
|
254
|
-
|
255
|
-
# Temp
|
256
|
-
@logger.warn "Not yet implemented"
|
257
|
-
end
|
258
|
-
|
259
280
|
|
260
281
|
private
|
261
282
|
|
262
|
-
def get_account_key(data)
|
263
|
-
if data.nil?
|
264
|
-
logger.info { 'No account key. Generate a new one...' }
|
265
|
-
OpenSSL::PKey::RSA.new(@options[:account_key_size])
|
266
|
-
else
|
267
|
-
data
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
# Get ACME client.
|
272
|
-
#
|
273
|
-
# Client is only created on first call, then it is cached.
|
274
|
-
def get_acme_client(account_key)
|
275
|
-
return @client if @client
|
276
|
-
|
277
|
-
key = get_account_key(account_key)
|
278
|
-
|
279
|
-
@logger.debug { "connect to #{@options[:server]}" }
|
280
|
-
@client = Acme::Client.new(private_key: key, endpoint: @options[:server])
|
281
|
-
|
282
|
-
if @options[:email].nil?
|
283
|
-
@logger.warn { '--email was not provided. ACME CA will have no way to ' +
|
284
|
-
'contact you!' }
|
285
|
-
end
|
286
|
-
|
287
|
-
begin
|
288
|
-
@logger.debug { "register with #{@options[:email]}" }
|
289
|
-
registration = @client.register(contact: "mailto:#{@options[:email]}")
|
290
|
-
rescue Acme::Client::Error::Malformed => ex
|
291
|
-
if ex.message != 'Registration key is already in use'
|
292
|
-
raise
|
293
|
-
end
|
294
|
-
else
|
295
|
-
#if registration.term_of_service_uri
|
296
|
-
# @logger.debug { "get terms of service" }
|
297
|
-
# terms = registration.get_terms
|
298
|
-
# if !terms.nil?
|
299
|
-
# tos_digest = OpenSSL::Digest::SHA256.digest(terms)
|
300
|
-
# if tos_digest != @options[:tos_sha256]
|
301
|
-
# raise Error, 'Terms Of Service mismatch'
|
302
|
-
# end
|
303
|
-
|
304
|
-
@logger.debug { "agree terms of service" }
|
305
|
-
registration.agree_terms
|
306
|
-
# end
|
307
|
-
#end
|
308
|
-
end
|
309
|
-
|
310
|
-
@client
|
311
|
-
end
|
312
|
-
|
313
283
|
# Load existing data from disk
|
284
|
+
# @param [Array<String>] files
|
285
|
+
# @return [Hash]
|
314
286
|
def load_data_from_disk(files)
|
315
287
|
all_data = IOPlugin.empty_data
|
316
288
|
|
@@ -335,6 +307,10 @@ module LetsCert
|
|
335
307
|
|
336
308
|
# Check if +cert+ exists and is always valid
|
337
309
|
# @todo For now, only check exitence.
|
310
|
+
# @param [nil, OpenSSL::X509::Certificate] cert certificate to valid
|
311
|
+
# @param [Array<String>] domains list if domains to valid
|
312
|
+
# @param [Number] valid_min minimum validity in seconds to ensure
|
313
|
+
# @return [Boolean]
|
338
314
|
def valid_existing_cert(cert, domains, valid_min)
|
339
315
|
if cert.nil?
|
340
316
|
@logger.debug { 'no existing cert' }
|
@@ -359,6 +335,9 @@ module LetsCert
|
|
359
335
|
end
|
360
336
|
|
361
337
|
# Check if a renewal is necessary for +cert+
|
338
|
+
# @param [OpenSSL::X509::Certificate] cert
|
339
|
+
# @param [Number] valid_min minimum validity in seconds to ensure
|
340
|
+
# @return [Boolean]
|
362
341
|
def renewal_necessary?(cert, valid_min)
|
363
342
|
now = Time.now.utc
|
364
343
|
diff = (cert.not_after - now).to_i
|
@@ -368,102 +347,6 @@ module LetsCert
|
|
368
347
|
diff < valid_min
|
369
348
|
end
|
370
349
|
|
371
|
-
# Create/renew key/cert/chain
|
372
|
-
def new_data(data)
|
373
|
-
@logger.info {"create key/cert/chain..." }
|
374
|
-
roots = compute_roots
|
375
|
-
@logger.debug { "webroots are: #{roots.inspect}" }
|
376
|
-
|
377
|
-
client = get_acme_client(data[:account_key])
|
378
|
-
|
379
|
-
@logger.debug { 'Get authorization for all domains' }
|
380
|
-
challenges = {}
|
381
|
-
roots.keys.each do |domain|
|
382
|
-
authorization = client.authorize(domain: domain)
|
383
|
-
if authorization
|
384
|
-
challenges[domain] = authorization.http01
|
385
|
-
else
|
386
|
-
challenges[domain] = nil
|
387
|
-
end
|
388
|
-
end
|
389
|
-
|
390
|
-
@logger.debug { 'Check all challenges are HTTP-01' }
|
391
|
-
if challenges.values.any? { |chall| chall.nil? }
|
392
|
-
raise Error, 'CA did not offer http-01-only challenge. ' +
|
393
|
-
'This client is unable to solve any other challenges.'
|
394
|
-
end
|
395
|
-
|
396
|
-
challenges.each do |domain, challenge|
|
397
|
-
begin
|
398
|
-
FileUtils.mkdir_p(File.join(roots[domain], File.dirname(challenge.filename)))
|
399
|
-
rescue SystemCallError => ex
|
400
|
-
raise Error, ex.message
|
401
|
-
end
|
402
|
-
|
403
|
-
path = File.join(roots[domain], challenge.filename)
|
404
|
-
logger.debug { "Save validation #{challenge.file_content} to #{path}" }
|
405
|
-
File.write path, challenge.file_content
|
406
|
-
|
407
|
-
challenge.request_verification
|
408
|
-
|
409
|
-
status = 'pending'
|
410
|
-
while(status == 'pending') do
|
411
|
-
sleep(1)
|
412
|
-
status = challenge.verify_status
|
413
|
-
end
|
414
|
-
|
415
|
-
if status != 'valid'
|
416
|
-
@logger.warn { "#{domain} was not successfully verified!" }
|
417
|
-
else
|
418
|
-
@logger.info { "#{domain} was successfully verified." }
|
419
|
-
end
|
420
|
-
|
421
|
-
File.unlink path
|
422
|
-
end
|
423
|
-
|
424
|
-
if @options[:reuse_key] and !data[:key].nil?
|
425
|
-
@logger.info { 'Reuse existing private key' }
|
426
|
-
key = data[:key]
|
427
|
-
else
|
428
|
-
@logger.info { 'Generate new private key' }
|
429
|
-
key = OpenSSL::PKey::RSA.generate(@options[:cert_key_size])
|
430
|
-
end
|
431
|
-
|
432
|
-
csr = Acme::Client::CertificateRequest.new(names: roots.keys, private_key: key)
|
433
|
-
cert = client.new_certificate(csr)
|
434
|
-
|
435
|
-
IOPlugin.registered.each do |name, plugin|
|
436
|
-
plugin.save( account_key: client.private_key, key: key, cert: cert.x509,
|
437
|
-
chain: cert.x509_chain)
|
438
|
-
end
|
439
|
-
end
|
440
|
-
|
441
|
-
# Compute webroots
|
442
|
-
# @return [Hash] whre key are domains and value are their webroot path
|
443
|
-
def compute_roots
|
444
|
-
roots = {}
|
445
|
-
no_roots = []
|
446
|
-
|
447
|
-
@options[:domains].each do |domain|
|
448
|
-
match = domain.match(/([\w+\.]+):(.*)/)
|
449
|
-
if match
|
450
|
-
roots[match[1]] = match[2]
|
451
|
-
elsif @options[:default_root]
|
452
|
-
roots[domain] = @options[:default_root]
|
453
|
-
else
|
454
|
-
no_roots << domain
|
455
|
-
end
|
456
|
-
end
|
457
|
-
|
458
|
-
if !no_roots.empty?
|
459
|
-
raise Error, 'root for the following domain(s) are not specified: ' +
|
460
|
-
no_roots.join(', ') + ".\nTry --default_root or use " +
|
461
|
-
'-d example.com:/var/www/html syntax.'
|
462
|
-
end
|
463
|
-
|
464
|
-
roots
|
465
|
-
end
|
466
|
-
|
467
350
|
end
|
468
351
|
|
469
352
|
end
|
data/lib/letscert.rb
CHANGED
@@ -1,8 +1,30 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2016 Sylvain Daubert
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
1
23
|
# Namespace for all letcert's classes.
|
2
24
|
module LetsCert
|
3
25
|
|
4
26
|
# Letscert version number
|
5
|
-
VERSION = '0.2.
|
27
|
+
VERSION = '0.2.2'
|
6
28
|
|
7
29
|
|
8
30
|
# Base error class
|
data/tasks/gem.rake
CHANGED
@@ -21,8 +21,8 @@ EOF
|
|
21
21
|
s.files = files
|
22
22
|
s.executables = ['letscert']
|
23
23
|
|
24
|
-
s.add_dependency 'acme-client', '~>0.
|
25
|
-
s.add_dependency 'yard', '~>0.8
|
24
|
+
s.add_dependency 'acme-client', '~>0.3.0'
|
25
|
+
s.add_dependency 'yard', '~>0.8'
|
26
26
|
|
27
27
|
#s.add_development_dependency 'rspec', '~>3.4'
|
28
28
|
end
|
data/tasks/yard.rake
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: letscert
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sylvain Daubert
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acme-client
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.3.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.3.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: yard
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.8
|
33
|
+
version: '0.8'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.8
|
40
|
+
version: '0.8'
|
41
41
|
description: |
|
42
42
|
letscert is a simple Let's Encrypt client written in Ruby. It aims at be as clean as
|
43
43
|
simp_le.
|
@@ -54,11 +54,12 @@ files:
|
|
54
54
|
- bin/letscert~
|
55
55
|
- lib/letscert.rb
|
56
56
|
- lib/letscert.rb~
|
57
|
+
- lib/letscert/certificate.rb
|
57
58
|
- lib/letscert/certificate.rb~
|
58
59
|
- lib/letscert/io_plugin.rb
|
59
|
-
- lib/letscert/
|
60
|
+
- lib/letscert/loggable.rb
|
61
|
+
- lib/letscert/loggable.rb~
|
60
62
|
- lib/letscert/runner.rb
|
61
|
-
- lib/letscert/runner.rb~
|
62
63
|
- tasks/gem.rake
|
63
64
|
- tasks/gem.rake~
|
64
65
|
- tasks/yard.rake
|
data/lib/letscert/io_plugin.rb~
DELETED