blix-letsencrypt 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51750c9fbd2104f084d61f0a07d3bc0be338cacdb935850b35cf3f427659f1c9
4
+ data.tar.gz: 0ec555ca56aa54faad52f135cb0b5e1bbb87a9c2420d783001a2bec9d64913b0
5
+ SHA512:
6
+ metadata.gz: 47f6145bfc9b098bfefd2dae95c0124a257060a473ec6922b2e5a9f8577b5623ab4c96ace1b3910abe59b59354c6e96f56968e09483b195deeae476d8988b106
7
+ data.tar.gz: 75e034dc207dba70fe331275c071a66c067ac07bf0b005a5f10c285aa4b272ba74af501d591e498e26d9d63dccc204d35b7c407ea4bbfbf4db42bfac0e783a52
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Clive Andrews
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # letsencrypt
2
+
3
+ a command line utility for managing letsencrypt ssl certificates.
4
+
5
+
6
+ ## depends
7
+
8
+ ruby >= 2.4
9
+
10
+ ## install
11
+
12
+ gem install blix-letsencrypt
13
+
14
+ ## command options:
15
+
16
+ Usage: letsencrypt [options]
17
+ -c, --create Create ACME private key
18
+ -k, --key=FILE ACME private key file
19
+ -e, --email=EMAIL your contact email
20
+ -d, --domain=DOMAIN domain name for certificate
21
+ --challenge_dir=CDIR challenge file directory
22
+ --ssl_dir=SSLDIR ssl certificate file directory
23
+ --ssl_key=SSLKEY ssl private key file
24
+ -t, --test enable test mode
25
+ --force force update even if not expired
26
+ -l, --logfile=LOGFILE log to file
27
+ -h, --hook=HOOK script to run on renewal
28
+
29
+
30
+ ## conventions used
31
+
32
+ * the private key is called `privkey.pem`
33
+
34
+
35
+ * the certificate is called `cert.pem` and is placed in a directory named
36
+ after the main (first) domain name.
37
+
38
+ ## create letsencrypt certificates
39
+
40
+ * create directory to hold your keys and certificates .. eg:
41
+
42
+ mkdir /etc/letsencrypt/account
43
+ mkdir /etc/letsencrypt/ssl
44
+
45
+ * create directory to serve challenges from.. eg:
46
+
47
+ mkdir /srv/certbot/.well-known
48
+
49
+ * create a ssl private key if you do not yet have one.. eg:
50
+
51
+ openssl genrsa -out /etc/letsencrypt/ssl/privkey.pem 2048
52
+
53
+ * update your webserver to serve the challengefiles eg for nginx..:
54
+
55
+ location /.well-known {
56
+ alias /srv/certbot/.well-known;
57
+ add_header "Content-Type" "text/plain";
58
+ break;
59
+ }
60
+
61
+ * now create your certificate
62
+
63
+ letsencrypt --key=/etc/letsencrypt/account/key.pem -d"example.com www.example.com" --challenge_dir="/srv/certbot/.well-known" --ssl_dir="/etc/letsencrypt/ssl" --logfile=/var/log/letsencrypt.log --create
64
+
65
+ * hopefully your certificate has be created so update your webserver to use it...
66
+
67
+ ssl_certificate /etc/letsencrypt/ssl/example.com/cert.pem;
68
+ ssl_certificate_key /etc/letsencrypt/ssl/privkey.pem;
69
+
70
+ * reload the webserver and check all is well.
71
+
72
+ ## auto renew letsencrypt certificates
73
+
74
+ the letsencrypt certificates are valid for 90 days. it is recommended that you
75
+ run a script every day to check if the certificates are due for renewal.
76
+
77
+ * create two shell scrips, one to renew the certificates and another to
78
+ restart the webserver.
79
+
80
+ * ensure that both scripts are executable..
81
+ * copy the first script to /etc/cron.daily directory.
82
+ * link the second script to the `--hook` option of the letsencrypt command.
83
+
84
+ eg:
85
+
86
+ cat /etc/cron.daily/renew_ssl
87
+
88
+ !/bin/sh
89
+ /opt/ruby-2.6.4/bin/letsencrypt --key=/etc/letsencrypt/account/key.pem \
90
+ -d"example.com www.example.com" \
91
+ --challenge_dir="/srv/certbot/.well-known" --ssl_dir="/etc/letsencrypt/ssl" \
92
+ --logfile=/var/log/letsencrypt.log \
93
+ --hook=/root/bin/reload_nginx
94
+
95
+ cat /root/bin/reload_nginx
96
+
97
+ !/bin/sh
98
+ /sbin/nginx -t && /sbin/nginx -sreload
data/bin/letsencrypt ADDED
@@ -0,0 +1,3 @@
1
+ #!/env/ruby
2
+
3
+ require_relative '../lib/blix/letsencrypt'
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this is a script for renewing letsencrypt certicates. .. to automate the
4
+ # process create a shell script to run this command and a shell script to
5
+ # restart the web server
6
+ #
7
+ # ensure that both scripts are executable..
8
+ # copy the first script to /etc/cron.daily directory.
9
+ # link the second script to the --hook option of this ruby command.
10
+ #
11
+ # eg:
12
+ #
13
+ # cat /etc/cron.daily/renew_ssl
14
+ #
15
+ # #!/bin/sh
16
+ # /opt/ruby-2.6.4/bin/ruby /root/bin/letsencrypt.rb --key=/etc/letsencrypt/account/key.pem \
17
+ # -d"example.com www.example.com" \
18
+ # --challenge_dir="/tmp/certbot/public_html/.well-known" \
19
+ # --ssl_dir="/etc/letsencrypt/ssl" \
20
+ # --logfile=/var/log/letsencrypt.log \
21
+ # --hook=/root/bin/reload_nginx
22
+ #
23
+ #
24
+ # cat /root/bin/reload_nginx
25
+ # #!/bin/sh
26
+ # /opt/nginx-1.2.2/sbin/nginx -t && /opt/nginx-1.2.2/sbin/nginx -sreload
27
+ #
28
+ #
29
+ # copyright Clive Andrews 2020
30
+ #
31
+ # licence MIT
32
+
33
+
34
+
35
+ require 'openssl'
36
+ require 'optparse'
37
+ require 'acme-client'
38
+ require 'logger'
39
+
40
+ # location /.well-known{
41
+ # rewrite ^\.well-known(.*)$ $1 break;
42
+ # alias /srv/letsencrypt/challenge;
43
+ # add_header Content-Type text/plain;
44
+ # }
45
+
46
+ CHALLENGE_DIR = '/srv/letsencrypt'
47
+ SSL_DIR = '/etc/letsencrypt/cert'
48
+ SSL_CERT = 'cert.pem'
49
+ SSL_KEY = 'privkey.pem'
50
+ ACME_DIR_S = 'https://acme-staging-v02.api.letsencrypt.org/directory' # letsencrypt
51
+ ACME_DIR_L = 'https://acme-v02.api.letsencrypt.org/directory' # letsencrypt live
52
+
53
+ TIMEOUT = 60
54
+
55
+ # check if certificate is about to expire.
56
+ def certificate_expiry_is_soon(ssl_dir, certificate_file, days = 30)
57
+ file = File.join(ssl_dir, certificate_file)
58
+ return true unless File.file?(file)
59
+
60
+ cert = OpenSSL::X509::Certificate.new(File.read(file))
61
+ cert.not_after < Time.now + days * (24 * 60 * 60)
62
+ end
63
+
64
+ def tidy_challenge_file(file)
65
+ file = file[1..-1] if file[0, 1] == '/'
66
+ str = '.well-known'
67
+ file = file[str.length..-1] if file[0, str.length] == str
68
+ file
69
+ end
70
+
71
+ # write the challenge file and ensure that intermediate dirs exist
72
+ def write_file(dir, file, content)
73
+ file = tidy_challenge_file(file)
74
+ parts = file.split('/')
75
+ last_index = parts.length - 1
76
+ path = nil
77
+ parts.each_with_index do |_part, idx|
78
+ path = File.join(dir, *parts[0, idx + 1])
79
+ if idx == last_index # the file name
80
+ File.write(path, content)
81
+ else
82
+ if File.file?(path)
83
+ raise "invalid challenge path: #{path}"
84
+ elsif File.directory?(path)
85
+
86
+ else
87
+ Dir.mkdir(path)
88
+ end
89
+ end
90
+ end
91
+ path
92
+ end
93
+
94
+ def backup_file(dir, file)
95
+ orig_path = File.join(dir, file)
96
+ orig_file = File.basename(orig_path)
97
+ orig_dir = File.dirname(orig_path)
98
+
99
+ raise "backup file does not exist:#{orig_path}" unless File.exist?(orig_path)
100
+
101
+ seq = 1
102
+ loop do
103
+ prefix = '%04d_' % seq
104
+ new_file = prefix + orig_file
105
+ new_path = File.join(orig_dir, new_file)
106
+ if File.exist?(new_path)
107
+ seq += 1
108
+ next
109
+ else
110
+ content = File.read(orig_path)
111
+ File.write(new_path, content)
112
+ break new_path
113
+ end
114
+ end
115
+ end
116
+
117
+ # delete the challenge files
118
+ def remove_file(dir, file)
119
+ file = tidy_challenge_file(file)
120
+ path = File.join(dir, file)
121
+ File.unlink(path) if File.file?(path)
122
+ true
123
+ end
124
+
125
+ # perform an authorization by creating the challenge files
126
+ # and waiting for validation to occur.
127
+ def perform_authorization(challenge_dir, authorization)
128
+ http_challenge = authorization.http
129
+
130
+ # write the challenge to file
131
+
132
+ http_challenge.content_type # => 'text/plain'
133
+ http_challenge.file_content # => example_token.TO1xJ0UDgfQ8WY5zT3txynup87UU3PhcDEIcuPyw4QU
134
+ http_challenge.filename # => '.well-known/acme-challenge/example_token'
135
+ http_challenge.token
136
+
137
+ challenge_file = tidy_challenge_file(http_challenge.filename)
138
+ challenge_path = write_file(challenge_dir, challenge_file, http_challenge.file_content)
139
+
140
+ puts "challenge has been written to :#{challenge_path}"
141
+
142
+ # now wait for the challenge ..
143
+
144
+ http_challenge.request_validation
145
+ timeout_time = Time.now + TIMEOUT
146
+
147
+ while http_challenge.status == 'pending'
148
+ if Time.now > timeout_time
149
+ remove_file(challenge_dir, challenge_file)
150
+ raise 'Challenge timeout'
151
+ end
152
+ sleep(2)
153
+ http_challenge.reload
154
+ end
155
+
156
+ remove_file(challenge_dir, challenge_file)
157
+ raise 'challenge failed' unless http_challenge.status == 'valid' # => 'valid'
158
+ end
159
+
160
+ # handle options here
161
+ options = {}
162
+ OptionParser.new do |opts|
163
+ # opts.banner = "Usage: example.rb [options]"
164
+
165
+ opts.on('-c', '--create', 'Create ACME private key') do |_v|
166
+ options[:create] = true
167
+ end
168
+
169
+ opts.on('-k', '--key=FILE', 'ACME private key file') do |v|
170
+ options[:key] = v
171
+ end
172
+
173
+ opts.on('-e', '--email=EMAIL', 'your contact email') do |v|
174
+ options[:email] = v
175
+ end
176
+
177
+ opts.on('-d', '--domain=DOMAIN', 'domain name for certificate') do |v|
178
+ options[:site] = v
179
+ end
180
+
181
+ opts.on('--challenge_dir=CDIR', 'challenge file directory') do |v|
182
+ options[:challenge_dir] = v
183
+ end
184
+
185
+ opts.on('--ssl_dir=SSLDIR', 'ssl certificate file directory') do |v|
186
+ options[:ssl_dir] = v
187
+ end
188
+
189
+ opts.on('--ssl_key=SSLKEY', 'ssl private key file') do |v|
190
+ options[:ssl_dir] = v
191
+ end
192
+
193
+ opts.on('-t', '--test', 'enable test mode') do |v|
194
+ options[:test] = v
195
+ end
196
+
197
+ opts.on('--force', 'force update even if not expired') do |v|
198
+ options[:force] = v
199
+ end
200
+
201
+ opts.on('-l', '--logfile=LOGFILE', 'log to file') do |v|
202
+ options[:logfile] = v
203
+ end
204
+
205
+ opts.on('-h', '--hook=HOOK', 'script to run on renewal') do |v|
206
+ options[:hook] = v
207
+ end
208
+ end.parse!
209
+
210
+ # check that we have sensible values four our options before we start the
211
+ # whole [rpcess]
212
+ domains = options[:site].to_s
213
+ domains.gsub!(',', ' ')
214
+ domains.gsub!(';', ' ')
215
+ domains.gsub!(/ +/, ' ')
216
+ names = domains.split(' ')
217
+ site = names[0]
218
+ ssl_dir = File.expand_path(options[:ssl_dir] || SSL_DIR)
219
+ challenge_dir = File.expand_path(options[:challenge_dir] || CHALLENGE_DIR)
220
+ ssl_key_path = options[:ssl_key] || File.join(ssl_dir, SSL_KEY)
221
+ hook_path = options[:hook]
222
+
223
+ raise 'domain name missing' unless site
224
+ raise 'invalid challenge directory' unless File.directory?(challenge_dir)
225
+ raise 'invalid ssl certificate directory' unless File.directory?(ssl_dir)
226
+ raise "ssl private key invalid:#{ssl_key_path}" unless File.file?(ssl_key_path)
227
+ raise "script missing or not executable:#{hook_path}" unless !hook_path || File.executable?(hook_path)
228
+
229
+ certificate_file = File.join(site, SSL_CERT)
230
+ acme_key = File.expand_path(options[:key])
231
+ ssl_key = OpenSSL::PKey::RSA.new(File.read(ssl_key_path))
232
+
233
+ logger = Logger.new(options[:logfile] || STDOUT)
234
+
235
+ # check to see if the certificate is due for renewal. if the
236
+ # certificate expires within 30 days then renew otherwise exit
237
+ # unless the force option is set.
238
+ unless options[:force]
239
+ unless certificate_expiry_is_soon(ssl_dir, certificate_file)
240
+ logger.info "certificate:#{certificate_file} not due for renewal"
241
+ exit
242
+ end
243
+ end
244
+
245
+ # first read our private key..
246
+ if File.file?(acme_key)
247
+ private_key = OpenSSL::PKey::RSA.new(File.read(acme_key)) # read
248
+ elsif options[:create]
249
+ private_key = OpenSSL::PKey::RSA.new(4096) # generate
250
+ File.write(acme_key, private_key)
251
+ else
252
+ raise "acme key file:#{acme_key} not found"
253
+ end
254
+
255
+ client = if options[:test]
256
+ Acme::Client.new(:private_key => private_key, :directory => ACME_DIR_S)
257
+ else
258
+ Acme::Client.new(:private_key => private_key, :directory => ACME_DIR_L)
259
+ end
260
+
261
+ # ensure that we hav an account.
262
+ kid = begin
263
+ client.kid
264
+ rescue StandardError
265
+ nil
266
+ end
267
+
268
+ unless kid
269
+ email = options[:email] || begin
270
+ print('enter your email:')
271
+ gets.strip
272
+ end
273
+ raise "invalid email:#{email}" unless email && email =~ /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/
274
+
275
+ account = client.new_account(:contact => "mailto:#{email}", :terms_of_service_agreed => true)
276
+ end
277
+
278
+ puts 'account found..'
279
+
280
+ # now set up our order ..
281
+
282
+ puts "setting up domain #{site}:#{names.join(',')}:"
283
+
284
+ order = client.new_order(:identifiers => names)
285
+ order.authorizations.each { |auth| perform_authorization(challenge_dir, auth) }
286
+
287
+ # download the certificate
288
+
289
+ puts 'now obtaining certificate..'
290
+
291
+ csr = Acme::Client::CertificateRequest.new(:private_key => ssl_key, :names => names, :subject => { :common_name => site })
292
+ order.finalize(:csr => csr)
293
+
294
+ timeout_time = Time.now + TIMEOUT
295
+ while order.status == 'processing'
296
+ raise 'certificate timeout' if Time.now > timeout_time
297
+
298
+ sleep(1)
299
+ order.reload
300
+ end
301
+
302
+ # now write the certificate to file
303
+
304
+ # backup the old file if it exists.
305
+
306
+ if File.file?(File.join(ssl_dir, certificate_file))
307
+ backup_file(ssl_dir, certificate_file)
308
+ end
309
+
310
+ ssl_path = write_file(ssl_dir, certificate_file, order.certificate)
311
+ logger.info "ssl certificate has been written to :#{ssl_path}"
312
+
313
+ if hook_path
314
+ if system(hook_path)
315
+ logger.info "script :#{hook_path} succeeded"
316
+ else
317
+ logger.info "script :#{hook_path} failed !!"
318
+ end
319
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blix-letsencrypt
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Clive Andrews
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: acme-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Command line utilities for letsencrypt
42
+ email:
43
+ - gems@realitybites.eu
44
+ executables:
45
+ - letsencrypt
46
+ extensions: []
47
+ extra_rdoc_files:
48
+ - README.md
49
+ - LICENSE
50
+ files:
51
+ - LICENSE
52
+ - README.md
53
+ - bin/letsencrypt
54
+ - lib/blix/letsencrypt.rb
55
+ homepage: https://github.com/realbite/blix-letsencrypt
56
+ licenses:
57
+ - MIT
58
+ metadata: {}
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.1.4
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: Command line utilities for managing letsencrypt ssl certificates
78
+ test_files: []