letscert 0.4.1 → 0.4.2

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: bf207714b90b6a7da996b80ce11c034408eedb01
4
- data.tar.gz: 037d0a835588236f7046b6ca09a0936fe34473ef
3
+ metadata.gz: 81459818c78f3836b106c632ae5befa99dcacf82
4
+ data.tar.gz: 1fce468a7511a75b84f584ff1fd8b5455db48065
5
5
  SHA512:
6
- metadata.gz: b2adc8ccf14ab988d0c4debd191b664a6793670b4f3218e477285e8907466c93f8689f514da0025c4d0113a0fcf00fea78a59dc4ba0345dc22fd60a1e5e62bed
7
- data.tar.gz: 4e84d8553a12638d26a7444cb9760a440ae468ffa870ba3078d1678f7de39f7828224a5ad8694b4226500d292e79b3beb390a1ac90e2e13ea3f977d197d0ba94
6
+ metadata.gz: 3b4ce524f540fbb291c6c88f13d32bfa27a501c2105d8c5002ff98ef9d4c808802cdaecd7cbffad6e53695792b84ed180517d16facd079af245f960f9cce100a
7
+ data.tar.gz: 2149c9cd3a620ecebf9d1ec2bdaaeef2f7f92944127c25f4351ecc42424503526c9fbf2d8498ad19b54277f76b301fe3a838eb96a3a7562745f69e08c60c4077
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
1
+ Style/AndOr:
2
+ Enabled: false
3
+
4
+ Style/EmptyLinesAroundClassBody:
5
+ Enabled: false
6
+
7
+ Style/EmptyLinesAroundModuleBody:
8
+ Enabled: false
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+
13
+ Style/FormatString:
14
+ Enabled: false
15
+
16
+ Metrics/ClassLength:
17
+ Max: 150
18
+
19
+ Metrics/ModuleLength:
20
+ Max: 150
21
+
22
+ Metrics/MethodLength:
23
+ Max: 20
24
+
25
+ # don't understand this metric
26
+ Metrics/AbcSize:
27
+ Enabled: false
28
+
29
+ AllCops:
30
+ Exclude:
31
+ - 'spec/**/*'
32
+ - 'vendor/**/*'
33
+
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ gem 'rubocop', '~> 0.42.0', require: false
data/README.md CHANGED
@@ -75,14 +75,17 @@ letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld --r
75
75
  * 2 in case of errors.
76
76
 
77
77
  # Installation
78
- `letscert` is cryptographically signed. To be sure the gem you install hasn’t been tampered:
78
+ Since v0.4.1, `letscert` is cryptographically signed. To be sure the gem you install
79
+ hasn’t been tampered:
79
80
  * add my public key as a trusted certificate:
80
81
  ```
81
- gem cert --add <(curl -Ls https://raw.github.com/metricfu/metric_fu/master/certs/gem-public_cert.pem)
82
+ gem cert --add <(curl -Ls https://raw.github.com/sdaubert/letscert/master/certs/gem-public_cert.pem)
82
83
  ```
83
84
  * install letscert gem with a policy:
84
85
  ```
85
86
  gem install letscert -P MediumSecurity
86
87
  ```
87
88
 
88
- The MediumSecurity trust profile will verify signed gems, but allow the installation of unsigned dependencies. This is necessary because not all of letcert’s dependencies are signed, so we cannot use HighSecurity.
89
+ The MediumSecurity trust profile will verify signed gems, but allow the installation of
90
+ unsigned dependencies. This is necessary because not all of letcert’s dependencies are
91
+ signed, so we cannot use HighSecurity.
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
3
  require 'yard'
4
+ require 'rubocop/rake_task'
4
5
 
5
6
  RSpec::Core::RakeTask.new
6
7
 
@@ -9,4 +10,6 @@ YARD::Rake::YardocTask.new do |t|
9
10
  t.files = ['lib/**/*.rb', '-', 'LICENSE']
10
11
  end
11
12
 
12
- task :default => :spec
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: :spec
data/letscert.gemspec CHANGED
@@ -16,17 +16,17 @@ EOF
16
16
  s.email = 'sylvain.daubert@laposte.net'
17
17
  s.homepage = 'https://github.com/sdaubert/letscert'
18
18
 
19
- s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ files = `git ls-files -z`.split("\x0")
20
+ s.files = files.reject { |f| f.match(%r{^(test|spec|features)/}) }
20
21
  s.executables = ['letscert']
21
- s.require_paths = ["lib"]
22
+ s.require_paths = ['lib']
22
23
 
23
24
  s.required_ruby_version = '>= 2.1.0'
24
25
 
25
26
  s.add_dependency 'acme-client', '~>0.4.0'
26
- s.add_dependency 'json', '~>1.8.3'
27
27
 
28
- s.add_development_dependency "bundler", "~> 1.12"
29
- s.add_development_dependency "rake", "~> 10.0"
28
+ s.add_development_dependency 'bundler', '~> 1.12'
29
+ s.add_development_dependency 'rake', '~> 10.0'
30
30
  s.add_development_dependency 'rspec', '~>3.4'
31
31
  s.add_development_dependency 'vcr', '~>3.0'
32
32
  s.add_development_dependency 'yard', '~>0.8'
@@ -37,7 +37,6 @@ module LetsCert
37
37
  # @return [Acme::Client,nil]
38
38
  attr_reader :client
39
39
 
40
-
41
40
  # @param [OpenSSL::X509::Certificate,nil] cert
42
41
  def initialize(cert)
43
42
  @cert = cert
@@ -45,26 +44,28 @@ module LetsCert
45
44
  end
46
45
 
47
46
  # Get a new certificate, or renew an existing one
48
- # @param [OpenSSL::PKey::PKey,nil] account_key private key to authenticate to ACME
49
- # server
50
- # @param [OpenSSL::PKey::PKey, nil] key private key from which make a certificate.
51
- # If +nil+, generate a new one with +options[:cet_key_size]+ bits.
47
+ # @param [OpenSSL::PKey::PKey,nil] account_key private key to
48
+ # authenticate to ACME server
49
+ # @param [OpenSSL::PKey::PKey, nil] key private key from which make a
50
+ # certificate. If +nil+, generate a new one with +options[:cet_key_size]+
51
+ # bits.
52
52
  # @param [Hash] options option hash
53
- # @option options [Fixnum] :account_key_size ACME account private key size in bits
53
+ # @option options [Fixnum] :account_key_size ACME account private key size
54
+ # in bits
54
55
  # @option options [Fixnum] :cert_key_size private key size used to generate
55
56
  # a certificate
56
57
  # @option options [String] :email e-mail used as ACME account
57
58
  # @option options [Array<String>] :files plugin names to use
58
59
  # @option options [Boolean] :reuse_key reuse private key when getting a new
59
60
  # certificate
60
- # @option options [Hash] :roots hash associating domains as keys to web roots as
61
- # values
61
+ # @option options [Hash] :roots hash associating domains as keys to web
62
+ # roots as values
62
63
  # @option options [String] :server ACME servel URL
63
64
  # @return [void]
64
65
  # @raise [Acme::Client::Error] error in protocol ACME with server
65
66
  # @raise [Error] issue with domain name, challenge fails,...
66
67
  def get(account_key, key, options)
67
- logger.info {"create key/cert/chain..." }
68
+ logger.info { 'create key/cert/chain...' }
68
69
  check_roots(options[:roots])
69
70
  logger.debug { "webroots are: #{options[:roots].inspect}" }
70
71
 
@@ -72,23 +73,20 @@ module LetsCert
72
73
 
73
74
  do_challenges client, options[:roots]
74
75
 
75
- if options[:reuse_key] and !key.nil?
76
- logger.info { 'Reuse existing private key' }
77
- else
78
- logger.info { 'Generate new private key' }
79
- key = OpenSSL::PKey::RSA.generate(options[:cert_key_size])
80
- end
81
-
82
- csr = Acme::Client::CertificateRequest.new(names: options[:roots].keys,
83
- private_key: key)
84
- acme_cert = client.new_certificate(csr)
85
- @cert = acme_cert.x509
86
- @chain = acme_cert.x509_chain
76
+ pkey = if options[:reuse_key]
77
+ raise Error, 'cannot reuse a non-existing key' if key.nil?
78
+ logger.info { 'Reuse existing private key' }
79
+ generate_certificate_from_pkey options[:roots].keys, key
80
+ else
81
+ logger.info { 'Generate new private key' }
82
+ generate_certificate options[:roots].keys,
83
+ options[:cert_key_size]
84
+ end
87
85
 
88
86
  options[:files] ||= []
89
87
  options[:files].each do |plugname|
90
88
  IOPlugin.registered[plugname].save(account_key: client.private_key,
91
- key: key, cert: @cert,
89
+ key: pkey, cert: @cert,
92
90
  chain: @chain)
93
91
  end
94
92
  end
@@ -96,22 +94,17 @@ module LetsCert
96
94
  # Revoke certificate
97
95
  # @param [OpenSSL::PKey::PKey] account_key
98
96
  # @param [Hash] options
99
- # @option options [Fixnum] :account_key_size ACME account private key size in bits
97
+ # @option options [Fixnum] :account_key_size ACME account private key size
98
+ # in bits
100
99
  # @option options [String] :email e-mail used as ACME account
101
100
  # @option options [String] :server ACME servel URL
102
101
  # @return [Boolean]
103
102
  # @raise [Error] no certificate to revole.
104
- def revoke(account_key, options={})
105
- if @cert.nil?
106
- raise Error, 'no certification data to revoke'
107
- end
103
+ def revoke(account_key, options = {})
104
+ raise Error, 'no certification data to revoke' if @cert.nil?
108
105
 
109
106
  client = get_acme_client(account_key, options)
110
- begin
111
- result = client.revoke_certificate(@cert)
112
- rescue Exception => ex
113
- raise
114
- end
107
+ result = client.revoke_certificate(@cert)
115
108
 
116
109
  if result
117
110
  logger.info { 'certificate is revoked' }
@@ -125,10 +118,10 @@ module LetsCert
125
118
  # Check if certificate is still valid for at least +valid_min+ seconds.
126
119
  # Also checks that +domains+ are certified by certificate.
127
120
  # @param [Array<String>] domains list of certificate domains
128
- # @param [Integer] valid_min minimum number of seconds of validity under which
129
- # a renewal is necessary.
121
+ # @param [Integer] valid_min minimum number of seconds of validity under
122
+ # which a renewal is necessary.
130
123
  # @return [Boolean]
131
- def valid?(domains, valid_min=0)
124
+ def valid?(domains, valid_min = 0)
132
125
  if @cert.nil?
133
126
  logger.debug { 'no existing certificate' }
134
127
  return false
@@ -144,8 +137,9 @@ module LetsCert
144
137
 
145
138
  # Check all domains are subjects of certificate
146
139
  unless domains.all? { |domain| subjects.include? domain }
147
- raise Error, "At least one domain is not declared as a certificate subject." +
148
- "Backup and remove existing cert if you want to proceed"
140
+ msg = 'At least one domain is not declared as a certificate subject. ' \
141
+ 'Backup and remove existing cert if you want to proceed.'
142
+ raise Error, msg
149
143
  end
150
144
 
151
145
  !renewal_necessary?(valid_min)
@@ -168,20 +162,18 @@ module LetsCert
168
162
  yield @client if block_given?
169
163
 
170
164
  if options[:email].nil?
171
- logger.warn { '--email was not provided. ACME CA will have no way to ' +
172
- 'contact you!' }
165
+ logger.warn { '--email was not provided. ACME CA will have no way to ' \
166
+ 'contact you!' }
173
167
  end
174
168
 
175
169
  begin
176
170
  logger.debug { "register with #{options[:email]}" }
177
171
  registration = @client.register(contact: "mailto:#{options[:email]}")
178
172
  rescue Acme::Client::Error::Malformed => ex
179
- if ex.message != 'Registration key is already in use'
180
- raise
181
- end
173
+ raise if ex.message != 'Registration key is already in use'
182
174
  else
183
- # Requesting ToS make acme-client throw an exception: Connection reset by peer
184
- # (Faraday::ConnectionFailed). To investigate...
175
+ # Requesting ToS make acme-client throw an exception: Connection reset
176
+ # by peer (Faraday::ConnectionFailed). To investigate...
185
177
  #if registration.term_of_service_uri
186
178
  # @logger.debug { "get terms of service" }
187
179
  # terms = registration.get_terms
@@ -190,8 +182,7 @@ module LetsCert
190
182
  # if tos_digest != @options[:tos_sha256]
191
183
  # raise Error, 'Terms Of Service mismatch'
192
184
  # end
193
-
194
- @logger.debug { "agree terms of service" }
185
+ @logger.debug { 'agree terms of service' }
195
186
  registration.agree_terms
196
187
  # end
197
188
  #end
@@ -200,19 +191,18 @@ module LetsCert
200
191
  @client
201
192
  end
202
193
 
203
-
204
194
  private
205
195
 
206
196
  # check webroots.
207
197
  # @param [Hash] roots
208
198
  # @raise [Error] if some domains have no defined root.
209
199
  def check_roots(roots)
210
- no_roots = roots.select { |k,v| v.nil? }
200
+ no_roots = roots.select { |_k, v| v.nil? }
211
201
 
212
- if !no_roots.empty?
213
- raise Error, 'root for the following domain(s) are not specified: ' +
214
- no_roots.keys.join(', ') + ".\nTry --default_root or use " +
215
- '-d example.com:/var/www/html syntax.'
202
+ unless no_roots.empty?
203
+ raise Error, 'root for the following domain(s) are not specified: ' \
204
+ "#{no_roots.keys.join(', ')}.\nTry --default_root or " \
205
+ 'use -d example.com:/var/www/html syntax.'
216
206
  end
217
207
  end
218
208
 
@@ -234,26 +224,12 @@ module LetsCert
234
224
  # @param [Hash] roots
235
225
  def do_challenges(client, roots)
236
226
  logger.debug { 'Get authorization for all domains' }
237
- challenges = {}
238
-
239
- roots.keys.each do |domain|
240
- authorization = client.authorize(domain: domain)
241
- if authorization
242
- challenges[domain] = authorization.http01
243
- else
244
- challenges[domain] = nil
245
- end
246
- end
247
-
248
- logger.debug { 'Check all challenges are HTTP-01' }
249
- if challenges.values.any? { |chall| chall.nil? }
250
- raise Error, 'CA did not offer http-01-only challenge. ' +
251
- 'This client is unable to solve any other challenges.'
252
- end
227
+ challenges = get_challenges(client, roots)
253
228
 
254
229
  challenges.each do |domain, challenge|
255
230
  begin
256
- FileUtils.mkdir_p(File.join(roots[domain], File.dirname(challenge.filename)))
231
+ path = File.join(roots[domain], File.dirname(challenge.filename))
232
+ FileUtils.mkdir_p path
257
233
  rescue SystemCallError => ex
258
234
  raise Error, ex.message
259
235
  end
@@ -263,20 +239,44 @@ module LetsCert
263
239
  File.write path, challenge.file_content
264
240
 
265
241
  challenge.request_verification
242
+ wait_for_verification challenge, domain
266
243
 
267
- status = 'pending'
268
- while(status == 'pending') do
269
- sleep(1)
270
- status = challenge.verify_status
271
- end
244
+ File.unlink path
245
+ end
246
+ end
272
247
 
273
- if status != 'valid'
274
- logger.warn { "#{domain} was not successfully verified!" }
275
- else
276
- logger.info { "#{domain} was successfully verified." }
277
- end
248
+ # Get challenges
249
+ # @param [Acme::Client] client
250
+ # @param [Hash] roots
251
+ # @return [Hash] key: domain, value: authorization
252
+ # @raise [Error] if any challenges does not support HTTP-01
253
+ def get_challenges(client, roots)
254
+ challenges = {}
255
+ roots.keys.each do |domain|
256
+ authorization = client.authorize(domain: domain)
257
+ challenges[domain] = authorization ? authorization.http01 : nil
258
+ end
278
259
 
279
- File.unlink path
260
+ logger.debug { 'Check all challenges are HTTP-01' }
261
+ if challenges.values.any?(&:nil?)
262
+ raise Error, 'CA did not offer http-01-only challenge. ' \
263
+ 'This client is unable to solve any other challenges.'
264
+ end
265
+
266
+ challenges
267
+ end
268
+
269
+ def wait_for_verification(challenge, domain)
270
+ status = 'pending'
271
+ while status == 'pending'
272
+ sleep(1)
273
+ status = challenge.verify_status
274
+ end
275
+
276
+ if status != 'valid'
277
+ logger.warn { "#{domain} was not successfully verified!" }
278
+ else
279
+ logger.info { "#{domain} was successfully verified." }
280
280
  end
281
281
  end
282
282
 
@@ -286,12 +286,34 @@ module LetsCert
286
286
  def renewal_necessary?(valid_min)
287
287
  now = Time.now.utc
288
288
  diff = (@cert.not_after - now).to_i
289
- logger.debug { "Certificate expires in #{diff}s on #{@cert.not_after}" +
289
+ logger.debug { "Certificate expires in #{diff}s on #{@cert.not_after}" \
290
290
  " (relative to #{now})" }
291
291
 
292
292
  diff < valid_min
293
293
  end
294
294
 
295
- end
295
+ # Generate new certificate for given domains with existing private key
296
+ # @param [Array<String>] domains
297
+ # @param [OpenSSL::PKey::PKey] pkey private key to use
298
+ # @return [OpenSSL::PKey::PKey] +pkey+
299
+ def generate_certificate_from_pkey(domains, pkey)
300
+ csr = Acme::Client::CertificateRequest.new(names: domains,
301
+ private_key: pkey)
302
+ acme_cert = client.new_certificate(csr)
303
+ @cert = acme_cert.x509
304
+ @chain = acme_cert.x509_chain
305
+
306
+ pkey
307
+ end
296
308
 
309
+ # Generate new certificate for given domains
310
+ # @param [Array<String>] domains
311
+ # @param [Integer] pkey_size size in bits for private key to generate
312
+ # @return [OpenSSL::PKey::PKey] generated private key
313
+ def generate_certificate(domains, pkey_size)
314
+ pkey = OpenSSL::PKey::RSA.generate(pkey_size)
315
+ generate_certificate_from_pkey domains, pkey
316
+ end
317
+
318
+ end
297
319
  end
@@ -19,7 +19,6 @@
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 'base64'
23
22
  require_relative 'loggable'
24
23
 
25
24
  module LetsCert
@@ -33,35 +32,36 @@ module LetsCert
33
32
  # @return [String]
34
33
  attr_reader :name
35
34
 
36
-
37
35
  # Registered plugins
38
- @@registered = {}
36
+ @registered = {}
39
37
 
40
- # Get empty data
41
- # @return [Hash] +{ account_key: nil, key: nil, cert: nil, chain: nil }+
42
- def self.empty_data
43
- { account_key: nil, key: nil, cert: nil, chain: nil }
44
- end
38
+ class << self
39
+
40
+ # Get registered plugins
41
+ # @return [Hash] keys are filenames and keys are instances of IOPlugin
42
+ # subclasses.
43
+ attr_reader :registered
45
44
 
46
- # Register a plugin
47
- # @param [Class] klass
48
- # @param [Array] args args to pass to +klass+ constructor
49
- # @return [IOPlugin]
50
- def self.register(klass, *args)
51
- plugin = klass.new(*args)
52
- if plugin.name =~ /[\/\\]/ or ['.', '..'].include?(plugin.name)
53
- raise Error, "plugin name should just be a file name, without path"
45
+ # Get empty data
46
+ # @return [Hash] +{ account_key: nil, key: nil, cert: nil, chain: nil }+
47
+ def empty_data
48
+ { account_key: nil, key: nil, cert: nil, chain: nil }
54
49
  end
55
50
 
56
- @@registered[plugin.name] = plugin
51
+ # Register a plugin
52
+ # @param [Class] klass
53
+ # @param [Array] args args to pass to +klass+ constructor
54
+ # @return [IOPlugin]
55
+ def register(klass, *args)
56
+ plugin = klass.new(*args)
57
+ if plugin.name =~ %r{[/\\]} or ['.', '..'].include?(plugin.name)
58
+ raise Error, 'plugin name should just be a file name, without path'
59
+ end
57
60
 
58
- klass
59
- end
61
+ @registered[plugin.name] = plugin
62
+ klass
63
+ end
60
64
 
61
- # Get registered plugins
62
- # @return [Hash] keys are filenames and keys are instances of IOPlugin subclasses.
63
- def self.registered
64
- @@registered
65
65
  end
66
66
 
67
67
  # @param [String] name
@@ -81,331 +81,13 @@ module LetsCert
81
81
 
82
82
  end
83
83
 
84
-
85
- # Mixin for IOPmugin subclasses that handle files
86
- # @author Sylvain Daubert
87
- module FileIOPluginMixin
88
-
89
- # Load data from file named +#name+
90
- # @return [Hash]
91
- def load
92
- logger.debug { "Loading #@name" }
93
-
94
- begin
95
- content = File.read(@name)
96
- rescue SystemCallError => ex
97
- if ex.is_a? Errno::ENOENT
98
- logger.info { "no #@name file" }
99
- return self.class.empty_data
100
- end
101
- raise
102
- end
103
-
104
- load_from_content(content)
105
- end
106
-
107
- # @abstract
108
- # @param [String] content
109
- # @return [Hash]
110
- def load_from_content(content)
111
- raise NotImplementedError
112
- end
113
-
114
- # Save data to file +#name+
115
- # @param [Hash] data
116
- # @return [void]
117
- def save_to_file(data)
118
- return if data.nil?
119
-
120
- logger.info { "saving #@name" }
121
- begin
122
- File.open(name, 'w') do |f|
123
- f.write(data)
124
- end
125
- rescue Errno => ex
126
- @logger.error { ex.message }
127
- raise Error, "Error when saving #@name"
128
- end
129
- end
130
-
131
- end
132
-
133
-
134
- # Mixin for IOPlugin subclasses that handle JWK
135
- # @author Sylvain Daubert
136
- module JWKIOPluginMixin
137
-
138
- # Encode string +data+ to base64
139
- # @param [String] data
140
- # @return [String]
141
- def urlsafe_encode64(data)
142
- Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
143
- end
144
-
145
- # Decode base64 string +data+
146
- # @param [String] data
147
- # @return [String]
148
- def urlsafe_decode64(data)
149
- Base64.urlsafe_decode64(data.sub(/[\s=]*\z/, ''))
150
- end
151
-
152
- # Load crypto data from JSON-encoded file
153
- # @param [String] data JSON-encoded data
154
- # @return [OpenSSL::PKey::PKey]
155
- def load_jwk(data)
156
- return nil if data.empty?
157
-
158
- h = JSON.parse(data)
159
- case h['kty']
160
- when 'RSA'
161
- pkey = OpenSSL::PKey::RSA.new
162
- %w(e n d p q).collect do |key|
163
- next if h[key].nil?
164
- value = OpenSSL::BN.new(urlsafe_decode64(h[key]), 2)
165
- pkey.send "#{key}=".to_sym, value
166
- end
167
- else
168
- raise Error, "unknown account key type '#{k['kty']}'"
169
- end
170
-
171
- pkey
172
- end
173
-
174
- # Dump crypto data (key) to a JSON-encoded string
175
- # @param [OpenSSL::PKey] key
176
- # @return [String]
177
- def dump_jwk(key)
178
- return {}.to_json if key.nil?
179
-
180
- h = { 'kty' => 'RSA' }
181
- case key
182
- when OpenSSL::PKey::RSA
183
- h['e'] = urlsafe_encode64(key.e.to_s(2)) if key.e
184
- h['n'] = urlsafe_encode64(key.n.to_s(2)) if key.n
185
- if key.private?
186
- h['d'] = urlsafe_encode64(key.d.to_s(2))
187
- h['p'] = urlsafe_encode64(key.p.to_s(2))
188
- h['q'] = urlsafe_encode64(key.q.to_s(2))
189
- end
190
- else
191
- raise Error, "only RSA keys are supported"
192
- end
193
- h.to_json
194
- end
195
- end
196
-
197
-
198
- # Account key IO plugin
199
- # @author Sylvain Daubert
200
- class AccountKey < IOPlugin
201
- include FileIOPluginMixin
202
- include JWKIOPluginMixin
203
-
204
- # @return [Hash] always get +true+ for +:account_key+ key
205
- def persisted
206
- { account_key: true }
207
- end
208
-
209
- # @return [Hash]
210
- def load_from_content(content)
211
- { account_key: load_jwk(content) }
212
- end
213
-
214
- # Save account key.
215
- # @param [Hash] data
216
- # @return [void]
217
- def save(data)
218
- save_to_file(dump_jwk(data[:account_key]))
219
- end
220
-
221
- end
222
- IOPlugin.register(AccountKey, 'account_key.json')
223
-
224
-
225
- # OpenSSL IOPlugin
226
- # @author Sylvain Daubert
227
- class OpenSSLIOPlugin < IOPlugin
228
-
229
- # @private Regular expression to discriminate PEM
230
- PEM_RE = /^-----BEGIN CERTIFICATE-----\n.*?\n-----END CERTIFICATE-----\n/m
231
-
232
- # @param [String] name filename
233
- # @param [:pem,:der] type
234
- def initialize(name, type)
235
- case type
236
- when :pem
237
- when :der
238
- else
239
- raise ArgumentError, "type should be :pem or :der"
240
- end
241
-
242
- @type = type
243
- super(name)
244
- end
245
-
246
- # Load key from raw +data+
247
- # @param [String] data
248
- # @return [OpenSSL::PKey]
249
- def load_key(data)
250
- OpenSSL::PKey::RSA.new data
251
- end
252
-
253
- # Dump key/cert data
254
- # @param [OpenSSL::PKey] key
255
- # @return [String]
256
- def dump_key(key)
257
- case @type
258
- when :pem
259
- key.to_pem
260
- when :der
261
- key.to_der
262
- end
263
- end
264
- alias :dump_cert :dump_key
265
-
266
- # Load certificate from raw +data+
267
- # @param [String] data
268
- # @return [OpenSSL::X509::Certificate]
269
- def load_cert(data)
270
- OpenSSL::X509::Certificate.new data
271
- end
272
-
273
-
274
- private
275
-
276
- # Split concatenated PEMs.
277
- # @param [String] data
278
- # @yield [String] pem
279
- def split_pems(data)
280
- my_data = data
281
- m = my_data.match(PEM_RE)
282
- while (m) do
283
- yield m[0]
284
- my_data = my_data[m.end(0)..-1]
285
- m = my_data.match(PEM_RE)
286
- end
287
- end
288
-
289
- end
290
-
291
-
292
- # Key file plugin
293
- # @author Sylvain Daubert
294
- class KeyFile < OpenSSLIOPlugin
295
- include FileIOPluginMixin
296
-
297
- # @return [Hash] always get +true+ for +:key+ key
298
- def persisted
299
- @persisted ||= { key: true }
300
- end
301
-
302
- # @return [Hash]
303
- def load_from_content(content)
304
- { key: load_key(content) }
305
- end
306
-
307
- # Save private key.
308
- # @param [Hash] data
309
- # @return [void]
310
- def save(data)
311
- save_to_file(dump_key(data[:key]))
312
- end
313
-
314
- end
315
- IOPlugin.register(KeyFile, 'key.pem', :pem)
316
- IOPlugin.register(KeyFile, 'key.der', :der)
317
-
318
-
319
- # Chain file plugin
320
- # @author Sylvain Daubert
321
- class ChainFile < OpenSSLIOPlugin
322
- include FileIOPluginMixin
323
-
324
- # @return [Hash] always get +true+ for +:chain+ key
325
- def persisted
326
- @persisted ||= { chain: true }
327
- end
328
-
329
- # @return [Hash]
330
- def load_from_content(content)
331
- chain = []
332
- split_pems(content) do |pem|
333
- chain << load_cert(pem)
334
- end
335
- { chain: chain }
336
- end
337
-
338
- # Save chain.
339
- # @param [Hash] data
340
- # @return [void]
341
- def save(data)
342
- save_to_file(data[:chain].map { |c| dump_cert(c) }.join)
343
- end
344
-
345
- end
346
- IOPlugin.register(ChainFile, 'chain.pem', :pem)
347
-
348
-
349
- # Fullchain file plugin
350
- # @author Sylvain Daubert
351
- class FullChainFile < ChainFile
352
-
353
- # @return [Hash] always get +true+ for +:cert+ and +:chain+ keys
354
- def persisted
355
- @persisted ||= { cert: true, chain: true }
356
- end
357
-
358
- # Load full certificate chain
359
- # @return [Hash]
360
- def load
361
- data = super
362
- if data[:chain].nil? or data[:chain].empty?
363
- cert = nil
364
- chain = []
365
- else
366
- cert = data[:chain].shift
367
- chain = data[:chain]
368
- end
369
-
370
- { account_key: data[:account_key], key: data[:key], cert: cert, chain: chain }
371
- end
372
-
373
- # Save fullchain.
374
- # @param [Hash] data
375
- # @return [void]
376
- def save(data)
377
- super(account_key: data[:account_key], key: data[:key], cert: nil,
378
- chain: [data[:cert]] + data[:chain])
379
- end
380
-
381
- end
382
- IOPlugin.register(FullChainFile, 'fullchain.pem', :pem)
383
-
384
-
385
- # Cert file plugin
386
- # @author Sylvain Daubert
387
- class CertFile < OpenSSLIOPlugin
388
- include FileIOPluginMixin
389
-
390
- # @return [Hash] always get +true+ for +:cert+ key
391
- def persisted
392
- @persisted ||= { cert: true }
393
- end
394
-
395
- # @return [Hash]
396
- def load_from_content(content)
397
- { cert: load_cert(content) }
398
- end
399
-
400
- # Save certificate.
401
- # @param [Hash] data
402
- # @return [void]
403
- def save(data)
404
- save_to_file(dump_cert(data[:cert]))
405
- end
406
-
407
- end
408
- IOPlugin.register(CertFile, 'cert.pem', :pem)
409
- IOPlugin.register(CertFile, 'cert.der', :der)
410
-
411
84
  end
85
+
86
+ require_relative 'io_plugins/file_io_plugin_mixin'
87
+ require_relative 'io_plugins/jwk_io_plugin_mixin'
88
+ require_relative 'io_plugins/openssl_io_plugin'
89
+ require_relative 'io_plugins/account_key'
90
+ require_relative 'io_plugins/key_file'
91
+ require_relative 'io_plugins/chain_file'
92
+ require_relative 'io_plugins/full_chain_file'
93
+ require_relative 'io_plugins/cert_file'