letscert 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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