letscert 0.2.4 → 0.3.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: bc09ae0885a6a70be1512b3819c07899cd84ecab
4
- data.tar.gz: 124c5daa4aab3d985d86a1d0fada0c65db0269a2
3
+ metadata.gz: 4d86e5665c2e35534ded942c0603cb47b927e05e
4
+ data.tar.gz: f2d9fa631de0f0aee3ec3e051f6c2a78ff85bdf1
5
5
  SHA512:
6
- metadata.gz: cf9e90399b2277647f72939d7669a0d562add40b1cd3a4a89346d215fe09e7a97bf0eed125342c30bed63096c92945f932276df3d82dc532a83221238cfba0e5
7
- data.tar.gz: ce17392296e10b1d45fff709662ec3de25c53f658b28140a895706b65fe8f0f44959a87cc418d33155246af98afe36639623a503f54a77dc015d00462e1b65ff
6
+ metadata.gz: 103bc74e75d1a69f004951cbd681768a3efd11520c59ed38cc389a81aaf750ec097c94d84a57a9898fc298237309bc84f48d27dc5dd18d371d952ba48eab09e1
7
+ data.tar.gz: 717dfaff074bd6c3809045e56ff51e7518c2c0f2bc2ce5990e8777121b3ea6dda3f67e4b90dec891534dd0f9e4cba60a3f72bd36e2e26e62b18de77ee23b89e3
data/README.md CHANGED
@@ -9,32 +9,47 @@ in Ruby.
9
9
 
10
10
  # Usage
11
11
 
12
- Generate a key pair and get signed certificate:
12
+ ## Generate a key pair and get signed certificate:
13
+ With full chain support (`fullchain.pem` file will contain all certificates):
13
14
  ```bash
14
- letscert -d example.com:/var/www/example.com/html -f account_key.json -f key.pem -f cert.pem -f fullchain.pem
15
+ letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld -f account_key.json -f key.pem -f fullchain.pem
15
16
  ```
16
-
17
- Generate a key pair and get a signed certificate for multi-domains:
17
+ else (certificate for example.com is in `cert.pem` file, rest of certification chain
18
+ is in `chain.pem`):
18
19
  ```bash
19
- letscert -d example.com -d www.example.com --default_root /var/www/html -f account_key.json -f key.pem -f cert.pem -f fullchain.pem
20
+ 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
20
21
  ```
21
22
 
22
23
  Commands are the sames for certificate renewal.
23
24
 
25
+
26
+ ## Generate a key pair and get a signed certificate for multi-domains:
27
+ Generate a single certificate for `example.com` and `www.example.com`:
28
+ ```bash
29
+ 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
+ ```
31
+
32
+ Command is the same for certificate renewal.
33
+
34
+ ## Revoke a key pair:
35
+ From directory where are stored `account_key.json` and `cert.pem` or `fullchain.pem`:
36
+ ```bash
37
+ letscert -d example.com:/var/www/example.com/html --email my.name@domain.tld --revoke
38
+ ```
39
+
40
+
24
41
  # What `letscert` do
25
42
 
26
- * Automagically a new ACME account if needed.
43
+ * Automagically create a new ACME account if needed.
27
44
  * Issue new certificate if no previous one found.
28
45
  * Renew certificate only if needed.
29
- * Only `http-01` challenge supported. An existing web server must be alreay running. `letscert` should have write access to `${webroot}/.well-known/acme-challenge`.
46
+ * Only `http-01` challenge supported. An existing web server must be alreay running.
47
+ `letscert` should have write access to `${webroot}/.well-known/acme-challenge`.
30
48
  * Crontab friendly: no promts.
31
49
  * No configuration file.
32
- * Support multiple domains with multiple roots. Always create a single certificate per un
33
- (ie a certificate may have multiple SANs).
50
+ * Support multiple domains with multiple roots. Always create a single certificate per
51
+ run (ie a certificate may have multiple SANs).
34
52
  * As `simp_le`, check the exit code to known if a renewal has happened:
35
53
  * 0 if certificate data was created or updated;
36
54
  * 1 if renewal not necessary;
37
55
  * 2 in case of errors.
38
-
39
- # Todo
40
- Add support to revocation.
data/lib/letscert.rb CHANGED
@@ -24,7 +24,7 @@
24
24
  module LetsCert
25
25
 
26
26
  # Letscert version number
27
- VERSION = '0.2.4'
27
+ VERSION = '0.3.0'
28
28
 
29
29
 
30
30
  # Base error class
@@ -24,34 +24,31 @@ require_relative 'loggable'
24
24
  module LetsCert
25
25
 
26
26
  # Class to handle ACME operations on certificates
27
+ # @author Sylvain Daubert
27
28
  class Certificate
28
29
  include Loggable
29
30
 
30
- # Revoke certificates
31
- # @param [Array<String>] files
32
- def self.revoke(files)
33
- logger.warn "revoke not yet implemented"
31
+
32
+ # @param [OpenSSL::X509::Certificate,nil] cert
33
+ def initialize(cert)
34
+ @cert = cert
34
35
  end
35
36
 
36
37
  # Get a new certificate, or renew an existing one
37
- # @param [Hash] options
38
+ # @param [OpenSSL::PKey::PKey] account_key private key to authenticate to ACME server
39
+ # @param [OpenSSL::PKey::PKey] key private key from which make a certificate
38
40
  # @param [Hash] data
39
- def self.get(options, data)
40
- new.get options, data
41
- end
42
-
43
- def get(options, data)
41
+ def get(account_key, key, options)
44
42
  logger.info {"create key/cert/chain..." }
45
43
  roots = compute_roots(options)
46
44
  logger.debug { "webroots are: #{roots.inspect}" }
47
45
 
48
- client = get_acme_client(data[:account_key], options)
46
+ client = get_acme_client(account_key, options)
49
47
 
50
48
  do_challenges client, roots
51
49
 
52
- if options[:reuse_key] and !data[:key].nil?
50
+ if options[:reuse_key] and !key.nil?
53
51
  logger.info { 'Reuse existing private key' }
54
- key = data[:key]
55
52
  else
56
53
  logger.info { 'Generate new private key' }
57
54
  key = OpenSSL::PKey::RSA.generate(options[:cert_key_size])
@@ -68,6 +65,58 @@ module LetsCert
68
65
  end
69
66
  end
70
67
 
68
+ # Revoke certificate
69
+ # @param [OpenSSL::PKey::PKey] account_key
70
+ # @return [Boolean]
71
+ def revoke(account_key, options)
72
+ if @cert.nil?
73
+ raise Error, 'no certification data to revoke'
74
+ end
75
+
76
+ client = get_acme_client(account_key, options)
77
+ begin
78
+ result = client.revoke_certificate(@cert)
79
+ rescue Exception => ex
80
+ raise
81
+ end
82
+
83
+ if result
84
+ logger.info { 'certificate is revoked' }
85
+ else
86
+ logger.warn { 'certificate is not revoked!' }
87
+ end
88
+
89
+ result
90
+ end
91
+
92
+ # Check if certificate is still valid for at least +valid_min+ seconds.
93
+ # Also checks that +domains+ are certified by certificate.
94
+ # @param [Array<String>] domains list of certificate domains
95
+ # @param [Integer] valid_min
96
+ # @return [Boolean]
97
+ def valid?(domains, valid_min=0)
98
+ if @cert.nil?
99
+ logger.debug { 'no existing certificate' }
100
+ return false
101
+ end
102
+
103
+ subjects = []
104
+ @cert.extensions.each do |ext|
105
+ if ext.oid == 'subjectAltName'
106
+ subjects += ext.value.split(/,\s*/).map { |s| s.sub(/DNS:/, '') }
107
+ end
108
+ end
109
+ logger.debug { "cert SANs: #{subjects.join(', ')}" }
110
+
111
+ # Check all domains are subjects of certificate
112
+ unless domains.all? { |domain| subjects.include? domain }
113
+ raise Error, "At least one domain is not declared as a certificate subject." +
114
+ "Backup and remove existing cert if you want to proceed"
115
+ end
116
+
117
+ !renewal_necessary?(valid_min)
118
+ end
119
+
71
120
 
72
121
  private
73
122
 
@@ -206,6 +255,19 @@ module LetsCert
206
255
  end
207
256
  end
208
257
 
258
+ # Check if a renewal is necessary for +cert+
259
+ # @param [OpenSSL::X509::Certificate] cert
260
+ # @param [Number] valid_min minimum validity in seconds to ensure
261
+ # @return [Boolean]
262
+ def renewal_necessary?(valid_min)
263
+ now = Time.now.utc
264
+ diff = (@cert.not_after - now).to_i
265
+ logger.debug { "Certificate expires in #{diff}s on #{@cert.not_after}" +
266
+ " (relative to #{now})" }
267
+
268
+ diff < valid_min
269
+ end
270
+
209
271
  end
210
272
 
211
273
  end
@@ -130,12 +130,19 @@ module LetsCert
130
130
  Certificate.logger = @logger
131
131
 
132
132
  begin
133
- if @options[:revoke]
134
- Certificate.revoke @options[:files]
135
- RETURN_OK
136
- elsif @options[:domains].empty?
133
+ if @options[:domains].empty?
137
134
  raise Error, "At leat one domain must be given with --domain option.\n" +
138
135
  "Try 'letscert --help' for more information."
136
+ end
137
+
138
+ if @options[:revoke]
139
+ data = load_data_from_disk(IOPlugin.registered.keys)
140
+ certificate = Certificate.new(data[:cert])
141
+ if certificate.revoke(data[:account_key], @options)
142
+ RETURN_OK
143
+ else
144
+ RETURN_ERROR
145
+ end
139
146
  else
140
147
  # Check all components are covered by plugins
141
148
  persisted = IOPlugin.empty_data
@@ -152,12 +159,13 @@ module LetsCert
152
159
 
153
160
  data = load_data_from_disk(@options[:files])
154
161
 
155
- if valid_existing_cert(data[:cert], @options[:domains], @options[:valid_min])
162
+ certificate = Certificate.new(data[:cert])
163
+ if certificate.valid?(@options[:domains], @options[:valid_min])
156
164
  @logger.info { 'no need to update cert' }
157
165
  RETURN_OK
158
166
  else
159
167
  # update/create cert
160
- Certificate.get @options, data
168
+ certificate.get data[:account_key], data[:key], @options
161
169
  RETURN_OK_CERT
162
170
  end
163
171
  end
@@ -197,7 +205,7 @@ module LetsCert
197
205
  @options[:domains] << domain
198
206
  end
199
207
 
200
- opts.on('--default_root PATH', 'Default webroot path',
208
+ opts.on('--default-root PATH', 'Default webroot path',
201
209
  'Use for domains without PATH part.') do |path|
202
210
  @options[:default_root] = path
203
211
  end
@@ -305,48 +313,6 @@ module LetsCert
305
313
  all_data
306
314
  end
307
315
 
308
- # Check if +cert+ exists and is always valid
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]
314
- def valid_existing_cert(cert, domains, valid_min)
315
- if cert.nil?
316
- @logger.debug { 'no existing cert' }
317
- return false
318
- end
319
-
320
- subjects = []
321
- cert.extensions.each do |ext|
322
- if ext.oid == 'subjectAltName'
323
- subjects += ext.value.split(/,\s*/).map { |s| s.sub(/DNS:/, '') }
324
- end
325
- end
326
- @logger.debug { "cert SANs: #{subjects.join(', ')}" }
327
-
328
- # Check all domains are subjects of certificate
329
- unless domains.all? { |domain| subjects.include? domain }
330
- raise Error, "At least one domain is not declared as a certificate subject." +
331
- "Backup and remove existing cert if you want to proceed"
332
- end
333
-
334
- !renewal_necessary?(cert, valid_min)
335
- end
336
-
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]
341
- def renewal_necessary?(cert, valid_min)
342
- now = Time.now.utc
343
- diff = (cert.not_after - now).to_i
344
- @logger.debug { "Certificate expires in #{diff}s on #{cert.not_after}" +
345
- " (relative to #{now})" }
346
-
347
- diff < valid_min
348
- end
349
-
350
316
  end
351
317
 
352
318
  end
@@ -0,0 +1,56 @@
1
+ require_relative 'spec_helper'
2
+
3
+ module LetsCert
4
+
5
+ describe Certificate do
6
+
7
+ before(:all) { Certificate.logger = Logger.new('/dev/null') }
8
+
9
+ context '#valid?' do
10
+
11
+ before(:all) do
12
+ root_key = OpenSSL::PKey::RSA.new(512)
13
+
14
+ @domains = %w(example.org www.example.org)
15
+
16
+ key = OpenSSL::PKey::RSA.new(512)
17
+ @cert = OpenSSL::X509::Certificate.new
18
+ @cert.version = 2
19
+ @cert.serial = 2
20
+ @cert.issuer = OpenSSL::X509::Name.parse "/DC=letscert/CN=CA"
21
+ @cert.public_key = key.public_key
22
+ @cert.not_before = Time.now
23
+ # 20 days validity
24
+ @cert.not_after = @cert.not_before + 20 * 24 * 60 * 60
25
+ ef = OpenSSL::X509::ExtensionFactory.new
26
+ ef.subject_certificate = @cert
27
+ @domains.each do |domain|
28
+ @cert.add_extension(ef.create_extension('subjectAltName',
29
+ "DNS:#{domain}",
30
+ false))
31
+ end
32
+ @cert.sign(root_key, OpenSSL::Digest::SHA256.new)
33
+ end
34
+
35
+ let(:certificate) { Certificate.new(@cert) }
36
+
37
+ it 'checks whether a certificate is valid given a minimum valid duration' do
38
+ expect(certificate.valid?(@domains)).to be(true)
39
+ expect(certificate.valid?(@domains, 19)).to be(true)
40
+ expect(certificate.valid?(@domains, 21 * 24 * 3600)).to be(false)
41
+ end
42
+
43
+ it 'raises whether a certificate does not validate a given domain' do
44
+ expect(certificate.valid?(@domains)).to be(true)
45
+ expect(certificate.valid?(@domains[0, 1])).to be(true)
46
+
47
+ domains = @domains + %w(another.tld)
48
+ expect { certificate.valid?(domains) }.to raise_error(LetsCert::Error)
49
+ expect { certificate.valid?(%w(another.tld)) }.to raise_error(LetsCert::Error)
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'spec_helper'
2
+
3
+ module LetsCert
4
+
5
+ describe Certificate do
6
+ end
7
+
8
+ end
data/tasks/gem.rake CHANGED
@@ -21,11 +21,13 @@ EOF
21
21
  s.files = files
22
22
  s.executables = ['letscert']
23
23
 
24
+ s.required_ruby_version = '>= 2.1.0'
25
+
24
26
  s.add_dependency 'acme-client', '~>0.3.0'
25
27
  s.add_dependency 'json-jwt', '~>1.5'
26
- s.add_dependency 'yard', '~>0.8'
27
28
 
28
- #s.add_development_dependency 'rspec', '~>3.4'
29
+ s.add_development_dependency 'rspec', '~>3.4'
30
+ s.add_development_dependency 'yard', '~>0.8'
29
31
  end
30
32
 
31
33
 
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
4
+ version: 0.3.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-14 00:00:00.000000000 Z
11
+ date: 2016-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acme-client
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.4'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: yard
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -45,7 +59,7 @@ dependencies:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
61
  version: '0.8'
48
- type: :runtime
62
+ type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
@@ -73,6 +87,8 @@ files:
73
87
  - spec/account_key.json
74
88
  - spec/cert.der
75
89
  - spec/cert.pem
90
+ - spec/certificate_spec.rb
91
+ - spec/certificate_spec.rb~
76
92
  - spec/chain.pem
77
93
  - spec/fullchain.pem
78
94
  - spec/io_plugin_spec.rb
@@ -96,7 +112,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
112
  requirements:
97
113
  - - ">="
98
114
  - !ruby/object:Gem::Version
99
- version: '0'
115
+ version: 2.1.0
100
116
  required_rubygems_version: !ruby/object:Gem::Requirement
101
117
  requirements:
102
118
  - - ">="