letscert 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/letscert/certificate.rb +28 -20
- data/lib/letscert/io_plugin.rb +48 -6
- data/lib/letscert/runner.rb +4 -2
- data/lib/letscert.rb +1 -1
- data/spec/certificate_spec.rb +61 -19
- data/spec/spec_helper.rb +26 -0
- data/tasks/gem.rake +4 -2
- metadata +36 -10
- data/spec/certificate_spec.rb~ +0 -8
- data/spec/runner_spec.rb~ +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2aaf92c6fdc9f696efb73eae2d3a86577b2dd37e
|
4
|
+
data.tar.gz: ec9885ad548b9ff43a645a2990eff2969326d646
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 298d19dda74b12aa3b44d67d80ecbf74f26ed9d68609eb8efe6b82caa5fa7f34e3a48460949350b80c0d4abee890d6b13f0fdf32a89445d4cd25cdbe55c79379
|
7
|
+
data.tar.gz: f2e907cb8c7da052030e5b743419839f45623a2c612e45f0d2e5e042e5ffd2f32480711d15de6bcce09a1717aae1026b30ea1a96ca431b3da307b35621c24e31
|
data/README.md
CHANGED
@@ -61,7 +61,7 @@ letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld --r
|
|
61
61
|
* Renew certificate only if needed.
|
62
62
|
* Only `http-01` challenge supported. An existing web server must be alreay running.
|
63
63
|
`letscert` should have write access to `${webroot}/.well-known/acme-challenge`.
|
64
|
-
* Crontab friendly: no
|
64
|
+
* Crontab friendly: no prompts.
|
65
65
|
* No configuration file.
|
66
66
|
* Support multiple domains with multiple roots. Always create a single certificate per
|
67
67
|
run (ie a certificate may have multiple SANs).
|
data/lib/letscert/certificate.rb
CHANGED
@@ -56,6 +56,7 @@ module LetsCert
|
|
56
56
|
# @option options [Hash] :roots hash associating domains as keys to web roots as
|
57
57
|
# values
|
58
58
|
# @option options [String] :server ACME servel URL
|
59
|
+
# @return [void]
|
59
60
|
def get(account_key, key, options)
|
60
61
|
logger.info {"create key/cert/chain..." }
|
61
62
|
check_roots(options[:roots])
|
@@ -85,8 +86,12 @@ module LetsCert
|
|
85
86
|
|
86
87
|
# Revoke certificate
|
87
88
|
# @param [OpenSSL::PKey::PKey] account_key
|
89
|
+
# @param [Hash] options
|
90
|
+
# @option options [Fixnum] :account_key_size ACME account private key size in bits
|
91
|
+
# @option options [String] :email e-mail used as ACME account
|
92
|
+
# @option options [String] :server ACME servel URL
|
88
93
|
# @return [Boolean]
|
89
|
-
def revoke(account_key, options)
|
94
|
+
def revoke(account_key, options={})
|
90
95
|
if @cert.nil?
|
91
96
|
raise Error, 'no certification data to revoke'
|
92
97
|
end
|
@@ -135,27 +140,12 @@ module LetsCert
|
|
135
140
|
!renewal_necessary?(valid_min)
|
136
141
|
end
|
137
142
|
|
138
|
-
|
139
|
-
private
|
140
|
-
|
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? }
|
146
|
-
|
147
|
-
if !no_roots.empty?
|
148
|
-
raise Error, 'root for the following domain(s) are not specified: ' +
|
149
|
-
no_roots.keys.join(', ') + ".\nTry --default_root or use " +
|
150
|
-
'-d example.com:/var/www/html syntax.'
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
143
|
# Get ACME client.
|
155
144
|
#
|
156
145
|
# Client is only created on first call, then it is cached.
|
157
146
|
# @param [Hash] account_key
|
158
147
|
# @param [Hash] options
|
148
|
+
# @return [Acme::Client]
|
159
149
|
def get_acme_client(account_key, options)
|
160
150
|
return @client if @client
|
161
151
|
|
@@ -164,6 +154,8 @@ module LetsCert
|
|
164
154
|
logger.debug { "connect to #{options[:server]}" }
|
165
155
|
@client = Acme::Client.new(private_key: key, endpoint: options[:server])
|
166
156
|
|
157
|
+
yield @client if block_given?
|
158
|
+
|
167
159
|
if options[:email].nil?
|
168
160
|
logger.warn { '--email was not provided. ACME CA will have no way to ' +
|
169
161
|
'contact you!' }
|
@@ -197,9 +189,26 @@ module LetsCert
|
|
197
189
|
@client
|
198
190
|
end
|
199
191
|
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
# check webroots.
|
196
|
+
# @param [Hash] roots
|
197
|
+
# @raise [Error] if some domains have no defined root.
|
198
|
+
def check_roots(roots)
|
199
|
+
no_roots = roots.select { |k,v| v.nil? }
|
200
|
+
|
201
|
+
if !no_roots.empty?
|
202
|
+
raise Error, 'root for the following domain(s) are not specified: ' +
|
203
|
+
no_roots.keys.join(', ') + ".\nTry --default_root or use " +
|
204
|
+
'-d example.com:/var/www/html syntax.'
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
200
208
|
# Generate a new account key if no one is given in +data+
|
201
209
|
# @param [OpenSSL::PKey,nil] key
|
202
|
-
# @param [
|
210
|
+
# @param [Integer] key_size
|
211
|
+
# @return [OpenSSL::PKey::PKey]
|
203
212
|
def get_account_key(key, key_size)
|
204
213
|
if key.nil?
|
205
214
|
logger.info { 'No account key. Generate a new one...' }
|
@@ -260,8 +269,7 @@ module LetsCert
|
|
260
269
|
end
|
261
270
|
end
|
262
271
|
|
263
|
-
# Check if a renewal is necessary
|
264
|
-
# @param [OpenSSL::X509::Certificate] cert
|
272
|
+
# Check if a renewal is necessary
|
265
273
|
# @param [Number] valid_min minimum validity in seconds to ensure
|
266
274
|
# @return [Boolean]
|
267
275
|
def renewal_necessary?(valid_min)
|
data/lib/letscert/io_plugin.rb
CHANGED
@@ -19,7 +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 '
|
22
|
+
require 'base64'
|
23
23
|
require_relative 'loggable'
|
24
24
|
|
25
25
|
module LetsCert
|
@@ -86,7 +86,7 @@ module LetsCert
|
|
86
86
|
# @author Sylvain Daubert
|
87
87
|
module FileIOPluginMixin
|
88
88
|
|
89
|
-
# Load data from file named
|
89
|
+
# Load data from file named +#name+
|
90
90
|
# @return [Hash]
|
91
91
|
def load
|
92
92
|
logger.debug { "Loading #@name" }
|
@@ -110,7 +110,7 @@ module LetsCert
|
|
110
110
|
raise NotImplementedError
|
111
111
|
end
|
112
112
|
|
113
|
-
# Save data to file
|
113
|
+
# Save data to file +#name+
|
114
114
|
# @param [Hash] data
|
115
115
|
# @return [void]
|
116
116
|
def save_to_file(data)
|
@@ -134,20 +134,62 @@ module LetsCert
|
|
134
134
|
# @author Sylvain Daubert
|
135
135
|
module JWKIOPluginMixin
|
136
136
|
|
137
|
+
# Encode string +data+ to base64
|
138
|
+
# @param [String] data
|
139
|
+
# @return [String]
|
140
|
+
def urlsafe_encode64(data)
|
141
|
+
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
142
|
+
end
|
143
|
+
|
144
|
+
# Decode base64 string +data+
|
145
|
+
# @param [String] data
|
146
|
+
# @return [String]
|
147
|
+
def urlsafe_decode64(data)
|
148
|
+
Base64.urlsafe_decode64(data.sub(/[\s=]*\z/, ''))
|
149
|
+
end
|
150
|
+
|
137
151
|
# Load crypto data from JSON-encoded file
|
138
152
|
# @param [String] data JSON-encoded data
|
139
|
-
# @return [
|
153
|
+
# @return [OpenSSL::PKey::PKey]
|
140
154
|
def load_jwk(data)
|
141
155
|
return nil if data.empty?
|
142
156
|
|
143
|
-
JSON
|
157
|
+
h = JSON.parse(data)
|
158
|
+
case h['kty']
|
159
|
+
when 'RSA'
|
160
|
+
pkey = OpenSSL::PKey::RSA.new
|
161
|
+
%w(e n d p q).collect do |key|
|
162
|
+
next if h[key].nil?
|
163
|
+
value = OpenSSL::BN.new(urlsafe_decode64(h[key]), 2)
|
164
|
+
pkey.send "#{key}=".to_sym, value
|
165
|
+
end
|
166
|
+
else
|
167
|
+
raise Error, "unknown account key type '#{k['kty']}'"
|
168
|
+
end
|
169
|
+
|
170
|
+
pkey
|
144
171
|
end
|
145
172
|
|
146
173
|
# Dump crypto data (key) to a JSON-encoded string
|
147
174
|
# @param [OpenSSL::PKey] key
|
148
175
|
# @return [String]
|
149
176
|
def dump_jwk(key)
|
150
|
-
key.
|
177
|
+
return {}.to_json if key.nil?
|
178
|
+
|
179
|
+
h = { 'kty' => 'RSA' }
|
180
|
+
case key
|
181
|
+
when OpenSSL::PKey::RSA
|
182
|
+
h['e'] = urlsafe_encode64(key.e.to_s(2)) if key.e
|
183
|
+
h['n'] = urlsafe_encode64(key.n.to_s(2)) if key.n
|
184
|
+
if key.private?
|
185
|
+
h['d'] = urlsafe_encode64(key.d.to_s(2))
|
186
|
+
h['p'] = urlsafe_encode64(key.p.to_s(2))
|
187
|
+
h['q'] = urlsafe_encode64(key.q.to_s(2))
|
188
|
+
end
|
189
|
+
else
|
190
|
+
raise Error, "only RSA keys are supported"
|
191
|
+
end
|
192
|
+
h.to_json
|
151
193
|
end
|
152
194
|
end
|
153
195
|
|
data/lib/letscert/runner.rb
CHANGED
@@ -203,8 +203,10 @@ module LetsCert
|
|
203
203
|
end
|
204
204
|
|
205
205
|
rescue Error, Acme::Client::Error => ex
|
206
|
-
|
207
|
-
|
206
|
+
msg = ex.message
|
207
|
+
msg = "[Acme] #{msg}" if ex.is_a?(Acme::Client::Error)
|
208
|
+
@logger.error msg
|
209
|
+
puts "Error: #{msg}"
|
208
210
|
RETURN_ERROR
|
209
211
|
end
|
210
212
|
end
|
data/lib/letscert.rb
CHANGED
data/spec/certificate_spec.rb
CHANGED
@@ -44,9 +44,11 @@ module LetsCert
|
|
44
44
|
ARGV << '-d' << 'example.com:/var/ww/html'
|
45
45
|
ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
|
46
46
|
runner.parse_options
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
VCR.use_cassette('single-domain') do
|
48
|
+
# raise error because no e-mail address was given
|
49
|
+
expect { certificate.get(nil, nil, runner.options) }.
|
50
|
+
to raise_error(Acme::Client::Error)
|
51
|
+
end
|
50
52
|
|
51
53
|
ARGV.clear
|
52
54
|
ARGV << '-d' << 'example.com:/var/www/html'
|
@@ -64,9 +66,11 @@ module LetsCert
|
|
64
66
|
ARGV << '--default-root' << '/opt/www'
|
65
67
|
ARGV << '--server' << 'https://acme-staging.api.letsencrypt.org/directory'
|
66
68
|
runner.parse_options
|
67
|
-
|
68
|
-
|
69
|
-
|
69
|
+
VCR.use_cassette('default-root') do
|
70
|
+
# raise error because no e-mail address was given
|
71
|
+
expect { certificate.get(nil, nil, runner.options) }.
|
72
|
+
to raise_error(Acme::Client::Error)
|
73
|
+
end
|
70
74
|
expect(runner.options[:roots]['example.com']).to eq('/var/www/html')
|
71
75
|
expect(runner.options[:roots]['www.example.com']).to eq('/opt/www')
|
72
76
|
end
|
@@ -74,9 +78,11 @@ module LetsCert
|
|
74
78
|
it 'uses existing account key' do
|
75
79
|
options = { roots: { 'example.com' => '/var/www/html' } }
|
76
80
|
|
77
|
-
|
78
|
-
|
79
|
-
|
81
|
+
VCR.use_cassette('no-server') do
|
82
|
+
# Connection error: no server to connect to
|
83
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
84
|
+
to raise_error(Faraday::ConnectionFailed)
|
85
|
+
end
|
80
86
|
expect(certificate.client.private_key).to eq(@account_key2048)
|
81
87
|
end
|
82
88
|
|
@@ -86,9 +92,11 @@ module LetsCert
|
|
86
92
|
account_key_size: 128,
|
87
93
|
}
|
88
94
|
|
89
|
-
|
90
|
-
|
91
|
-
|
95
|
+
VCR.use_cassette('no-server') do
|
96
|
+
# Connection error: no server to connect to
|
97
|
+
expect { certificate.get(nil, nil, options) }.
|
98
|
+
to raise_error(Faraday::ConnectionFailed)
|
99
|
+
end
|
92
100
|
expect(certificate.client.private_key).to be_a(OpenSSL::PKey::RSA)
|
93
101
|
end
|
94
102
|
|
@@ -98,9 +106,11 @@ module LetsCert
|
|
98
106
|
server: 'https://acme-staging.api.letsencrypt.org/directory',
|
99
107
|
}
|
100
108
|
|
101
|
-
|
102
|
-
|
103
|
-
|
109
|
+
VCR.use_cassette('create-acme-client') do
|
110
|
+
# Acme error: not valid e-mail address
|
111
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
112
|
+
to raise_error(Acme::Client::Error)
|
113
|
+
end
|
104
114
|
expect(certificate.client.private_key).to eq(@account_key2048)
|
105
115
|
expect(certificate.client.instance_eval { @endpoint }).to eq(options[:server])
|
106
116
|
end
|
@@ -111,12 +121,44 @@ module LetsCert
|
|
111
121
|
server: 'https://acme-staging.api.letsencrypt.org/directory',
|
112
122
|
}
|
113
123
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
124
|
+
VCR.use_cassette('create-acme-client-but-bad-email') do
|
125
|
+
# Acme error: not valid e-mail address
|
126
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
127
|
+
to raise_error(Acme::Client::Error).
|
128
|
+
with_message('not a valid e-mail address')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'responds to HTTP-01 challenge'
|
133
|
+
|
134
|
+
it 'raises if HTTP-01 challenge is unavailable' do
|
135
|
+
options = {
|
136
|
+
roots: { 'example.com' => '/var/www/html' },
|
137
|
+
server: 'https://acme-staging.api.letsencrypt.org/directory',
|
138
|
+
email: 'test@example.org',
|
139
|
+
}
|
140
|
+
|
141
|
+
VCR.use_cassette('no-http-01-challenge') do
|
142
|
+
certificate.get_acme_client(@account_key2048, options) do |client|
|
143
|
+
client.connection.builder.insert 0, RemoveHttp01Middleware
|
144
|
+
end
|
145
|
+
expect { certificate.get(@account_key2048, nil, options) }.
|
146
|
+
to raise_error(LetsCert::Error).with_message(/not offer http-01/)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'reuses existing private key if --reuse-key is present'
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
context '#revoke' do
|
155
|
+
it 'raises if no certificate is given' do
|
156
|
+
certificate = Certificate.new(nil)
|
157
|
+
expect { certificate.revoke(@account_key2048) }.
|
158
|
+
to raise_error(LetsCert::Error)
|
118
159
|
end
|
119
160
|
|
161
|
+
it 'revokes an existing certificate'
|
120
162
|
end
|
121
163
|
|
122
164
|
context '#valid?' do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,2 +1,28 @@
|
|
1
1
|
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
2
2
|
require 'letscert'
|
3
|
+
|
4
|
+
require 'vcr'
|
5
|
+
require 'faraday'
|
6
|
+
|
7
|
+
VCR.configure do |config|
|
8
|
+
config.cassette_library_dir = "fixtures/vcr_cassettes"
|
9
|
+
config.hook_into :faraday
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
# Faraday Middleware to remove HTTP-01 challenge
|
14
|
+
class RemoveHttp01Middleware < Faraday::Middleware
|
15
|
+
def call(request_env)
|
16
|
+
@app.call(request_env).on_complete do |response_env|
|
17
|
+
body = response_env.response.body
|
18
|
+
if body['challenges'] and !body['challenges'].empty?
|
19
|
+
body['challenges'].each_with_index do |challenge, index|
|
20
|
+
if challenge['type'] == 'http-01'
|
21
|
+
body['challenges'].delete_at(index)
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/tasks/gem.rake
CHANGED
@@ -23,10 +23,12 @@ EOF
|
|
23
23
|
|
24
24
|
s.required_ruby_version = '>= 2.1.0'
|
25
25
|
|
26
|
-
s.add_dependency 'acme-client', '~>0.
|
27
|
-
s.add_dependency 'json
|
26
|
+
s.add_dependency 'acme-client', '~>0.4.0'
|
27
|
+
s.add_dependency 'json', '~>1.8.3'
|
28
28
|
|
29
29
|
s.add_development_dependency 'rspec', '~>3.4'
|
30
|
+
s.add_development_dependency 'vcr', '~>3.0'
|
31
|
+
s.add_development_dependency 'faraday', '~>0.9'
|
30
32
|
s.add_development_dependency 'yard', '~>0.8'
|
31
33
|
end
|
32
34
|
|
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.
|
4
|
+
version: 0.4.0
|
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-
|
11
|
+
date: 2016-08-14 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.4.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.4.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: json
|
28
|
+
name: json
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 1.8.3
|
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:
|
40
|
+
version: 1.8.3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,34 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '3.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: vcr
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: faraday
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.9'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: yard
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,7 +116,6 @@ files:
|
|
88
116
|
- spec/cert.der
|
89
117
|
- spec/cert.pem
|
90
118
|
- spec/certificate_spec.rb
|
91
|
-
- spec/certificate_spec.rb~
|
92
119
|
- spec/chain.pem
|
93
120
|
- spec/fullchain.pem
|
94
121
|
- spec/io_plugin_spec.rb
|
@@ -96,7 +123,6 @@ files:
|
|
96
123
|
- spec/key.pem
|
97
124
|
- spec/loggable_spec.rb
|
98
125
|
- spec/runner_spec.rb
|
99
|
-
- spec/runner_spec.rb~
|
100
126
|
- spec/spec_helper.rb
|
101
127
|
- spec/test.json
|
102
128
|
- tasks/gem.rake
|
@@ -122,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
148
|
version: '0'
|
123
149
|
requirements: []
|
124
150
|
rubyforge_project:
|
125
|
-
rubygems_version: 2.
|
151
|
+
rubygems_version: 2.5.1
|
126
152
|
signing_key:
|
127
153
|
specification_version: 4
|
128
154
|
summary: letscert, a simple Let's Encrypt client
|
data/spec/certificate_spec.rb~
DELETED