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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.rubocop.yml +33 -0
- data/Gemfile +2 -0
- data/README.md +6 -3
- data/Rakefile +4 -1
- data/letscert.gemspec +5 -5
- data/lib/letscert/certificate.rb +104 -82
- data/lib/letscert/io_plugin.rb +32 -350
- data/lib/letscert/io_plugins/account_key.rb +29 -0
- data/lib/letscert/io_plugins/cert_file.rb +29 -0
- data/lib/letscert/io_plugins/chain_file.rb +32 -0
- data/lib/letscert/io_plugins/file_io_plugin_mixin.rb +48 -0
- data/lib/letscert/io_plugins/full_chain_file.rb +39 -0
- data/lib/letscert/io_plugins/jwk_io_plugin_mixin.rb +68 -0
- data/lib/letscert/io_plugins/key_file.rb +29 -0
- data/lib/letscert/io_plugins/openssl_io_plugin.rb +68 -0
- data/lib/letscert/loggable.rb +2 -3
- data/lib/letscert/runner.rb +130 -157
- data/lib/letscert/runner/logger_formatter.rb +34 -0
- data/lib/letscert/runner/valid_time.rb +48 -0
- data/lib/letscert/version.rb +1 -1
- metadata +13 -16
- metadata.gz.sig +1 -2
@@ -0,0 +1,29 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# Account key IO plugin
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class AccountKey < IOPlugin
|
6
|
+
include FileIOPluginMixin
|
7
|
+
include JWKIOPluginMixin
|
8
|
+
|
9
|
+
# @return [Hash] always get +true+ for +:account_key+ key
|
10
|
+
def persisted
|
11
|
+
{ account_key: true }
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Hash]
|
15
|
+
def load_from_content(content)
|
16
|
+
{ account_key: load_jwk(content) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Save account key.
|
20
|
+
# @param [Hash] data
|
21
|
+
# @return [void]
|
22
|
+
def save(data)
|
23
|
+
save_to_file(dump_jwk(data[:account_key]))
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
IOPlugin.register(AccountKey, 'account_key.json')
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# Cert file plugin
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class CertFile < OpenSSLIOPlugin
|
6
|
+
include FileIOPluginMixin
|
7
|
+
|
8
|
+
# @return [Hash] always get +true+ for +:cert+ key
|
9
|
+
def persisted
|
10
|
+
@persisted ||= { cert: true }
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Hash]
|
14
|
+
def load_from_content(content)
|
15
|
+
{ cert: load_cert(content) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Save certificate.
|
19
|
+
# @param [Hash] data
|
20
|
+
# @return [void]
|
21
|
+
def save(data)
|
22
|
+
save_to_file(dump_cert(data[:cert]))
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
IOPlugin.register(CertFile, 'cert.pem', :pem)
|
28
|
+
IOPlugin.register(CertFile, 'cert.der', :der)
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# Chain file plugin
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class ChainFile < OpenSSLIOPlugin
|
6
|
+
include FileIOPluginMixin
|
7
|
+
|
8
|
+
# @return [Hash] always get +true+ for +:chain+ key
|
9
|
+
def persisted
|
10
|
+
@persisted ||= { chain: true }
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Hash]
|
14
|
+
def load_from_content(content)
|
15
|
+
chain = []
|
16
|
+
split_pems(content) do |pem|
|
17
|
+
chain << load_cert(pem)
|
18
|
+
end
|
19
|
+
{ chain: chain }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Save chain.
|
23
|
+
# @param [Hash] data
|
24
|
+
# @return [void]
|
25
|
+
def save(data)
|
26
|
+
save_to_file(data[:chain].map { |c| dump_cert(c) }.join)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
IOPlugin.register(ChainFile, 'chain.pem', :pem)
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# Mixin for IOPmugin subclasses that handle files
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
module FileIOPluginMixin
|
6
|
+
|
7
|
+
# Load data from file named +#name+
|
8
|
+
# @return [Hash]
|
9
|
+
def load
|
10
|
+
logger.debug { "Loading #{@name}" }
|
11
|
+
|
12
|
+
begin
|
13
|
+
content = File.read(@name)
|
14
|
+
rescue Errno::ENOENT => ex
|
15
|
+
logger.info { "no #{@name} file" }
|
16
|
+
return self.class.empty_data
|
17
|
+
end
|
18
|
+
|
19
|
+
load_from_content(content)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @abstract
|
23
|
+
# @param [String] _content
|
24
|
+
# @return [Hash]
|
25
|
+
def load_from_content(_content)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
# Save data to file +#name+
|
30
|
+
# @param [Hash] data
|
31
|
+
# @return [void]
|
32
|
+
def save_to_file(data)
|
33
|
+
return if data.nil?
|
34
|
+
|
35
|
+
logger.info { "saving #{@name}" }
|
36
|
+
begin
|
37
|
+
File.open(name, 'w') do |f|
|
38
|
+
f.write(data)
|
39
|
+
end
|
40
|
+
rescue Errno => ex
|
41
|
+
@logger.error { ex.message }
|
42
|
+
raise Error, "Error when saving #{@name}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# Fullchain file plugin
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class FullChainFile < ChainFile
|
6
|
+
|
7
|
+
# @return [Hash] always get +true+ for +:cert+ and +:chain+ keys
|
8
|
+
def persisted
|
9
|
+
@persisted ||= { cert: true, chain: true }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Load full certificate chain
|
13
|
+
# @return [Hash]
|
14
|
+
def load
|
15
|
+
data = super
|
16
|
+
if data[:chain].nil? or data[:chain].empty?
|
17
|
+
cert = nil
|
18
|
+
chain = []
|
19
|
+
else
|
20
|
+
cert = data[:chain].shift
|
21
|
+
chain = data[:chain]
|
22
|
+
end
|
23
|
+
|
24
|
+
{ account_key: data[:account_key], key: data[:key], cert: cert,
|
25
|
+
chain: chain }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Save fullchain.
|
29
|
+
# @param [Hash] data
|
30
|
+
# @return [void]
|
31
|
+
def save(data)
|
32
|
+
super(account_key: data[:account_key], key: data[:key], cert: nil,
|
33
|
+
chain: [data[:cert]] + data[:chain])
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
IOPlugin.register(FullChainFile, 'fullchain.pem', :pem)
|
39
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module LetsCert
|
4
|
+
|
5
|
+
# Mixin for IOPlugin subclasses that handle JWK
|
6
|
+
# @author Sylvain Daubert
|
7
|
+
module JWKIOPluginMixin
|
8
|
+
|
9
|
+
# Encode string +data+ to base64
|
10
|
+
# @param [String] data
|
11
|
+
# @return [String]
|
12
|
+
def urlsafe_encode64(data)
|
13
|
+
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
14
|
+
end
|
15
|
+
|
16
|
+
# Decode base64 string +data+
|
17
|
+
# @param [String] data
|
18
|
+
# @return [String]
|
19
|
+
def urlsafe_decode64(data)
|
20
|
+
Base64.urlsafe_decode64(data.sub(/[\s=]*\z/, ''))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Load crypto data from JSON-encoded file
|
24
|
+
# @param [String] data JSON-encoded data
|
25
|
+
# @return [OpenSSL::PKey::PKey]
|
26
|
+
def load_jwk(data)
|
27
|
+
return nil if data.empty?
|
28
|
+
|
29
|
+
h = JSON.parse(data)
|
30
|
+
case h['kty']
|
31
|
+
when 'RSA'
|
32
|
+
pkey = OpenSSL::PKey::RSA.new
|
33
|
+
%w(e n d p q).collect do |key|
|
34
|
+
next if h[key].nil?
|
35
|
+
value = OpenSSL::BN.new(urlsafe_decode64(h[key]), 2)
|
36
|
+
pkey.send "#{key}=".to_sym, value
|
37
|
+
end
|
38
|
+
else
|
39
|
+
raise Error, "unknown account key type '#{k['kty']}'"
|
40
|
+
end
|
41
|
+
|
42
|
+
pkey
|
43
|
+
end
|
44
|
+
|
45
|
+
# Dump crypto data (key) to a JSON-encoded string
|
46
|
+
# @param [OpenSSL::PKey] key
|
47
|
+
# @return [String]
|
48
|
+
def dump_jwk(key)
|
49
|
+
return {}.to_json if key.nil?
|
50
|
+
|
51
|
+
h = { 'kty' => 'RSA' }
|
52
|
+
case key
|
53
|
+
when OpenSSL::PKey::RSA
|
54
|
+
h['e'] = urlsafe_encode64(key.e.to_s(2)) if key.e
|
55
|
+
h['n'] = urlsafe_encode64(key.n.to_s(2)) if key.n
|
56
|
+
if key.private?
|
57
|
+
h['d'] = urlsafe_encode64(key.d.to_s(2))
|
58
|
+
h['p'] = urlsafe_encode64(key.p.to_s(2))
|
59
|
+
h['q'] = urlsafe_encode64(key.q.to_s(2))
|
60
|
+
end
|
61
|
+
else
|
62
|
+
raise Error, 'only RSA keys are supported'
|
63
|
+
end
|
64
|
+
h.to_json
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# Key file plugin
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class KeyFile < OpenSSLIOPlugin
|
6
|
+
include FileIOPluginMixin
|
7
|
+
|
8
|
+
# @return [Hash] always get +true+ for +:key+ key
|
9
|
+
def persisted
|
10
|
+
@persisted ||= { key: true }
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Hash]
|
14
|
+
def load_from_content(content)
|
15
|
+
{ key: load_key(content) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Save private key.
|
19
|
+
# @param [Hash] data
|
20
|
+
# @return [void]
|
21
|
+
def save(data)
|
22
|
+
save_to_file(dump_key(data[:key]))
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
IOPlugin.register(KeyFile, 'key.pem', :pem)
|
28
|
+
IOPlugin.register(KeyFile, 'key.der', :der)
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module LetsCert
|
2
|
+
|
3
|
+
# OpenSSL IOPlugin
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
class OpenSSLIOPlugin < IOPlugin
|
6
|
+
|
7
|
+
# @private Regular expression to discriminate PEM
|
8
|
+
PEM_RE = /^-----BEGIN CERTIFICATE-----\n.*?\n-----END CERTIFICATE-----\n/m
|
9
|
+
|
10
|
+
# @param [String] name filename
|
11
|
+
# @param [:pem,:der] type
|
12
|
+
def initialize(name, type)
|
13
|
+
case type
|
14
|
+
when :pem
|
15
|
+
when :der
|
16
|
+
else
|
17
|
+
raise ArgumentError, 'type should be :pem or :der'
|
18
|
+
end
|
19
|
+
|
20
|
+
@type = type
|
21
|
+
super(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Load key from raw +data+
|
25
|
+
# @param [String] data
|
26
|
+
# @return [OpenSSL::PKey]
|
27
|
+
def load_key(data)
|
28
|
+
OpenSSL::PKey::RSA.new data
|
29
|
+
end
|
30
|
+
|
31
|
+
# Dump key/cert data
|
32
|
+
# @param [OpenSSL::PKey] key
|
33
|
+
# @return [String]
|
34
|
+
def dump_key(key)
|
35
|
+
case @type
|
36
|
+
when :pem
|
37
|
+
key.to_pem
|
38
|
+
when :der
|
39
|
+
key.to_der
|
40
|
+
end
|
41
|
+
end
|
42
|
+
alias dump_cert dump_key
|
43
|
+
|
44
|
+
# Load certificate from raw +data+
|
45
|
+
# @param [String] data
|
46
|
+
# @return [OpenSSL::X509::Certificate]
|
47
|
+
def load_cert(data)
|
48
|
+
OpenSSL::X509::Certificate.new data
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Split concatenated PEMs.
|
54
|
+
# @param [String] data
|
55
|
+
# @yield [String] pem
|
56
|
+
def split_pems(data)
|
57
|
+
my_data = data
|
58
|
+
m = my_data.match(PEM_RE)
|
59
|
+
while m
|
60
|
+
yield m[0]
|
61
|
+
my_data = my_data[m.end(0)..-1]
|
62
|
+
m = my_data.match(PEM_RE)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
data/lib/letscert/loggable.rb
CHANGED
@@ -41,8 +41,8 @@ module LetsCert
|
|
41
41
|
module ClassMethods
|
42
42
|
|
43
43
|
# @private hook called when a subclass is created.
|
44
|
-
# Take care of all subclasses to later properly set @logger class
|
45
|
-
# variable.
|
44
|
+
# Take care of all subclasses to later properly set @logger class
|
45
|
+
# instance variable.
|
46
46
|
# @param [Class] subclass
|
47
47
|
# @return [void]
|
48
48
|
def inherited(subclass)
|
@@ -69,5 +69,4 @@ module LetsCert
|
|
69
69
|
end
|
70
70
|
|
71
71
|
end
|
72
|
-
|
73
72
|
end
|
data/lib/letscert/runner.rb
CHANGED
@@ -25,86 +25,15 @@ require 'fileutils'
|
|
25
25
|
|
26
26
|
require_relative 'io_plugin'
|
27
27
|
require_relative 'certificate'
|
28
|
+
require_relative 'runner/logger_formatter'
|
29
|
+
require_relative 'runner/valid_time'
|
28
30
|
|
29
31
|
module LetsCert
|
30
32
|
|
31
33
|
# Runner class: analyse and execute CLI commands.
|
32
34
|
# @author Sylvain Daubert
|
35
|
+
# rubocop:disable Metrics/ClassLength
|
33
36
|
class Runner
|
34
|
-
# Get options
|
35
|
-
# @return [Hash]
|
36
|
-
attr_reader :options
|
37
|
-
# @return [Logger]
|
38
|
-
attr_accessor :logger
|
39
|
-
|
40
|
-
# Custom logger formatter
|
41
|
-
class LoggerFormatter < Logger::Formatter
|
42
|
-
|
43
|
-
# @private log format
|
44
|
-
FORMAT = "[%s] %5s: %s\n"
|
45
|
-
|
46
|
-
# @param [String] severity
|
47
|
-
# @param [Datetime] time
|
48
|
-
# @param [nil,String] progname
|
49
|
-
# @param [String] msg
|
50
|
-
# @return [String]
|
51
|
-
def call(severity, time, progname, msg)
|
52
|
-
FORMAT % [format_datetime(time), severity, msg2str(msg)]
|
53
|
-
end
|
54
|
-
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
# @private simple datetime formatter
|
59
|
-
# @param [DateTime] time
|
60
|
-
# @return [String]
|
61
|
-
def format_datetime(time)
|
62
|
-
time.strftime("%Y-%m-%d %H:%M:%S")
|
63
|
-
end
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
# Class used to process validation time from String.
|
68
|
-
# @author Sylvain Daubert
|
69
|
-
class ValidTime
|
70
|
-
|
71
|
-
# @param [String] str time string. May be:
|
72
|
-
# * an integer -> time in seconds
|
73
|
-
# * an integer plus a letter:
|
74
|
-
# * 30m: 30 minutes,
|
75
|
-
# * 30h: 30 hours,
|
76
|
-
# * 30d: 30 days.
|
77
|
-
def initialize(str)
|
78
|
-
m = str.match(/^(\d+)([mhd])?$/)
|
79
|
-
if m
|
80
|
-
case m[2]
|
81
|
-
when nil
|
82
|
-
@seconds = m[1].to_i
|
83
|
-
when 'm'
|
84
|
-
@seconds = m[1].to_i * 60
|
85
|
-
when 'h'
|
86
|
-
@seconds = m[1].to_i * 60 * 60
|
87
|
-
when 'd'
|
88
|
-
@seconds = m[1].to_i * 24 * 60 * 60
|
89
|
-
end
|
90
|
-
else
|
91
|
-
raise OptionParser::InvalidArgument, "invalid argument: --valid-min #{str}"
|
92
|
-
end
|
93
|
-
@string = str
|
94
|
-
end
|
95
|
-
|
96
|
-
# Get time in seconds
|
97
|
-
# @return [Integer]
|
98
|
-
def to_seconds
|
99
|
-
@seconds
|
100
|
-
end
|
101
|
-
|
102
|
-
# Get time as string
|
103
|
-
# @return [String]
|
104
|
-
def to_s
|
105
|
-
@string
|
106
|
-
end
|
107
|
-
end
|
108
37
|
|
109
38
|
# Exit value for OK
|
110
39
|
RETURN_OK = 1
|
@@ -112,9 +41,12 @@ module LetsCert
|
|
112
41
|
RETURN_OK_CERT = 0
|
113
42
|
# Exit value for error(s)
|
114
43
|
RETURN_ERROR = 2
|
115
|
-
|
44
|
+
|
45
|
+
# Get options
|
46
|
+
# @return [Hash]
|
47
|
+
attr_reader :options
|
116
48
|
# @return [Logger]
|
117
|
-
|
49
|
+
attr_accessor :logger
|
118
50
|
|
119
51
|
# Run LetsCert
|
120
52
|
# @return [Integer]
|
@@ -125,7 +57,6 @@ module LetsCert
|
|
125
57
|
runner.run
|
126
58
|
end
|
127
59
|
|
128
|
-
|
129
60
|
def initialize
|
130
61
|
@options = {
|
131
62
|
verbose: 0,
|
@@ -134,9 +65,9 @@ module LetsCert
|
|
134
65
|
cert_key_size: 2048,
|
135
66
|
valid_min: ValidTime.new('30d'),
|
136
67
|
account_key_size: 4096,
|
137
|
-
tos_sha256: '
|
138
|
-
|
139
|
-
server: 'https://acme-v01.api.letsencrypt.org/directory'
|
68
|
+
tos_sha256: '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f3' \
|
69
|
+
'5226540f',
|
70
|
+
server: 'https://acme-v01.api.letsencrypt.org/directory'
|
140
71
|
}
|
141
72
|
|
142
73
|
@logger = Logger.new($stdout)
|
@@ -148,62 +79,19 @@ module LetsCert
|
|
148
79
|
# * 1 if renewal was not necessery
|
149
80
|
# * 2 in case of errors
|
150
81
|
def run
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
if @options[:show_version]
|
157
|
-
puts "letscert #{LetsCert::VERSION}"
|
158
|
-
puts "Copyright (c) 2016 Sylvain Daubert"
|
159
|
-
puts "License MIT: see http://opensource.org/licenses/MIT"
|
160
|
-
exit RETURN_OK
|
161
|
-
end
|
162
|
-
|
163
|
-
case @options[:verbose]
|
164
|
-
when 0
|
165
|
-
@logger.level = Logger::Severity::WARN
|
166
|
-
when 1
|
167
|
-
@logger.level = Logger::Severity::INFO
|
168
|
-
when 2..5
|
169
|
-
@logger.level = Logger::Severity::DEBUG
|
170
|
-
end
|
171
|
-
|
172
|
-
@logger.debug { "options are: #{@options.inspect}" }
|
173
|
-
|
174
|
-
IOPlugin.logger = @logger
|
175
|
-
Certificate.logger = @logger
|
82
|
+
print_help_if_needed
|
83
|
+
show_version_if_needed
|
84
|
+
set_logger_level
|
85
|
+
set_logger
|
176
86
|
|
177
87
|
begin
|
178
|
-
|
179
|
-
raise Error, "At leat one domain must be given with --domain option.\n" +
|
180
|
-
"Try 'letscert --help' for more information."
|
181
|
-
end
|
182
|
-
|
88
|
+
check_domains
|
183
89
|
if @options[:revoke]
|
184
|
-
|
185
|
-
certificate = Certificate.new(data[:cert])
|
186
|
-
if certificate.revoke(data[:account_key], @options)
|
187
|
-
RETURN_OK
|
188
|
-
else
|
189
|
-
RETURN_ERROR
|
190
|
-
end
|
90
|
+
revoke
|
191
91
|
else
|
192
92
|
check_persisted
|
193
|
-
|
194
|
-
data = load_data_from_disk(@options[:files])
|
195
|
-
|
196
|
-
certificate = Certificate.new(data[:cert])
|
197
|
-
if certificate.valid?(@options[:domains], @options[:valid_min].to_seconds)
|
198
|
-
@logger.info { 'no need to update cert' }
|
199
|
-
RETURN_OK
|
200
|
-
else
|
201
|
-
# update/create cert
|
202
|
-
certificate.get data[:account_key], data[:key], @options
|
203
|
-
RETURN_OK_CERT
|
204
|
-
end
|
93
|
+
get_certificate
|
205
94
|
end
|
206
|
-
|
207
95
|
rescue Error, Acme::Client::Error => ex
|
208
96
|
msg = ex.message
|
209
97
|
msg = "[Acme] #{msg}" if ex.is_a?(Acme::Client::Error)
|
@@ -213,13 +101,13 @@ module LetsCert
|
|
213
101
|
end
|
214
102
|
end
|
215
103
|
|
216
|
-
|
217
104
|
# Parse line command options
|
218
105
|
# @raise [OptionParser::InvalidOption] on unrecognized or malformed option
|
219
106
|
# @return [void]
|
107
|
+
# rubocop:disable Metrics/MethodLength
|
220
108
|
def parse_options
|
221
109
|
@opt_parser = OptionParser.new do |opts|
|
222
|
-
opts.banner =
|
110
|
+
opts.banner = 'Usage: lestcert [options]'
|
223
111
|
|
224
112
|
opts.separator('')
|
225
113
|
|
@@ -229,8 +117,9 @@ module LetsCert
|
|
229
117
|
opts.on('-V', '--version', 'Show version and exit') do |v|
|
230
118
|
@options[:show_version] = v
|
231
119
|
end
|
232
|
-
opts.on('-v', '--verbose', 'Run verbosely')
|
233
|
-
|
120
|
+
opts.on('-v', '--verbose', 'Run verbosely') do |v|
|
121
|
+
@options[:verbose] += 1 if v
|
122
|
+
end
|
234
123
|
|
235
124
|
opts.separator("\nWebroot manager:")
|
236
125
|
|
@@ -252,7 +141,7 @@ module LetsCert
|
|
252
141
|
@options[:revoke] = revoke
|
253
142
|
end
|
254
143
|
|
255
|
-
opts.on(
|
144
|
+
opts.on('-f', '--file FILE', 'Input/output file.',
|
256
145
|
'Can be specified multiple times',
|
257
146
|
'Allowed values: account_key.json, cert.der,',
|
258
147
|
'cert.pem, chain.pem, full.pem,',
|
@@ -269,9 +158,10 @@ module LetsCert
|
|
269
158
|
opts.accept(ValidTime) do |valid_time|
|
270
159
|
ValidTime.new(valid_time)
|
271
160
|
end
|
272
|
-
opts.on('--valid-min TIME', ValidTime,
|
161
|
+
opts.on('--valid-min TIME', ValidTime,
|
162
|
+
'Renew existing certificate if validity',
|
273
163
|
'is lesser than TIME',
|
274
|
-
"(default: #{@options[:valid_min]
|
164
|
+
"(default: #{@options[:valid_min]})") do |vt|
|
275
165
|
@options[:valid_min] = vt
|
276
166
|
end
|
277
167
|
|
@@ -280,12 +170,13 @@ module LetsCert
|
|
280
170
|
end
|
281
171
|
|
282
172
|
opts.separator("\nRegistration:")
|
283
|
-
opts.separator(
|
284
|
-
|
173
|
+
opts.separator(' Automatically register an account with he ACME CA' \
|
174
|
+
' specified by --server')
|
285
175
|
opts.separator('')
|
286
176
|
|
287
177
|
opts.on('--account-key-size BITS', Integer,
|
288
|
-
|
178
|
+
'Account key size (default: ' \
|
179
|
+
"#{@options[:account_key_size]})") do |bits|
|
289
180
|
@options[:account_key_size] = bits
|
290
181
|
end
|
291
182
|
|
@@ -307,11 +198,6 @@ module LetsCert
|
|
307
198
|
opts.separator(' Configure properties of HTTP requests and responses.')
|
308
199
|
opts.separator('')
|
309
200
|
|
310
|
-
opts.on('--user-agent NAME', 'User-Agent sent in all HTTP requests',
|
311
|
-
"(default: #{@options[:user_agent]})") do |ua|
|
312
|
-
@options[:user_agent] = ua
|
313
|
-
end
|
314
|
-
|
315
201
|
opts.on('--server URI', 'URI for the CA ACME API endpoint',
|
316
202
|
"(default: #{@options[:server]})") do |uri|
|
317
203
|
@options[:server] = uri
|
@@ -325,14 +211,8 @@ module LetsCert
|
|
325
211
|
# Check all components are covered by plugins
|
326
212
|
# @raise [Error]
|
327
213
|
def check_persisted
|
328
|
-
persisted =
|
329
|
-
|
330
|
-
@options[:files].each do |file|
|
331
|
-
persisted.merge!(IOPlugin.registered[file].persisted) do |k, oldv, newv|
|
332
|
-
oldv || newv
|
333
|
-
end
|
334
|
-
end
|
335
|
-
not_persisted = persisted.keys.find_all { |k| !persisted[k] }
|
214
|
+
persisted = persisted_data
|
215
|
+
not_persisted = persisted.keys.find_all { |k| persisted[k].nil? }
|
336
216
|
|
337
217
|
unless not_persisted.empty?
|
338
218
|
raise Error, 'Selected IO plugins do not cover following components: ' +
|
@@ -340,9 +220,90 @@ module LetsCert
|
|
340
220
|
end
|
341
221
|
end
|
342
222
|
|
343
|
-
|
344
223
|
private
|
345
224
|
|
225
|
+
# Print help and exit, if +:print_help+ option is set
|
226
|
+
# @return [void]
|
227
|
+
# rubocop:disable Style/GuardClause
|
228
|
+
def print_help_if_needed
|
229
|
+
if @options[:print_help]
|
230
|
+
puts @opt_parser
|
231
|
+
exit RETURN_OK
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Show version and exit, if +:show_version+ option is set
|
236
|
+
# @return [void]
|
237
|
+
def show_version_if_needed
|
238
|
+
if @options[:show_version]
|
239
|
+
puts "letscert #{LetsCert::VERSION}"
|
240
|
+
puts 'Copyright (c) 2016 Sylvain Daubert'
|
241
|
+
puts 'License MIT: see http://opensource.org/licenses/MIT'
|
242
|
+
exit RETURN_OK
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Set logger level from +:verbose+ option
|
247
|
+
# @return [void]
|
248
|
+
def set_logger_level
|
249
|
+
case @options[:verbose]
|
250
|
+
when 0
|
251
|
+
@logger.level = Logger::Severity::WARN
|
252
|
+
when 1
|
253
|
+
@logger.level = Logger::Severity::INFO
|
254
|
+
when 2..5
|
255
|
+
@logger.level = Logger::Severity::DEBUG
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Set logger for IOPlugin and Certificate classes.
|
260
|
+
# @return [void]
|
261
|
+
def set_logger
|
262
|
+
@logger.debug { "options are: #{@options.inspect}" }
|
263
|
+
IOPlugin.logger = @logger
|
264
|
+
Certificate.logger = @logger
|
265
|
+
end
|
266
|
+
|
267
|
+
# Check at least on domain is given.
|
268
|
+
# @return [void]
|
269
|
+
# @raise [Error] no domain given
|
270
|
+
def check_domains
|
271
|
+
if @options[:domains].empty?
|
272
|
+
raise Error, 'At leat one domain must be given with --domain ' \
|
273
|
+
"option.\nTry 'letscert --help' for more information."
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Revoke a certificate
|
278
|
+
# @return [Integer] exit status
|
279
|
+
def revoke
|
280
|
+
data = load_data_from_disk(IOPlugin.registered.keys)
|
281
|
+
certificate = Certificate.new(data[:cert])
|
282
|
+
if certificate.revoke(data[:account_key], @options)
|
283
|
+
RETURN_OK
|
284
|
+
else
|
285
|
+
RETURN_ERROR
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# Create/update a certificate
|
290
|
+
# @return [Integer] exit status
|
291
|
+
# rubocop:disable Style/AccessorMethodName
|
292
|
+
def get_certificate
|
293
|
+
data = load_data_from_disk(@options[:files])
|
294
|
+
|
295
|
+
certificate = Certificate.new(data[:cert])
|
296
|
+
min_time = @options[:valid_min].to_seconds
|
297
|
+
if certificate.valid?(@options[:domains], min_time)
|
298
|
+
@logger.info { 'no need to update cert' }
|
299
|
+
RETURN_OK
|
300
|
+
else
|
301
|
+
# update/create cert
|
302
|
+
certificate.get data[:account_key], data[:key], @options
|
303
|
+
RETURN_OK_CERT
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
346
307
|
# Load existing data from disk
|
347
308
|
# @param [Array<String>] files
|
348
309
|
# @return [Hash]
|
@@ -358,9 +319,9 @@ module LetsCert
|
|
358
319
|
end
|
359
320
|
raise Error unless test
|
360
321
|
|
361
|
-
# Merge data into all_data. New value replace old one only if old
|
362
|
-
# not defined
|
363
|
-
all_data.merge!(data) do |
|
322
|
+
# Merge data into all_data. New value replace old one only if old
|
323
|
+
# one was not defined
|
324
|
+
all_data.merge!(data) do |_key, oldval, newval|
|
364
325
|
oldval || newval
|
365
326
|
end
|
366
327
|
end
|
@@ -387,6 +348,18 @@ module LetsCert
|
|
387
348
|
@options[:roots] = roots
|
388
349
|
end
|
389
350
|
|
351
|
+
def persisted_data
|
352
|
+
persisted = IOPlugin.empty_data
|
353
|
+
@options[:files].each do |file|
|
354
|
+
ioplugin = IOPlugin.registered[file]
|
355
|
+
next if ioplugin.nil?
|
356
|
+
persisted.merge!(ioplugin.persisted) do |_k, oldv, newv|
|
357
|
+
oldv || newv
|
358
|
+
end
|
359
|
+
end
|
360
|
+
persisted
|
361
|
+
end
|
362
|
+
|
390
363
|
end
|
391
364
|
|
392
365
|
end
|