acme-pki 0.1.3 → 0.2.3
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 +5 -5
- data/.gitignore +1 -2
- data/Gemfile.lock +47 -0
- data/acme-pki.gemspec +11 -8
- data/bin/letsencrypt +96 -72
- data/lib/acme/pki.rb +288 -261
- data/lib/acme/pki/information.rb +31 -19
- metadata +47 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7599cb3c3ceecf3e1ce1fb057339106937398c2ce6887a9f16442de981d351af
|
|
4
|
+
data.tar.gz: c81b97fc9132d5442185cfc4ea011b93175e8568dfeb582c0f1e7834ba3b2942
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ade350294c8dc14ee322120e342697d5f98b0d66cc98b611e908fda4502e011b196ca25ba0b0b548579800db9865600898cc77f3cf2a035c1ed17e36f5acfcd5
|
|
7
|
+
data.tar.gz: beeb43ea7b6d2e2f2551bc114edcc69349f7bcd85d66f54a872dce086bc062bbd56b77ae619b5344404a786c7b7e05a5ed13b9efd585bddff4f6c897321b8aa9
|
data/.gitignore
CHANGED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
acme-pki (0.2.3)
|
|
5
|
+
acme-client (~> 2.0)
|
|
6
|
+
colorize (~> 0.8)
|
|
7
|
+
faraday_middleware (~> 0.13)
|
|
8
|
+
simpleidn (~> 0.1)
|
|
9
|
+
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
acme-client (2.0.6)
|
|
14
|
+
faraday (>= 0.17, < 2.0.0)
|
|
15
|
+
awesome_print (1.8.0)
|
|
16
|
+
byebug (11.1.3)
|
|
17
|
+
coderay (1.1.2)
|
|
18
|
+
colorize (0.8.1)
|
|
19
|
+
faraday (0.17.3)
|
|
20
|
+
multipart-post (>= 1.2, < 3)
|
|
21
|
+
faraday_middleware (0.14.0)
|
|
22
|
+
faraday (>= 0.7.4, < 1.0)
|
|
23
|
+
method_source (1.0.0)
|
|
24
|
+
multipart-post (2.1.1)
|
|
25
|
+
pry (0.13.1)
|
|
26
|
+
coderay (~> 1.1)
|
|
27
|
+
method_source (~> 1.0)
|
|
28
|
+
pry-byebug (3.9.0)
|
|
29
|
+
byebug (~> 11.0)
|
|
30
|
+
pry (~> 0.13.0)
|
|
31
|
+
simpleidn (0.1.1)
|
|
32
|
+
unf (~> 0.1.4)
|
|
33
|
+
unf (0.1.4)
|
|
34
|
+
unf_ext
|
|
35
|
+
unf_ext (0.0.7.7)
|
|
36
|
+
|
|
37
|
+
PLATFORMS
|
|
38
|
+
ruby
|
|
39
|
+
|
|
40
|
+
DEPENDENCIES
|
|
41
|
+
acme-pki!
|
|
42
|
+
awesome_print (~> 1.8)
|
|
43
|
+
bundler (~> 2.0)
|
|
44
|
+
pry-byebug (~> 3.7)
|
|
45
|
+
|
|
46
|
+
BUNDLED WITH
|
|
47
|
+
2.1.4
|
data/acme-pki.gemspec
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
1
|
lib = File.expand_path('../lib', __FILE__)
|
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
3
|
|
|
4
|
+
require 'acme/pki/version'
|
|
5
|
+
|
|
5
6
|
Gem::Specification.new do |spec|
|
|
6
7
|
spec.name = 'acme-pki'
|
|
7
|
-
spec.version =
|
|
8
|
+
spec.version = Acme::PKI::VERSION
|
|
8
9
|
spec.authors = ['Aeris']
|
|
9
10
|
spec.email = ['aeris@imirhil.fr']
|
|
10
|
-
spec.summary = %q{Ruby client for Let
|
|
11
|
+
spec.summary = %q{Ruby client for Let's Encrypt}
|
|
11
12
|
spec.description = %q{Manage your keys, requests and certificates.}
|
|
12
13
|
spec.homepage = 'https://github.com/aeris/acme-pki/'
|
|
13
14
|
spec.license = 'AGPL-3.0+'
|
|
@@ -17,10 +18,12 @@ Gem::Specification.new do |spec|
|
|
|
17
18
|
spec.test_files = spec.files.grep %r{^(test|spec|features)/}
|
|
18
19
|
spec.require_paths = %w(lib)
|
|
19
20
|
|
|
20
|
-
spec.add_development_dependency 'bundler', '~>
|
|
21
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
22
|
+
spec.add_development_dependency 'awesome_print', '~> 1.8'
|
|
23
|
+
spec.add_development_dependency 'pry-byebug', '~> 3.7'
|
|
21
24
|
|
|
22
|
-
spec.add_dependency 'acme-client', '~> 0
|
|
23
|
-
spec.add_dependency 'faraday_middleware', '~> 0.
|
|
24
|
-
spec.add_dependency 'colorize', '~> 0.
|
|
25
|
-
spec.add_dependency 'simpleidn', '~> 0.
|
|
25
|
+
spec.add_dependency 'acme-client', '~> 2.0'
|
|
26
|
+
spec.add_dependency 'faraday_middleware', '~> 0.13'
|
|
27
|
+
spec.add_dependency 'colorize', '~> 0.8'
|
|
28
|
+
spec.add_dependency 'simpleidn', '~> 0.1'
|
|
26
29
|
end
|
data/bin/letsencrypt
CHANGED
|
@@ -3,77 +3,101 @@ require 'acme/pki'
|
|
|
3
3
|
|
|
4
4
|
pki = Acme::PKI.new
|
|
5
5
|
|
|
6
|
+
MYNAME = File.basename $PROGRAM_NAME
|
|
7
|
+
|
|
8
|
+
HELP = <<-"EOTEXT"
|
|
9
|
+
#{MYNAME} v#{Acme::PKI::VERSION}
|
|
10
|
+
|
|
11
|
+
Available Commands:
|
|
12
|
+
crt
|
|
13
|
+
csr
|
|
14
|
+
help
|
|
15
|
+
info
|
|
16
|
+
key
|
|
17
|
+
register
|
|
18
|
+
renew
|
|
19
|
+
EOTEXT
|
|
20
|
+
|
|
21
|
+
# if nothing, force help
|
|
22
|
+
ARGV << 'help' if ARGV.length.zero?
|
|
23
|
+
|
|
6
24
|
case ARGV.shift
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
25
|
+
when /help|-[hH]|--help/
|
|
26
|
+
puts HELP
|
|
27
|
+
exit 0
|
|
28
|
+
when 'register'
|
|
29
|
+
OptionParser.new do |opts|
|
|
30
|
+
opts.banner = "Usage: #{File.basename __FILE__} register <email>"
|
|
31
|
+
end.parse!
|
|
32
|
+
if ARGV.empty?
|
|
33
|
+
puts "An email address is required !"
|
|
34
|
+
exit -1
|
|
35
|
+
end
|
|
36
|
+
pki.register ARGV.first
|
|
37
|
+
when 'key'
|
|
38
|
+
options = OpenStruct.new type: Acme::PKI::DEFAULT_KEY_TYPE
|
|
39
|
+
OptionParser.new do |opts|
|
|
40
|
+
opts.banner = "Usage: #{File.basename __FILE__} key <domain> [options]"
|
|
41
|
+
opts.on('-r [KEYSIZE]', '--rsa [KEYSIZE]', 'RSA key, key size') { |k| options.type = [:rsa, k.to_i] }
|
|
42
|
+
opts.on('-e [CURVE]', '--ecc [CURVE]', 'ECC key, curve') { |k| options.type = [:ecc, k] }
|
|
43
|
+
end.parse!
|
|
44
|
+
if ARGV.empty?
|
|
45
|
+
puts 'A domain is required !'
|
|
46
|
+
exit -1
|
|
47
|
+
end
|
|
48
|
+
pki.generate_key ARGV.first, type: options.type
|
|
49
|
+
when 'csr'
|
|
50
|
+
options = OpenStruct.new domains: [], adds: [], removes: []
|
|
51
|
+
OptionParser.new do |opts|
|
|
52
|
+
opts.banner = "Usage: #{File.basename __FILE__} csr <domain> [options]"
|
|
53
|
+
opts.on('-k [KEYFILE]', '--key [KEYFILE]', 'Key file') { |k| options.key = k }
|
|
54
|
+
opts.on('-d [DOMAIN]', '--domain [DOMAIN]', 'Domain') { |d| options.domains << d }
|
|
55
|
+
opts.on('-a [DOMAIN]', '--add [DOMAIN]', 'Add domain') { |d| options.adds << d }
|
|
56
|
+
opts.on('-r [DOMAIN]', '--remove [DOMAIN]', 'Remove domain') { |d| options.removes << d }
|
|
57
|
+
end.parse!
|
|
58
|
+
if ARGV.empty?
|
|
59
|
+
puts 'A domain is required !'
|
|
60
|
+
exit -1
|
|
61
|
+
end
|
|
62
|
+
pki.generate_csr ARGV.first, key: options.key, domains: options.domains,
|
|
63
|
+
add: options.adds, remove: options.removes
|
|
64
|
+
when 'crt'
|
|
65
|
+
options = OpenStruct.new
|
|
66
|
+
OptionParser.new do |opts|
|
|
67
|
+
opts.banner = "Usage: #{File.basename __FILE__} crt <domain> [options]"
|
|
68
|
+
opts.on('-c [CSR]', '--csr [CSR]', 'CSR file') { |c| options.csr = c }
|
|
69
|
+
end.parse!
|
|
70
|
+
if ARGV.empty?
|
|
71
|
+
puts 'A domain is required !'
|
|
72
|
+
exit -1
|
|
73
|
+
end
|
|
74
|
+
pki.generate_crt ARGV.first, csr: options.csr
|
|
75
|
+
when 'renew'
|
|
76
|
+
options = OpenStruct.new
|
|
77
|
+
OptionParser.new do |opts|
|
|
78
|
+
opts.banner = "Usage: #{File.basename __FILE__} renew <domain> [options]"
|
|
79
|
+
opts.on('-c [CSR]', '--csr [CSR]', 'CSR file') { |c| options.csr = c }
|
|
80
|
+
end.parse!
|
|
81
|
+
if ARGV.empty?
|
|
82
|
+
puts 'A domain is required !'
|
|
83
|
+
exit -1
|
|
84
|
+
end
|
|
85
|
+
exit pki.renew(ARGV.first, csr: options.csr) ? 0 : 1
|
|
86
|
+
when 'info'
|
|
87
|
+
type = :key
|
|
88
|
+
OptionParser.new do |opts|
|
|
89
|
+
opts.banner = "Usage: #{File.basename __FILE__} info <domain> [options]"
|
|
90
|
+
opts.on('-k', '--key', 'Key information') { type = :key }
|
|
91
|
+
opts.on('-c', '--crt', 'Certificate information') { type = :crt }
|
|
92
|
+
end.parse!
|
|
93
|
+
if ARGV.empty?
|
|
94
|
+
puts 'A domain is required !'
|
|
95
|
+
exit -1
|
|
96
|
+
end
|
|
97
|
+
case type
|
|
98
|
+
when :key
|
|
99
|
+
pki.key_info pki.key ARGV.first
|
|
100
|
+
when :crt
|
|
101
|
+
pki.chain_info pki.crt ARGV.first
|
|
102
|
+
end
|
|
79
103
|
end
|
data/lib/acme/pki.rb
CHANGED
|
@@ -10,267 +10,294 @@ require 'simpleidn'
|
|
|
10
10
|
|
|
11
11
|
require 'acme/pki/monkey_patch'
|
|
12
12
|
require 'acme/pki/information'
|
|
13
|
+
require 'acme/pki/version'
|
|
13
14
|
|
|
14
15
|
module Acme
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
16
|
+
class PKI
|
|
17
|
+
include Information
|
|
18
|
+
|
|
19
|
+
if ENV['ACME_STAGING']
|
|
20
|
+
puts 'Using Let\'s Encrypt ACME staging'.colorize :yellow
|
|
21
|
+
ENV['ACME_ENDPOINT'] = 'https://acme-staging-v02.api.letsencrypt.org/directory'
|
|
22
|
+
ENV['ACME_ACCOUNT_KEY'] = 'account-staging'
|
|
23
|
+
end
|
|
24
|
+
DEFAULT_ENDPOINT = ENV['ACME_ENDPOINT'] || 'https://acme-v02.api.letsencrypt.org/directory'
|
|
25
|
+
DEFAULT_DIRECTORY = ENV['ACME_DIRECTORY'] || Dir.pwd
|
|
26
|
+
DEFAULT_ACCOUNT_KEY = ENV['ACME_ACCOUNT_KEY'] || 'account'
|
|
27
|
+
DEFAULT_ACCOUNT_KEY_TYPE = [:rsa, 4096].freeze
|
|
28
|
+
DEFAULT_KEY_TYPE = [:ecc, 'prime256v1'].freeze
|
|
29
|
+
DEFAULT_RENEW_DURATION = 60 * 60 * 24 * 30 # 1 month
|
|
30
|
+
|
|
31
|
+
def initialize(directory: DEFAULT_DIRECTORY, account_key: DEFAULT_ACCOUNT_KEY, endpoint: DEFAULT_ENDPOINT)
|
|
32
|
+
@directory = directory
|
|
33
|
+
@challenge_dir = ENV['ACME_CHALLENGE'] || File.join(@directory, 'acme-challenge')
|
|
34
|
+
@account_key_file = self.key DEFAULT_ACCOUNT_KEY, 'key'
|
|
35
|
+
@account_key = if File.exists? @account_key_file
|
|
36
|
+
open(@account_key_file, 'r') { |f| OpenSSL::PKey.read f }
|
|
37
|
+
else
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
@endpoint = endpoint
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def key(name, extension = 'pem')
|
|
44
|
+
self.file name, extension
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def csr(name)
|
|
48
|
+
self.file name, 'csr'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def crt(name)
|
|
52
|
+
self.file name, 'crt'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def register(mail)
|
|
56
|
+
@account_key_file, @account_key = self.generate_key DEFAULT_ACCOUNT_KEY, 'key', type: DEFAULT_ACCOUNT_KEY_TYPE
|
|
57
|
+
tos = self.client.meta['termsOfService']
|
|
58
|
+
$stdout.print "Are you agree with Let's Encrypt terms of service available at #{tos}? [yN] "
|
|
59
|
+
$stdout.flush
|
|
60
|
+
accept = $stdin.gets.chomp.downcase == 'y'
|
|
61
|
+
exit unless accept
|
|
62
|
+
self.process("Registering account key #{@account_key_file}") do
|
|
63
|
+
self.client.new_account contact: "mailto:#{mail}", terms_of_service_agreed: accept
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def generate_key(name, extension = 'pem', type: DEFAULT_KEY_TYPE)
|
|
68
|
+
key_file = self.key name, extension
|
|
69
|
+
type, size = type
|
|
70
|
+
|
|
71
|
+
log = case type
|
|
72
|
+
when :rsa
|
|
73
|
+
"RSA #{size} bits"
|
|
74
|
+
when :ecc
|
|
75
|
+
"ECC #{size} curve"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
key = self.process "Generating #{log} private key into #{key_file}" do
|
|
79
|
+
key = case type
|
|
80
|
+
when :rsa
|
|
81
|
+
OpenSSL::PKey::RSA.new size
|
|
82
|
+
when :ecc
|
|
83
|
+
OpenSSL::PKey::EC.new(size).generate_key
|
|
84
|
+
end
|
|
85
|
+
open(key_file, 'w') { |f| f.write key.to_pem }
|
|
86
|
+
key
|
|
87
|
+
end
|
|
88
|
+
self.key_info key
|
|
89
|
+
[key_file, key]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def generate_csr(csr, domains: [], add: [], remove: [], key: nil)
|
|
93
|
+
key = csr unless key
|
|
94
|
+
csr_file = self.csr csr
|
|
95
|
+
key_file = self.key key
|
|
96
|
+
self.generate_key key unless File.exist? key_file
|
|
97
|
+
|
|
98
|
+
domains = if add.empty? && remove.empty?
|
|
99
|
+
[csr, *domains]
|
|
100
|
+
else
|
|
101
|
+
tmp = OpenSSL::X509::Request.new File.read csr_file
|
|
102
|
+
domains = self.domains tmp
|
|
103
|
+
domains - remove + add
|
|
104
|
+
end.collect { |d| SimpleIDN.to_ascii d }
|
|
105
|
+
|
|
106
|
+
self.process "Generating CSR for #{domains.join ', '} with key #{key_file} into #{csr_file}" do
|
|
107
|
+
key_file = open(key_file, 'r') { |f| OpenSSL::PKey.read f }
|
|
108
|
+
csr = OpenSSL::X509::Request.new
|
|
109
|
+
csr.subject = OpenSSL::X509::Name.parse "/CN=#{domains.first}"
|
|
110
|
+
|
|
111
|
+
public_key = case key_file
|
|
112
|
+
when OpenSSL::PKey::EC
|
|
113
|
+
curve = key_file.group.curve_name
|
|
114
|
+
public = OpenSSL::PKey::EC.new curve
|
|
115
|
+
public.public_key = key_file.public_key
|
|
116
|
+
|
|
117
|
+
public
|
|
118
|
+
else
|
|
119
|
+
key_file.public_key
|
|
120
|
+
end
|
|
121
|
+
csr.public_key = public_key
|
|
122
|
+
|
|
123
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
|
124
|
+
extensions = []
|
|
125
|
+
#extensions << factory.create_extension('basicConstraints', 'CA:FALSE', true)
|
|
126
|
+
extensions << factory.create_extension('keyUsage', 'digitalSignature,nonRepudiation,keyEncipherment')
|
|
127
|
+
extensions << factory.create_extension('subjectAltName', domains.collect { |d| "DNS:#{d}" }.join(', '))
|
|
128
|
+
|
|
129
|
+
extensions = OpenSSL::ASN1::Sequence extensions
|
|
130
|
+
extensions = OpenSSL::ASN1::Set [extensions]
|
|
131
|
+
csr.add_attribute OpenSSL::X509::Attribute.new 'extReq', extensions
|
|
132
|
+
|
|
133
|
+
csr.sign key_file, OpenSSL::Digest::SHA512.new
|
|
134
|
+
open(csr_file, 'w') { |f| f.write csr.to_pem }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def generate_crt(crt, csr: nil)
|
|
139
|
+
csr = crt unless csr
|
|
140
|
+
short_csr = csr
|
|
141
|
+
crt = self.crt crt
|
|
142
|
+
csr = self.csr csr
|
|
143
|
+
self.generate_csr short_csr unless File.exist? csr
|
|
144
|
+
self.internal_generate_crt crt, csr: csr
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def renew(crt, csr: nil, duration: DEFAULT_RENEW_DURATION)
|
|
148
|
+
csr = crt unless csr
|
|
149
|
+
crt = self.crt crt
|
|
150
|
+
csr = self.csr csr
|
|
151
|
+
puts "Renewing #{crt} CRT from #{csr} CSR"
|
|
152
|
+
|
|
153
|
+
if File.exists? crt
|
|
154
|
+
x509 = OpenSSL::X509::Certificate.new File.read crt
|
|
155
|
+
delay = x509.not_after - Time.now
|
|
156
|
+
if delay > duration
|
|
157
|
+
puts "No need to renew (#{humanize delay})"
|
|
158
|
+
return false
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
self.internal_generate_crt crt, csr: csr
|
|
163
|
+
true
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def client
|
|
167
|
+
unless @account_key
|
|
168
|
+
puts 'No account key available'.colorize :yellow
|
|
169
|
+
puts 'Please register yourself before'.colorize :red
|
|
170
|
+
exit -1
|
|
171
|
+
end
|
|
172
|
+
@client ||= Acme::Client.new private_key: @account_key, directory: @endpoint
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def process(line, io: STDOUT)
|
|
176
|
+
io.print "#{line}..."
|
|
177
|
+
io.flush
|
|
178
|
+
|
|
179
|
+
result = yield
|
|
180
|
+
|
|
181
|
+
io.puts " [#{'OK'.colorize :green}]"
|
|
182
|
+
|
|
183
|
+
result
|
|
184
|
+
rescue Exception
|
|
185
|
+
io.puts " [#{'KO'.colorize :red}]"
|
|
186
|
+
raise
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def file(name, extension = nil)
|
|
190
|
+
return nil unless name
|
|
191
|
+
name = name.split('.').reverse.join('.')
|
|
192
|
+
name = "#{name}.#{extension}" if extension
|
|
193
|
+
File.join @directory, name
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def domains(csr)
|
|
197
|
+
domains = []
|
|
198
|
+
|
|
199
|
+
cn = csr.subject.to_a.first { |n, _, _| n == 'CN' }
|
|
200
|
+
domains << cn[1] if cn
|
|
201
|
+
|
|
202
|
+
attribute = csr.attributes.detect { |a| %w(extReq msExtReq).include? a.oid }
|
|
203
|
+
if attribute
|
|
204
|
+
set = OpenSSL::ASN1.decode attribute.value
|
|
205
|
+
seq = set.value.first
|
|
206
|
+
sans = seq.value.collect { |s| OpenSSL::X509::Extension.new(s).to_a }
|
|
207
|
+
.detect { |e| e.first == 'subjectAltName' }
|
|
208
|
+
if sans
|
|
209
|
+
sans = sans[1]
|
|
210
|
+
sans = sans.split(/\s*,\s*/)
|
|
211
|
+
.collect { |s| s.split /\s*:\s*/ }
|
|
212
|
+
.select { |t, _| t == 'DNS' }
|
|
213
|
+
.collect { |_, v| v }
|
|
214
|
+
domains.concat sans
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
domains.uniq
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def authorize(authorization)
|
|
222
|
+
domain = authorization.domain
|
|
223
|
+
challenge = authorization.http
|
|
224
|
+
status = challenge.status
|
|
225
|
+
if status == 'valid'
|
|
226
|
+
puts "Domain #{domain.colorize :green} already authorized"
|
|
227
|
+
return
|
|
228
|
+
end
|
|
229
|
+
puts "Authorizing domain #{domain.colorize :yellow} (current status: #{status.colorize :yellow})"
|
|
230
|
+
|
|
231
|
+
unless Dir.exists? @challenge_dir
|
|
232
|
+
self.process "Creating directory #{@challenge_dir}" do
|
|
233
|
+
FileUtils.mkdir_p @challenge_dir
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
filename = challenge.token
|
|
238
|
+
file = File.join @challenge_dir, filename
|
|
239
|
+
content = challenge.file_content
|
|
240
|
+
self.process "Writing challenge for #{domain.colorize :yellow} into #{file.colorize :yellow}" do
|
|
241
|
+
File.write file, content
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
url = "http://#{domain}/.well-known/acme-challenge/#{filename}"
|
|
245
|
+
self.process "Test challenge for #{url.colorize :yellow}" do
|
|
246
|
+
response = begin
|
|
247
|
+
Faraday.new do |conn|
|
|
248
|
+
conn.use FaradayMiddleware::FollowRedirects
|
|
249
|
+
conn.adapter Faraday.default_adapter
|
|
250
|
+
end.get url
|
|
251
|
+
rescue => e
|
|
252
|
+
raise Exception, e.message
|
|
253
|
+
end
|
|
254
|
+
raise Exception, "Got response code #{response.status.to_s.colorize :red}" unless response.success?
|
|
255
|
+
real_content = response.body
|
|
256
|
+
raise Exception, "Got #{real_content.colorize :red}, expected #{content.colorize :green}" unless real_content == content
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
self.process "Authorizing domain #{domain.colorize :yellow}" do
|
|
260
|
+
challenge.request_validation
|
|
261
|
+
status = nil
|
|
262
|
+
60.times do
|
|
263
|
+
sleep 1
|
|
264
|
+
challenge.reload
|
|
265
|
+
status = challenge.status
|
|
266
|
+
break if status != 'pending'
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
raise Exception, "Got status #{status.colorize :red} instead of valid" unless status == 'valid'
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
File.unlink file
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def internal_generate_crt(crt, csr: nil)
|
|
276
|
+
csr = crt unless csr
|
|
277
|
+
csr_file = csr
|
|
278
|
+
csr = OpenSSL::X509::Request.new File.read csr
|
|
279
|
+
domains = self.domains csr
|
|
280
|
+
|
|
281
|
+
order = client.new_order identifiers: domains
|
|
282
|
+
order.authorizations.each { |a| self.authorize a }
|
|
283
|
+
|
|
284
|
+
crt = self.process "Generating CRT #{crt} from CSR #{csr_file}" do
|
|
285
|
+
order.finalize csr: csr
|
|
286
|
+
certificate = order.certificate
|
|
287
|
+
File.write crt, certificate
|
|
288
|
+
OpenSSL::X509::Certificate.new certificate
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
self.certifificate_info crt
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def humanize(secs)
|
|
295
|
+
[[60, :seconds], [60, :minutes], [24, :hours], [30, :days], [12, :months]].map { |count, name|
|
|
296
|
+
if secs > 0
|
|
297
|
+
secs, n = secs.divmod count
|
|
298
|
+
"#{n.to_i} #{name}"
|
|
299
|
+
end
|
|
300
|
+
}.compact.reverse.join(' ')
|
|
301
|
+
end
|
|
302
|
+
end
|
|
276
303
|
end
|
data/lib/acme/pki/information.rb
CHANGED
|
@@ -5,7 +5,19 @@ module Acme
|
|
|
5
5
|
module Information
|
|
6
6
|
def key_info(key, tab: 0)
|
|
7
7
|
key = open(key, 'r') { |f| OpenSSL::PKey.read f } unless key.is_a? OpenSSL::PKey::PKey
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
der = case key
|
|
10
|
+
when OpenSSL::PKey::EC
|
|
11
|
+
puts "\t" * (tab) + "#{'Key'.colorize :red} : ECC #{key.group.curve_name}"
|
|
12
|
+
|
|
13
|
+
point = key.public_key
|
|
14
|
+
pub = OpenSSL::PKey::EC.new point.group
|
|
15
|
+
pub.public_key = point
|
|
16
|
+
pub
|
|
17
|
+
when OpenSSL::PKey::RSA
|
|
18
|
+
puts "\t" * (tab) + "#{'Key'.colorize :red} : RSA #{key.n.num_bits} bits"
|
|
19
|
+
key.public_key
|
|
20
|
+
end.to_der
|
|
9
21
|
|
|
10
22
|
fingerprint der, tab: tab
|
|
11
23
|
|
|
@@ -61,25 +73,25 @@ module Acme
|
|
|
61
73
|
puts "Fetch certificate #{issuer} from #{uri}"
|
|
62
74
|
file = Digest::MD5.hexdigest uri
|
|
63
75
|
file = file File.join 'cache', file
|
|
64
|
-
dir
|
|
76
|
+
dir = File.dirname file
|
|
65
77
|
FileUtils.mkpath dir unless Dir.exist? dir
|
|
66
|
-
crt
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
crt = if File.exist? file
|
|
79
|
+
open(file, 'r') { |f| OpenSSL::X509::Certificate.new f }
|
|
80
|
+
else
|
|
81
|
+
crt = Faraday.get uri
|
|
82
|
+
break unless crt.success?
|
|
83
|
+
crt = crt.body
|
|
84
|
+
|
|
85
|
+
crt = begin
|
|
86
|
+
OpenSSL::X509::Certificate.new crt
|
|
87
|
+
rescue
|
|
88
|
+
pkcs7 = OpenSSL::PKCS7.new crt
|
|
89
|
+
pkcs7.certificates.first
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
File.write file, crt.to_pem
|
|
93
|
+
crt
|
|
94
|
+
end
|
|
83
95
|
|
|
84
96
|
subject = crt.subject
|
|
85
97
|
puts "Warning : expecting #{issuer}, get #{subject}".colorize :magenta unless subject == issuer
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acme-pki
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aeris
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-06-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -16,70 +16,98 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '2.0'
|
|
20
20
|
type: :development
|
|
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: '
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: awesome_print
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.8'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.8'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: pry-byebug
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.7'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.7'
|
|
27
55
|
- !ruby/object:Gem::Dependency
|
|
28
56
|
name: acme-client
|
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
|
30
58
|
requirements:
|
|
31
59
|
- - "~>"
|
|
32
60
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0
|
|
61
|
+
version: '2.0'
|
|
34
62
|
type: :runtime
|
|
35
63
|
prerelease: false
|
|
36
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
65
|
requirements:
|
|
38
66
|
- - "~>"
|
|
39
67
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0
|
|
68
|
+
version: '2.0'
|
|
41
69
|
- !ruby/object:Gem::Dependency
|
|
42
70
|
name: faraday_middleware
|
|
43
71
|
requirement: !ruby/object:Gem::Requirement
|
|
44
72
|
requirements:
|
|
45
73
|
- - "~>"
|
|
46
74
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.
|
|
75
|
+
version: '0.13'
|
|
48
76
|
type: :runtime
|
|
49
77
|
prerelease: false
|
|
50
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
79
|
requirements:
|
|
52
80
|
- - "~>"
|
|
53
81
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.
|
|
82
|
+
version: '0.13'
|
|
55
83
|
- !ruby/object:Gem::Dependency
|
|
56
84
|
name: colorize
|
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
|
58
86
|
requirements:
|
|
59
87
|
- - "~>"
|
|
60
88
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.
|
|
89
|
+
version: '0.8'
|
|
62
90
|
type: :runtime
|
|
63
91
|
prerelease: false
|
|
64
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
93
|
requirements:
|
|
66
94
|
- - "~>"
|
|
67
95
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.
|
|
96
|
+
version: '0.8'
|
|
69
97
|
- !ruby/object:Gem::Dependency
|
|
70
98
|
name: simpleidn
|
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
|
72
100
|
requirements:
|
|
73
101
|
- - "~>"
|
|
74
102
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 0.
|
|
103
|
+
version: '0.1'
|
|
76
104
|
type: :runtime
|
|
77
105
|
prerelease: false
|
|
78
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
107
|
requirements:
|
|
80
108
|
- - "~>"
|
|
81
109
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: 0.
|
|
110
|
+
version: '0.1'
|
|
83
111
|
description: Manage your keys, requests and certificates.
|
|
84
112
|
email:
|
|
85
113
|
- aeris@imirhil.fr
|
|
@@ -90,6 +118,7 @@ extra_rdoc_files: []
|
|
|
90
118
|
files:
|
|
91
119
|
- ".gitignore"
|
|
92
120
|
- Gemfile
|
|
121
|
+
- Gemfile.lock
|
|
93
122
|
- LICENSE
|
|
94
123
|
- README.md
|
|
95
124
|
- acme-pki.gemspec
|
|
@@ -97,11 +126,12 @@ files:
|
|
|
97
126
|
- lib/acme/pki.rb
|
|
98
127
|
- lib/acme/pki/information.rb
|
|
99
128
|
- lib/acme/pki/monkey_patch.rb
|
|
129
|
+
- lib/acme/pki/version.rb
|
|
100
130
|
homepage: https://github.com/aeris/acme-pki/
|
|
101
131
|
licenses:
|
|
102
132
|
- AGPL-3.0+
|
|
103
133
|
metadata: {}
|
|
104
|
-
post_install_message:
|
|
134
|
+
post_install_message:
|
|
105
135
|
rdoc_options: []
|
|
106
136
|
require_paths:
|
|
107
137
|
- lib
|
|
@@ -116,9 +146,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
116
146
|
- !ruby/object:Gem::Version
|
|
117
147
|
version: '0'
|
|
118
148
|
requirements: []
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
signing_key:
|
|
149
|
+
rubygems_version: 3.0.3
|
|
150
|
+
signing_key:
|
|
122
151
|
specification_version: 4
|
|
123
|
-
summary: Ruby client for Let
|
|
152
|
+
summary: Ruby client for Let's Encrypt
|
|
124
153
|
test_files: []
|