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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ecc722b11b425c139e92b66d7436be0b818d0d03
4
- data.tar.gz: 10b112ecdefce5e90e0c5cf8f99cd8bef961c2c0
3
+ metadata.gz: 2aaf92c6fdc9f696efb73eae2d3a86577b2dd37e
4
+ data.tar.gz: ec9885ad548b9ff43a645a2990eff2969326d646
5
5
  SHA512:
6
- metadata.gz: 32d5b6a6cfe0fd937506fc60791c793cac8024ab13a04531fc4a4bc56529074131d27f80c642f2d9e68a6026f3081b3d5578c0c2d1343f8a07e7b5acc1eb484c
7
- data.tar.gz: a2c004cd721d85ce5aec4afe22325eaf9cfd5f70fd173f029bd443826e7cf1361d341ef9db105a69afb91cf6ceca8da37539bdb32e382a629865612101fccbaf
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 promts.
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).
@@ -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 [Hash] options
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 for +cert+
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)
@@ -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 'json/jwt'
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 {#name}
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 {#name}
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 [Hash]
153
+ # @return [OpenSSL::PKey::PKey]
140
154
  def load_jwk(data)
141
155
  return nil if data.empty?
142
156
 
143
- JSON::JWK.new(JSON.parse(data)).to_key
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.to_jwk.to_json
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
 
@@ -203,8 +203,10 @@ module LetsCert
203
203
  end
204
204
 
205
205
  rescue Error, Acme::Client::Error => ex
206
- @logger.error ex.message
207
- puts "Error: #{ex.message}"
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
@@ -24,7 +24,7 @@
24
24
  module LetsCert
25
25
 
26
26
  # Letscert version number
27
- VERSION = '0.3.1'
27
+ VERSION = '0.4.0'
28
28
 
29
29
 
30
30
  # Base error class
@@ -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
- # raise error because no e-mail address was given
48
- expect { certificate.get(nil, nil, runner.options) }.
49
- to raise_error(Acme::Client::Error)
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
- # raise error because no e-mail address was given
68
- expect { certificate.get(nil, nil, runner.options) }.
69
- to raise_error(Acme::Client::Error)
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
- # Connection error: no server to connect to
78
- expect { certificate.get(@account_key2048, nil, options) }.
79
- to raise_error(Faraday::ConnectionFailed)
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
- # Connection error: no server to connect to
90
- expect { certificate.get(nil, nil, options) }.
91
- to raise_error(Faraday::ConnectionFailed)
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
- # Acme error: not valid e-mail address
102
- expect { certificate.get(@account_key2048, nil, options) }.
103
- to raise_error(Acme::Client::Error)
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
- # 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')
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.3.0'
27
- s.add_dependency 'json-jwt', '~>1.5'
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.3.1
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-02-22 00:00:00.000000000 Z
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.3.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.3.0
26
+ version: 0.4.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: json-jwt
28
+ name: json
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.5'
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: '1.5'
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.4.5.1
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
@@ -1,8 +0,0 @@
1
- require_relative 'spec_helper'
2
-
3
- module LetsCert
4
-
5
- describe Certificate do
6
- end
7
-
8
- end
data/spec/runner_spec.rb~ DELETED
@@ -1,8 +0,0 @@
1
- require_relative 'spec_helper'
2
-
3
- module LetsCert
4
-
5
- describe Runner do
6
- end
7
-
8
- end