letscert 0.3.0 → 0.3.1
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 +16 -0
- data/lib/letscert/certificate.rb +31 -26
- data/lib/letscert/io_plugin.rb +0 -4
- data/lib/letscert/runner.rb +99 -29
- data/lib/letscert.rb +1 -1
- data/spec/certificate_spec.rb +113 -26
- data/spec/runner_spec.rb +127 -0
- data/spec/runner_spec.rb~ +8 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ecc722b11b425c139e92b66d7436be0b818d0d03
|
4
|
+
data.tar.gz: 10b112ecdefce5e90e0c5cf8f99cd8bef961c2c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 32d5b6a6cfe0fd937506fc60791c793cac8024ab13a04531fc4a4bc56529074131d27f80c642f2d9e68a6026f3081b3d5578c0c2d1343f8a07e7b5acc1eb484c
|
7
|
+
data.tar.gz: a2c004cd721d85ce5aec4afe22325eaf9cfd5f70fd173f029bd443826e7cf1361d341ef9db105a69afb91cf6ceca8da37539bdb32e382a629865612101fccbaf
|
data/README.md
CHANGED
@@ -11,11 +11,14 @@ in Ruby.
|
|
11
11
|
|
12
12
|
## Generate a key pair and get signed certificate:
|
13
13
|
With full chain support (`fullchain.pem` file will contain all certificates):
|
14
|
+
|
14
15
|
```bash
|
15
16
|
letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f fullchain.pem
|
16
17
|
```
|
18
|
+
|
17
19
|
else (certificate for example.com is in `cert.pem` file, rest of certification chain
|
18
20
|
is in `chain.pem`):
|
21
|
+
|
19
22
|
```bash
|
20
23
|
letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f cert.pem -f chain.pem
|
21
24
|
```
|
@@ -25,14 +28,27 @@ Commands are the sames for certificate renewal.
|
|
25
28
|
|
26
29
|
## Generate a key pair and get a signed certificate for multi-domains:
|
27
30
|
Generate a single certificate for `example.com` and `www.example.com`:
|
31
|
+
|
28
32
|
```bash
|
29
33
|
letscert -d example.com -d www.example.com --default-root /var/www/html --email my.name@domain.tld -f account_key.json -f key.pem -f fullchain.pem
|
30
34
|
```
|
31
35
|
|
32
36
|
Command is the same for certificate renewal.
|
33
37
|
|
38
|
+
## Generate a key pair and get a signed certificate if existing one is valid for less than xx days
|
39
|
+
|
40
|
+
In this example, `xx` is 10:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f cert.pem -f chain.pem --valid-min 10d
|
44
|
+
```
|
45
|
+
|
46
|
+
Valid time may also be set as number of hours (`h` suffix), minutes (`m` suffix) or
|
47
|
+
seconds (no suffix).
|
48
|
+
|
34
49
|
## Revoke a key pair:
|
35
50
|
From directory where are stored `account_key.json` and `cert.pem` or `fullchain.pem`:
|
51
|
+
|
36
52
|
```bash
|
37
53
|
letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld --revoke
|
38
54
|
```
|
data/lib/letscert/certificate.rb
CHANGED
@@ -19,6 +19,7 @@
|
|
19
19
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
21
|
# SOFTWARE.
|
22
|
+
require 'acme-client'
|
22
23
|
require_relative 'loggable'
|
23
24
|
|
24
25
|
module LetsCert
|
@@ -28,6 +29,11 @@ module LetsCert
|
|
28
29
|
class Certificate
|
29
30
|
include Loggable
|
30
31
|
|
32
|
+
# @return [OpenSSL::X509::Certificate,nil]
|
33
|
+
attr_reader :cert
|
34
|
+
# @return [Acme::Client,nil]
|
35
|
+
attr_reader :client
|
36
|
+
|
31
37
|
|
32
38
|
# @param [OpenSSL::X509::Certificate,nil] cert
|
33
39
|
def initialize(cert)
|
@@ -35,17 +41,29 @@ module LetsCert
|
|
35
41
|
end
|
36
42
|
|
37
43
|
# Get a new certificate, or renew an existing one
|
38
|
-
# @param [OpenSSL::PKey::PKey] account_key private key to authenticate to ACME
|
39
|
-
#
|
40
|
-
# @param [
|
44
|
+
# @param [OpenSSL::PKey::PKey,nil] account_key private key to authenticate to ACME
|
45
|
+
# server
|
46
|
+
# @param [OpenSSL::PKey::PKey, nil] key private key from which make a certificate.
|
47
|
+
# If +nil+, generate a new one with +options[:cet_key_size]+ bits.
|
48
|
+
# @param [Hash] options option hash
|
49
|
+
# @option options [Fixnum] :account_key_size ACME account private key size in bits
|
50
|
+
# @option options [Fixnum] :cert_key_size private key size used to generate
|
51
|
+
# a certificate
|
52
|
+
# @option options [String] :email e-mail used as ACME account
|
53
|
+
# @option options [Array<String>] :files plugin names to use
|
54
|
+
# @option options [Boolean] :reuse_key reuse private key when getting a new
|
55
|
+
# certificate
|
56
|
+
# @option options [Hash] :roots hash associating domains as keys to web roots as
|
57
|
+
# values
|
58
|
+
# @option options [String] :server ACME servel URL
|
41
59
|
def get(account_key, key, options)
|
42
60
|
logger.info {"create key/cert/chain..." }
|
43
|
-
|
44
|
-
logger.debug { "webroots are: #{roots.inspect}" }
|
61
|
+
check_roots(options[:roots])
|
62
|
+
logger.debug { "webroots are: #{options[:roots].inspect}" }
|
45
63
|
|
46
64
|
client = get_acme_client(account_key, options)
|
47
65
|
|
48
|
-
do_challenges client, roots
|
66
|
+
do_challenges client, options[:roots]
|
49
67
|
|
50
68
|
if options[:reuse_key] and !key.nil?
|
51
69
|
logger.info { 'Reuse existing private key' }
|
@@ -54,7 +72,7 @@ module LetsCert
|
|
54
72
|
key = OpenSSL::PKey::RSA.generate(options[:cert_key_size])
|
55
73
|
end
|
56
74
|
|
57
|
-
csr = Acme::Client::CertificateRequest.new(names: roots.keys,
|
75
|
+
csr = Acme::Client::CertificateRequest.new(names: options[:roots].keys,
|
58
76
|
private_key: key)
|
59
77
|
cert = client.new_certificate(csr)
|
60
78
|
|
@@ -120,30 +138,17 @@ module LetsCert
|
|
120
138
|
|
121
139
|
private
|
122
140
|
|
123
|
-
#
|
124
|
-
# @
|
125
|
-
|
126
|
-
|
127
|
-
no_roots =
|
128
|
-
|
129
|
-
options[:domains].each do |domain|
|
130
|
-
match = domain.match(/([\w+\.]+):(.*)/)
|
131
|
-
if match
|
132
|
-
roots[match[1]] = match[2]
|
133
|
-
elsif options[:default_root]
|
134
|
-
roots[domain] = options[:default_root]
|
135
|
-
else
|
136
|
-
no_roots << domain
|
137
|
-
end
|
138
|
-
end
|
141
|
+
# check webroots.
|
142
|
+
# @param [Hash] roots
|
143
|
+
# @raise [Error] if some domains have no defined root.
|
144
|
+
def check_roots(roots)
|
145
|
+
no_roots = roots.select { |k,v| v.nil? }
|
139
146
|
|
140
147
|
if !no_roots.empty?
|
141
148
|
raise Error, 'root for the following domain(s) are not specified: ' +
|
142
|
-
no_roots.join(', ') + ".\nTry --default_root or use " +
|
149
|
+
no_roots.keys.join(', ') + ".\nTry --default_root or use " +
|
143
150
|
'-d example.com:/var/www/html syntax.'
|
144
151
|
end
|
145
|
-
|
146
|
-
roots
|
147
152
|
end
|
148
153
|
|
149
154
|
# Get ACME client.
|
data/lib/letscert/io_plugin.rb
CHANGED
@@ -33,10 +33,6 @@ module LetsCert
|
|
33
33
|
# @return [String]
|
34
34
|
attr_reader :name
|
35
35
|
|
36
|
-
# Allowed plugin names
|
37
|
-
ALLOWED_PLUGINS = %w(account_key.json cert.der cert.pem chain.pem full.pem) +
|
38
|
-
%w(fullchain.pem key.der key.pem)
|
39
|
-
|
40
36
|
|
41
37
|
# Registered plugins
|
42
38
|
@@registered = {}
|
data/lib/letscert/runner.rb
CHANGED
@@ -31,6 +31,9 @@ module LetsCert
|
|
31
31
|
# Runner class: analyse and execute CLI commands.
|
32
32
|
# @author Sylvain Daubert
|
33
33
|
class Runner
|
34
|
+
# Get options
|
35
|
+
# @return [Hash]
|
36
|
+
attr_reader :options
|
34
37
|
|
35
38
|
# Custom logger formatter
|
36
39
|
class LoggerFormatter < Logger::Formatter
|
@@ -59,6 +62,47 @@ module LetsCert
|
|
59
62
|
|
60
63
|
end
|
61
64
|
|
65
|
+
# Class used to process validation time from String.
|
66
|
+
# @author Sylvain Daubert
|
67
|
+
class ValidTime
|
68
|
+
|
69
|
+
# @param [String] str time string. May be:
|
70
|
+
# * an integer -> time in seconds
|
71
|
+
# * an integer plus a letter:
|
72
|
+
# * 30m: 30 minutes,
|
73
|
+
# * 30h: 30 hours,
|
74
|
+
# * 30d: 30 days.
|
75
|
+
def initialize(str)
|
76
|
+
m = str.match(/^(\d+)([mhd])?$/)
|
77
|
+
if m
|
78
|
+
case m[2]
|
79
|
+
when nil
|
80
|
+
@seconds = m[1].to_i
|
81
|
+
when 'm'
|
82
|
+
@seconds = m[1].to_i * 60
|
83
|
+
when 'h'
|
84
|
+
@seconds = m[1].to_i * 60 * 60
|
85
|
+
when 'd'
|
86
|
+
@seconds = m[1].to_i * 24 * 60 * 60
|
87
|
+
end
|
88
|
+
else
|
89
|
+
raise OptionParser::InvalidArgument, "invalid argument: --valid-min #{str}"
|
90
|
+
end
|
91
|
+
@string = str
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get time in seconds
|
95
|
+
# @return [Integer]
|
96
|
+
def to_seconds
|
97
|
+
@seconds
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get time as string
|
101
|
+
# @return [String]
|
102
|
+
def to_s
|
103
|
+
@string
|
104
|
+
end
|
105
|
+
end
|
62
106
|
|
63
107
|
# Exit value for OK
|
64
108
|
RETURN_OK = 1
|
@@ -86,11 +130,10 @@ module LetsCert
|
|
86
130
|
domains: [],
|
87
131
|
files: [],
|
88
132
|
cert_key_size: 2048,
|
89
|
-
valid_min:
|
90
|
-
account_key_public_exponent: 65537,
|
133
|
+
valid_min: ValidTime.new('30d'),
|
91
134
|
account_key_size: 4096,
|
92
135
|
tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f35226540f',
|
93
|
-
user_agent:
|
136
|
+
user_agent: "letscert/#{VERSION.gsub(/\..*/, '')}",
|
94
137
|
server: 'https://acme-v01.api.letsencrypt.org/directory',
|
95
138
|
}
|
96
139
|
|
@@ -144,23 +187,12 @@ module LetsCert
|
|
144
187
|
RETURN_ERROR
|
145
188
|
end
|
146
189
|
else
|
147
|
-
|
148
|
-
persisted = IOPlugin.empty_data
|
149
|
-
@options[:files].each do |file|
|
150
|
-
persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
|
151
|
-
oldv || newv
|
152
|
-
end
|
153
|
-
end
|
154
|
-
not_persisted = persisted.keys.find_all { |k| !persisted[k] }
|
155
|
-
unless not_persisted.empty?
|
156
|
-
raise Error, 'Selected IO plugins do not cover following components: ' +
|
157
|
-
not_persisted.join(', ')
|
158
|
-
end
|
190
|
+
check_persisted
|
159
191
|
|
160
192
|
data = load_data_from_disk(@options[:files])
|
161
193
|
|
162
194
|
certificate = Certificate.new(data[:cert])
|
163
|
-
if certificate.valid?(@options[:domains], @options[:valid_min])
|
195
|
+
if certificate.valid?(@options[:domains], @options[:valid_min].to_seconds)
|
164
196
|
@logger.info { 'no need to update cert' }
|
165
197
|
RETURN_OK
|
166
198
|
else
|
@@ -226,13 +258,17 @@ module LetsCert
|
|
226
258
|
|
227
259
|
opts.on('--cert-key-size BITS', Integer,
|
228
260
|
'Certificate key size in bits',
|
229
|
-
|
261
|
+
"(default: #{@options[:cert_key_size]})") do |bits|
|
230
262
|
@options[:cert_key_size] = bits
|
231
263
|
end
|
232
264
|
|
233
|
-
opts.
|
234
|
-
|
235
|
-
|
265
|
+
opts.accept(ValidTime) do |valid_time|
|
266
|
+
ValidTime.new(valid_time)
|
267
|
+
end
|
268
|
+
opts.on('--valid-min TIME', ValidTime, 'Renew existing certificate if validity',
|
269
|
+
'is lesser than TIME',
|
270
|
+
"(default: #{@options[:valid_min].to_s})") do |vt|
|
271
|
+
@options[:valid_min] = vt
|
236
272
|
end
|
237
273
|
|
238
274
|
opts.on('--reuse-key', 'Reuse previous private key') do |rk|
|
@@ -244,18 +280,14 @@ module LetsCert
|
|
244
280
|
" by --server")
|
245
281
|
opts.separator('')
|
246
282
|
|
247
|
-
opts.on('--account-key-public-exponent BITS', Integer,
|
248
|
-
'Account key public exponent value (default: 65537)') do |bits|
|
249
|
-
@options[:account_key_public_exponent] = bits
|
250
|
-
end
|
251
|
-
|
252
283
|
opts.on('--account-key-size BITS', Integer,
|
253
|
-
|
284
|
+
"Account key size (default: #{@options[:account_key_size]})") do |bits|
|
254
285
|
@options[:account_key_size] = bits
|
255
286
|
end
|
256
287
|
|
257
288
|
opts.on('--tos-sha256 HASH', String,
|
258
|
-
'SHA-256 digest of the content of Terms
|
289
|
+
'SHA-256 digest of the content of Terms',
|
290
|
+
'Of Service URI') do |hash|
|
259
291
|
@options[:tos_sha256] = hash
|
260
292
|
end
|
261
293
|
|
@@ -272,17 +304,36 @@ module LetsCert
|
|
272
304
|
opts.separator('')
|
273
305
|
|
274
306
|
opts.on('--user-agent NAME', 'User-Agent sent in all HTTP requests',
|
275
|
-
|
307
|
+
"(default: #{@options[:user_agent]})") do |ua|
|
276
308
|
@options[:user_agent] = ua
|
277
309
|
end
|
278
310
|
|
279
311
|
opts.on('--server URI', 'URI for the CA ACME API endpoint',
|
280
|
-
|
312
|
+
"(default: #{@options[:server]})") do |uri|
|
281
313
|
@options[:server] = uri
|
282
314
|
end
|
283
315
|
end
|
284
316
|
|
285
317
|
@opt_parser.parse!
|
318
|
+
compute_roots
|
319
|
+
end
|
320
|
+
|
321
|
+
# Check all components are covered by plugins
|
322
|
+
# @raise [Error]
|
323
|
+
def check_persisted
|
324
|
+
persisted = IOPlugin.empty_data
|
325
|
+
|
326
|
+
@options[:files].each do |file|
|
327
|
+
persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
|
328
|
+
oldv || newv
|
329
|
+
end
|
330
|
+
end
|
331
|
+
not_persisted = persisted.keys.find_all { |k| !persisted[k] }
|
332
|
+
|
333
|
+
unless not_persisted.empty?
|
334
|
+
raise Error, 'Selected IO plugins do not cover following components: ' +
|
335
|
+
not_persisted.join(', ')
|
336
|
+
end
|
286
337
|
end
|
287
338
|
|
288
339
|
|
@@ -313,6 +364,25 @@ module LetsCert
|
|
313
364
|
all_data
|
314
365
|
end
|
315
366
|
|
367
|
+
# Compute webroots and set +@options[:roots]+
|
368
|
+
# @return [Hash] where keys are domains and value are their webroot path
|
369
|
+
def compute_roots
|
370
|
+
roots = {}
|
371
|
+
|
372
|
+
@options[:domains].each do |domain|
|
373
|
+
match = domain.match(/([-\w\.]+):(.*)/)
|
374
|
+
if match
|
375
|
+
roots[match[1]] = match[2]
|
376
|
+
elsif @options[:default_root]
|
377
|
+
roots[domain] = @options[:default_root]
|
378
|
+
else
|
379
|
+
roots[domain] = nil
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
@options[:roots] = roots
|
384
|
+
end
|
385
|
+
|
316
386
|
end
|
317
387
|
|
318
388
|
end
|
data/lib/letscert.rb
CHANGED
data/spec/certificate_spec.rb
CHANGED
@@ -6,33 +6,120 @@ module LetsCert
|
|
6
6
|
|
7
7
|
before(:all) { Certificate.logger = Logger.new('/dev/null') }
|
8
8
|
|
9
|
-
|
9
|
+
before(:all) do
|
10
|
+
root_key = OpenSSL::PKey::RSA.new(512)
|
11
|
+
|
12
|
+
@domains = %w(example.org www.example.org)
|
13
|
+
|
14
|
+
key = OpenSSL::PKey::RSA.new(512)
|
15
|
+
@cert = OpenSSL::X509::Certificate.new
|
16
|
+
@cert.version = 2
|
17
|
+
@cert.serial = 2
|
18
|
+
@cert.issuer = OpenSSL::X509::Name.parse "/DC=letscert/CN=CA"
|
19
|
+
@cert.public_key = key.public_key
|
20
|
+
@cert.not_before = Time.now
|
21
|
+
# 20 days validity
|
22
|
+
@cert.not_after = @cert.not_before + 20 * 24 * 60 * 60
|
23
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
24
|
+
ef.subject_certificate = @cert
|
25
|
+
@domains.each do |domain|
|
26
|
+
@cert.add_extension(ef.create_extension('subjectAltName',
|
27
|
+
"DNS:#{domain}",
|
28
|
+
false))
|
29
|
+
end
|
30
|
+
@cert.sign(root_key, OpenSSL::Digest::SHA256.new)
|
31
|
+
|
32
|
+
# minimum size accepted by ACME server
|
33
|
+
@account_key2048 = OpenSSL::PKey::RSA.new(2048)
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:certificate) { Certificate.new(@cert) }
|
37
|
+
|
38
|
+
context '#get' do
|
39
|
+
|
40
|
+
it 'checks all domains have a root' do
|
41
|
+
runner = Runner.new
|
42
|
+
ARGV.clear
|
43
|
+
|
44
|
+
ARGV << '-d' << 'example.com:/var/ww/html'
|
45
|
+
ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
|
46
|
+
runner.parse_options
|
47
|
+
# raise error because no e-mail address was given
|
48
|
+
expect { certificate.get(nil, nil, runner.options) }.
|
49
|
+
to raise_error(Acme::Client::Error)
|
50
|
+
|
51
|
+
ARGV.clear
|
52
|
+
ARGV << '-d' << 'example.com:/var/www/html'
|
53
|
+
ARGV << '-d' << 'www.example.com'
|
54
|
+
ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
|
55
|
+
runner.options[:domains] = []
|
56
|
+
runner.parse_options
|
57
|
+
expect { certificate.get(nil, nil, runner.options) }.
|
58
|
+
to raise_error(LetsCert::Error).
|
59
|
+
with_message(/not specified: www\.example\.com\./)
|
60
|
+
|
61
|
+
ARGV.clear
|
62
|
+
ARGV << '-d' << 'example.com:/var/www/html'
|
63
|
+
ARGV << '-d' << 'www.example.com'
|
64
|
+
ARGV << '--default-root' << '/opt/www'
|
65
|
+
ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
|
66
|
+
runner.parse_options
|
67
|
+
# raise error because no e-mail address was given
|
68
|
+
expect { certificate.get(nil, nil, runner.options) }.
|
69
|
+
to raise_error(Acme::Client::Error)
|
70
|
+
expect(runner.options[:roots]['example.com']).to eq('/var/www/html')
|
71
|
+
expect(runner.options[:roots]['www.example.com']).to eq('/opt/www')
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'uses existing account key' do
|
75
|
+
options = { roots: { 'example.com' => '/var/www/html' } }
|
10
76
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
77
|
+
# Connection error: no server to connect to
|
78
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
79
|
+
to raise_error(Faraday::ConnectionFailed)
|
80
|
+
expect(certificate.client.private_key).to eq(@account_key2048)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'creates an ACME account key if non exists' do
|
84
|
+
options = {
|
85
|
+
roots: { 'example.com' => '/var/www/html' },
|
86
|
+
account_key_size: 128,
|
87
|
+
}
|
88
|
+
|
89
|
+
# Connection error: no server to connect to
|
90
|
+
expect { certificate.get(nil, nil, options) }.
|
91
|
+
to raise_error(Faraday::ConnectionFailed)
|
92
|
+
expect(certificate.client.private_key).to be_a(OpenSSL::PKey::RSA)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'creates an ACME client with provided account key and end point' do
|
96
|
+
options = {
|
97
|
+
roots: { 'example.com' => '/var/www/html' },
|
98
|
+
server: 'https://acme-staging.api.letsencrypt.org/directory',
|
99
|
+
}
|
100
|
+
|
101
|
+
# Acme error: not valid e-mail address
|
102
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
103
|
+
to raise_error(Acme::Client::Error)
|
104
|
+
expect(certificate.client.private_key).to eq(@account_key2048)
|
105
|
+
expect(certificate.client.instance_eval { @endpoint }).to eq(options[:server])
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'raises when register without e-mail' do
|
109
|
+
options = {
|
110
|
+
roots: { 'example.com' => '/var/www/html' },
|
111
|
+
server: 'https://acme-staging.api.letsencrypt.org/directory',
|
112
|
+
}
|
113
|
+
|
114
|
+
# Acme error: not valid e-mail address
|
115
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
116
|
+
to raise_error(Acme::Client::Error).
|
117
|
+
with_message('not a valid e-mail address')
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
context '#valid?' do
|
36
123
|
|
37
124
|
it 'checks whether a certificate is valid given a minimum valid duration' do
|
38
125
|
expect(certificate.valid?(@domains)).to be(true)
|
data/spec/runner_spec.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
module LetsCert
|
4
|
+
|
5
|
+
describe Runner do
|
6
|
+
|
7
|
+
before(:each) { ARGV.clear }
|
8
|
+
|
9
|
+
let(:runner) { Runner.new }
|
10
|
+
|
11
|
+
context '#parse_options' do
|
12
|
+
|
13
|
+
it 'accepts --domain with DOMAIN only' do
|
14
|
+
ARGV << '--domain' << 'example.com'
|
15
|
+
|
16
|
+
runner.parse_options
|
17
|
+
expect(runner.options[:domains]).to be_a(Array)
|
18
|
+
expect(runner.options[:domains].size).to eq(1)
|
19
|
+
expect(runner.options[:domains]).to include('example.com')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'accepts --domain with DOMAIN:PATH' do
|
23
|
+
ARGV << '--domain' << 'example.com:/var/www/html'
|
24
|
+
|
25
|
+
runner.parse_options
|
26
|
+
expect(runner.options[:domains]).to be_a(Array)
|
27
|
+
expect(runner.options[:domains].size).to eq(1)
|
28
|
+
expect(runner.options[:domains]).to include('example.com:/var/www/html')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'accepts multiple domains with --domain option' do
|
32
|
+
ARGV << '--domain' << 'example.com'
|
33
|
+
ARGV << '--domain' << 'www.example.com'
|
34
|
+
ARGV << '--domain' << 'www2.example.com'
|
35
|
+
|
36
|
+
runner.parse_options
|
37
|
+
expect(runner.options[:domains]).to be_a(Array)
|
38
|
+
expect(runner.options[:domains].size).to eq(3)
|
39
|
+
expect(runner.options[:domains]).to include('example.com')
|
40
|
+
expect(runner.options[:domains]).to include('www.example.com')
|
41
|
+
expect(runner.options[:domains]).to include('www2.example.com')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'sets default root path with --default-root for domains without PATH' do
|
45
|
+
ARGV << '--domain' << 'example.com'
|
46
|
+
ARGV << '--domain' << 'another-example.com:/var/www/html'
|
47
|
+
ARGV << '--default-root' << '/opt/www'
|
48
|
+
|
49
|
+
runner.parse_options
|
50
|
+
expect(runner.options[:default_root]).to eq('/opt/www')
|
51
|
+
expect(runner.options[:roots]).to be_a(Hash)
|
52
|
+
expect(runner.options[:roots]['example.com']).to eq(runner.options[:default_root])
|
53
|
+
expect(runner.options[:roots]['another-example.com']).to eq('/var/www/html')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'accepts multiples files with --file option' do
|
57
|
+
ARGV << '--file' << 'key.pem'
|
58
|
+
ARGV << '-f' << 'cert.pem'
|
59
|
+
|
60
|
+
runner.parse_options
|
61
|
+
expect(runner.options[:files]).to be_a(Array)
|
62
|
+
expect(runner.options[:files].size).to eq(2)
|
63
|
+
expect(runner.options[:files]).to include('key.pem')
|
64
|
+
expect(runner.options[:files]).to include('cert.pem')
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'sets minimum validity time with --valid-min option' do
|
68
|
+
ARGV << '--valid-min' << '30000'
|
69
|
+
|
70
|
+
runner.parse_options
|
71
|
+
expect(runner.options[:valid_min].to_seconds).to eq(30000)
|
72
|
+
end
|
73
|
+
|
74
|
+
it '--valid-min option accepts minute format' do
|
75
|
+
minutes = 156
|
76
|
+
ARGV << '--valid-min' << "#{minutes}m"
|
77
|
+
|
78
|
+
runner.parse_options
|
79
|
+
expect(runner.options[:valid_min].to_seconds).to eq(minutes * 60)
|
80
|
+
end
|
81
|
+
|
82
|
+
it '--valid-min option accepts hour format' do
|
83
|
+
hours = 4
|
84
|
+
ARGV << '--valid-min' << "#{hours}h"
|
85
|
+
|
86
|
+
runner.parse_options
|
87
|
+
expect(runner.options[:valid_min].to_seconds).to eq(hours * 3600)
|
88
|
+
end
|
89
|
+
|
90
|
+
it '--valid-min option accepts day format' do
|
91
|
+
days = 20
|
92
|
+
ARGV << '--valid-min' << "#{days}d"
|
93
|
+
|
94
|
+
runner.parse_options
|
95
|
+
expect(runner.options[:valid_min].to_seconds).to eq(days * 24 * 3600)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
it '#check_persisted checks all mandatory components are covered by files' do
|
101
|
+
expect { runner.check_persisted }.to raise_error(LetsCert::Error)
|
102
|
+
|
103
|
+
all_needed = [%w(account_key.json cert.pem chain.pem key.pem),
|
104
|
+
%w(account_key.json cert.der chain.pem key.der),
|
105
|
+
%w(account_key.json fullchain.pem key.pem),
|
106
|
+
%w(account_key.json fullchain.pem key.der)]
|
107
|
+
all_needed.each do |needed|
|
108
|
+
needed.size.times do |nb|
|
109
|
+
ARGV.clear
|
110
|
+
runner.options[:files] = []
|
111
|
+
0.upto(nb) do |i|
|
112
|
+
ARGV << '-f' << needed[i]
|
113
|
+
end
|
114
|
+
runner.parse_options
|
115
|
+
|
116
|
+
if nb == needed.size - 1
|
117
|
+
expect { runner.check_persisted }.to_not raise_error
|
118
|
+
else
|
119
|
+
expect { runner.check_persisted }.to raise_error(LetsCert::Error)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
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.3.
|
4
|
+
version: 0.3.1
|
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-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acme-client
|
@@ -95,6 +95,8 @@ files:
|
|
95
95
|
- spec/key.der
|
96
96
|
- spec/key.pem
|
97
97
|
- spec/loggable_spec.rb
|
98
|
+
- spec/runner_spec.rb
|
99
|
+
- spec/runner_spec.rb~
|
98
100
|
- spec/spec_helper.rb
|
99
101
|
- spec/test.json
|
100
102
|
- tasks/gem.rake
|