anchor-pki 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +94 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/lib/anchor/auto_cert/configuration.rb +199 -0
- data/lib/anchor/auto_cert/identifier_policy.rb +68 -0
- data/lib/anchor/auto_cert/managed_certificate.rb +63 -0
- data/lib/anchor/auto_cert/manager.rb +200 -0
- data/lib/anchor/auto_cert/policy_check/for_hostname.rb +40 -0
- data/lib/anchor/auto_cert/policy_check/for_ipaddr.rb +48 -0
- data/lib/anchor/auto_cert/policy_check/for_wildcard_hostname.rb +57 -0
- data/lib/anchor/auto_cert/policy_check.rb +37 -0
- data/lib/anchor/auto_cert/railtie.rb +88 -0
- data/lib/anchor/auto_cert/registry.rb +63 -0
- data/lib/anchor/auto_cert/renewal_busy_wait.rb +39 -0
- data/lib/anchor/auto_cert/terms_of_service_acceptor.rb +34 -0
- data/lib/anchor/auto_cert.rb +21 -0
- data/lib/anchor/version.rb +5 -0
- data/lib/anchor-pki.rb +6 -14
- data/lib/anchor.rb +23 -0
- data/lib/puma/dsl.rb +28 -0
- data/lib/puma/plugin/auto_cert.rb +97 -0
- metadata +134 -9
- data/lib/anchor-pki/auto_cert.rb +0 -134
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 342dadd5c28835816da2c07cdd1d4b7b546cc2a35ca80738cc98707d2a048cd4
|
4
|
+
data.tar.gz: 97cb3d7c9c9bbb770c8a3a52eae0690bc7f18cadd5f3a31689d6d00239f5a523
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06b0f93d4bc1962f60a4122bd2f4a046818e36c49779eae9255ff24d6cbb7ba9bd9029bb4f72f516b96c5fa8aef98cea7fbbd6f29ae3b69e42e6cb31e5990ade
|
7
|
+
data.tar.gz: f220a29a0c170f74b8926a1ae804f65d1719a4f9956edc04c6844ac27b2404f15c56908cf5b608a35fec1fe10679b74442ace2b3f6034af3eab814d564da670d
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [0.4.0] - 2023-06-06
|
4
|
+
|
5
|
+
- add a puma plugin and configuration dsl for better integration
|
6
|
+
- auto restart of puma when a certificate is renewed
|
7
|
+
- improve tests
|
8
|
+
- internal refactor to support the puma plugin
|
9
|
+
|
10
|
+
## [0.3.0] - 2023-06-05
|
11
|
+
|
12
|
+
- improve gem packaging
|
13
|
+
- extract out a configuration class for Acme
|
14
|
+
- add tests
|
15
|
+
|
16
|
+
## [0.2.0] - 2023-04-18
|
17
|
+
|
18
|
+
-
|
19
|
+
|
20
|
+
## [0.1.0] - 2021-11-05
|
21
|
+
|
22
|
+
- Initial release
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
anchor-pki (0.2.1)
|
5
|
+
acme-client (~> 2.0.13)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
acme-client (2.0.13)
|
11
|
+
faraday (>= 1.0, < 3.0.0)
|
12
|
+
faraday-retry (>= 1.0, < 3.0.0)
|
13
|
+
addressable (2.8.4)
|
14
|
+
public_suffix (>= 2.0.2, < 6.0)
|
15
|
+
ast (2.4.2)
|
16
|
+
crack (0.4.5)
|
17
|
+
rexml
|
18
|
+
diff-lcs (1.5.0)
|
19
|
+
faraday (2.7.5)
|
20
|
+
faraday-net_http (>= 2.0, < 3.1)
|
21
|
+
ruby2_keywords (>= 0.0.4)
|
22
|
+
faraday-net_http (3.0.2)
|
23
|
+
faraday-retry (2.2.0)
|
24
|
+
faraday (~> 2.0)
|
25
|
+
hashdiff (1.0.1)
|
26
|
+
json (2.6.3)
|
27
|
+
minitest (5.18.0)
|
28
|
+
parallel (1.23.0)
|
29
|
+
parser (3.2.2.1)
|
30
|
+
ast (~> 2.4.1)
|
31
|
+
public_suffix (5.0.1)
|
32
|
+
rainbow (3.1.1)
|
33
|
+
rake (13.0.6)
|
34
|
+
regexp_parser (2.8.0)
|
35
|
+
rexml (3.2.5)
|
36
|
+
rspec (3.12.0)
|
37
|
+
rspec-core (~> 3.12.0)
|
38
|
+
rspec-expectations (~> 3.12.0)
|
39
|
+
rspec-mocks (~> 3.12.0)
|
40
|
+
rspec-core (3.12.2)
|
41
|
+
rspec-support (~> 3.12.0)
|
42
|
+
rspec-expectations (3.12.3)
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
44
|
+
rspec-support (~> 3.12.0)
|
45
|
+
rspec-mocks (3.12.5)
|
46
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
47
|
+
rspec-support (~> 3.12.0)
|
48
|
+
rspec-support (3.12.0)
|
49
|
+
rubocop (1.51.0)
|
50
|
+
json (~> 2.3)
|
51
|
+
parallel (~> 1.10)
|
52
|
+
parser (>= 3.2.0.0)
|
53
|
+
rainbow (>= 2.2.2, < 4.0)
|
54
|
+
regexp_parser (>= 1.8, < 3.0)
|
55
|
+
rexml (>= 3.2.5, < 4.0)
|
56
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
57
|
+
ruby-progressbar (~> 1.7)
|
58
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
59
|
+
rubocop-ast (1.28.1)
|
60
|
+
parser (>= 3.2.1.0)
|
61
|
+
rubocop-capybara (2.18.0)
|
62
|
+
rubocop (~> 1.41)
|
63
|
+
rubocop-factory_bot (2.23.1)
|
64
|
+
rubocop (~> 1.33)
|
65
|
+
rubocop-rspec (2.22.0)
|
66
|
+
rubocop (~> 1.33)
|
67
|
+
rubocop-capybara (~> 2.17)
|
68
|
+
rubocop-factory_bot (~> 2.22)
|
69
|
+
ruby-progressbar (1.13.0)
|
70
|
+
ruby2_keywords (0.0.5)
|
71
|
+
unicode-display_width (2.4.2)
|
72
|
+
vcr (6.1.0)
|
73
|
+
webmock (3.18.1)
|
74
|
+
addressable (>= 2.8.0)
|
75
|
+
crack (>= 0.3.2)
|
76
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
77
|
+
|
78
|
+
PLATFORMS
|
79
|
+
aarch64-linux
|
80
|
+
arm64-darwin-21
|
81
|
+
x86_64-darwin-22
|
82
|
+
|
83
|
+
DEPENDENCIES
|
84
|
+
anchor-pki!
|
85
|
+
minitest (~> 5.14)
|
86
|
+
rake (~> 13.0)
|
87
|
+
rspec (~> 3.9)
|
88
|
+
rubocop (~> 1.50)
|
89
|
+
rubocop-rspec (~> 2.22)
|
90
|
+
vcr (~> 6.1)
|
91
|
+
webmock (~> 3.8)
|
92
|
+
|
93
|
+
BUNDLED WITH
|
94
|
+
2.3.26
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Anchor Security, Inc.
|
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,50 @@
|
|
1
|
+
# Anchor
|
2
|
+
|
3
|
+
Ruby client for Anchor PKI. See https://anchor.dev/ for details.
|
4
|
+
|
5
|
+
## Configuration
|
6
|
+
|
7
|
+
The Following environment variables are available to configure the default
|
8
|
+
[`AutoCert::Manager`](./lib/anchor/auto_cert/manager.rb).
|
9
|
+
|
10
|
+
* `HTTPS_PORT` - the TCP numerical port to bind SSL to.
|
11
|
+
* `ACME_ALLOW_IDENTIFIERS` - A comma separated list of hostnames for provisioning certs
|
12
|
+
* `ACME_DIRECTORY_URL` - the ACME provider's directory
|
13
|
+
* `ACME_KID` - your External Account Binding (EAB) KID for authenticating with the ACME directory above with an
|
14
|
+
* `ACME_HMAC_KEY` - your EAB HMAC_KEY for authenticating with the ACME directory above
|
15
|
+
* `ACME_RENEW_BEFORE_SECONDS` - **optional** Start a renewal this number number of seconds before the cert expires. This defaults to 30 days (2592000 seconds)
|
16
|
+
* `ACME_RENEW_BEFORE_FRACTION` - **optional** Start the renewal when this fraction of a cert's valid window is left. This defaults to 0.5, which means when the cert is in the last 50% of its lifespan a renewal is attempted.
|
17
|
+
* `AUTO_CERT_CHECK_EVERY` - **optional** the number of seconds to wait between checking if the certificate has expired. This defaults to 1 hour (3600 seconds)
|
18
|
+
* `AUTO_CERT_NAME` - **optional** the name to use to lookup the default `AutoCert::Configuration` in the `AutoCert::Registry`. This is `default` by default
|
19
|
+
|
20
|
+
If both `ACME_RENEW_BEFORE_SECONDS` and `ACME_RENEW_BEFORE_FRACTION` are set,
|
21
|
+
the one that causes the renewal to take place earlier is used.
|
22
|
+
|
23
|
+
Example:
|
24
|
+
|
25
|
+
* Cert start (not_before) moment is : `2023-05-24 20:53:11 UTC`
|
26
|
+
* Cert expiration (not_after) moment is : `2023-06-21 20:53:10 UTC`
|
27
|
+
* `ACME_RENEW_BEFORE_SECONDS` is `1209600` (14 days)
|
28
|
+
* `ACME_RENEW_BEFORE_FRACTION` is `0.25` - which equates to a before seconds value of `604799` (~7 days)
|
29
|
+
|
30
|
+
The possible moments to start renewing are:
|
31
|
+
|
32
|
+
* 14 days before expiration moment - `2023-06-07 20:53:10 UTC`
|
33
|
+
* when 25% of the valid time is left - `2023-06-14 20:53:11 UTC`
|
34
|
+
|
35
|
+
Currently the `AutoCert::Manager` will use whichever is earlier.
|
36
|
+
|
37
|
+
### Example configuration
|
38
|
+
|
39
|
+
```sh
|
40
|
+
HTTPS_PORT=44300
|
41
|
+
ACME_ALLOW_IDENTIFIERS=my.lcl.host,*.my.lcl.host
|
42
|
+
ACME_DIRECTORY_URL=https://acme-v02.api.letsencrypt.org/directory
|
43
|
+
ACME_KID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
44
|
+
ACME_HMAC_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
45
|
+
```
|
46
|
+
|
47
|
+
## License
|
48
|
+
|
49
|
+
The gem is available as open source under the terms of the [MIT
|
50
|
+
License](./LICENSE.txt)
|
data/Rakefile
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
# AutoCert Configuration provides a way to configure the AutoCert Manager.
|
6
|
+
#
|
7
|
+
class Configuration
|
8
|
+
DEFAULT_BEFORE_SECONDS = 60 * 60 * 24 * 30 # 30 days in seconds
|
9
|
+
DEFAULT_BEFORE_FRACTION = 0.5 # when in the last 50% of the validity window, renew
|
10
|
+
|
11
|
+
# Note - although it is possible to set change the name of a config, it is
|
12
|
+
# not recommended. The name is used as the key in the Registry, and if a
|
13
|
+
# Configuration is in the Registry, and its name is changed, it does not
|
14
|
+
# change its registry key.
|
15
|
+
attr_accessor :name,
|
16
|
+
:allow_identifiers,
|
17
|
+
:cache,
|
18
|
+
:contact,
|
19
|
+
:directory_url,
|
20
|
+
:external_account_binding,
|
21
|
+
:renew_before_fraction,
|
22
|
+
:renew_before_seconds,
|
23
|
+
:tos_acceptors,
|
24
|
+
:work_dir
|
25
|
+
|
26
|
+
# rubocop:disable Metrics/ParameterLists
|
27
|
+
# Data defined classes have all required parameters in the initializer, so
|
28
|
+
# override the default initializer to allow for optional parameters and
|
29
|
+
# to pull in the defaults form the environment
|
30
|
+
#
|
31
|
+
def initialize(name:,
|
32
|
+
allow_identifiers: nil,
|
33
|
+
cache: nil,
|
34
|
+
contact: nil,
|
35
|
+
directory_url: nil,
|
36
|
+
external_account_binding: nil,
|
37
|
+
renew_before_fraction: DEFAULT_BEFORE_FRACTION,
|
38
|
+
renew_before_seconds: DEFAULT_BEFORE_SECONDS,
|
39
|
+
tos_acceptors: nil,
|
40
|
+
work_dir: nil)
|
41
|
+
|
42
|
+
@name = name
|
43
|
+
|
44
|
+
@allow_identifiers = allow_identifiers
|
45
|
+
@cache = cache
|
46
|
+
@contact = contact
|
47
|
+
@directory_url = directory_url
|
48
|
+
@external_account_binding = external_account_binding
|
49
|
+
@renew_before_fraction = renew_before_fraction
|
50
|
+
@renew_before_seconds = renew_before_seconds
|
51
|
+
@tos_acceptors = tos_acceptors
|
52
|
+
@work_dir = work_dir
|
53
|
+
end
|
54
|
+
# rubocop:enable Metrics/ParameterLists
|
55
|
+
|
56
|
+
def account
|
57
|
+
{
|
58
|
+
contact: contact,
|
59
|
+
external_account_binding: external_account_binding
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Enabled just means that the configuration is valid
|
64
|
+
def enabled?
|
65
|
+
validate!
|
66
|
+
true
|
67
|
+
rescue ConfigurationError => _e
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def validate!
|
72
|
+
@allow_identifiers = prepare_allow_identifiers(@allow_identifiers)
|
73
|
+
@cache = prepare_cache(@cache)
|
74
|
+
@directory_url = prepare_directory_url(@directory_url)
|
75
|
+
@external_account_binding = prepare_external_account_binding(@external_account_binding)
|
76
|
+
@renew_before_fraction = prepare_renew_before_fraction(@renew_before_fraction)
|
77
|
+
@renew_before_seconds = prepare_renew_before_seconds(@renew_before_seconds)
|
78
|
+
@tos_acceptors = prepare_tos_acceptors(@tos_acceptors)
|
79
|
+
@work_dir = prepare_work_dir(@work_dir)
|
80
|
+
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def prepare_allow_identifiers(allow_identifiers)
|
87
|
+
prepared = case allow_identifiers
|
88
|
+
when Array
|
89
|
+
allow_identifiers
|
90
|
+
when String
|
91
|
+
allow_identifiers.split(',')
|
92
|
+
when nil
|
93
|
+
ENV.fetch('ACME_ALLOW_IDENTIFIERS', nil)&.split(',')
|
94
|
+
end
|
95
|
+
|
96
|
+
if prepared.nil? || prepared.empty?
|
97
|
+
raise ConfigurationError,
|
98
|
+
"The '#{name}' #{self.class} instance has a misconfigured `allow_identifiers` value. " \
|
99
|
+
'Set it to a string, or an array of strings, ' \
|
100
|
+
'or set the ACME_ALLOW_IDENTIFIERS environment variable to a comma separated list of identifiers.'
|
101
|
+
end
|
102
|
+
|
103
|
+
prepared
|
104
|
+
end
|
105
|
+
|
106
|
+
def prepare_cache(cache)
|
107
|
+
return nil if cache.nil?
|
108
|
+
|
109
|
+
unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:fetch)
|
110
|
+
raise ConfigurationError,
|
111
|
+
"The '#{name}' #{self.class} instance has a misconfigured `cache` value. " \
|
112
|
+
'It must be set to an object that responds to `read`, `write`, and `fetch`.'
|
113
|
+
end
|
114
|
+
|
115
|
+
cache
|
116
|
+
end
|
117
|
+
|
118
|
+
def prepare_directory_url(directory_url)
|
119
|
+
directory_url ||= ENV.fetch('ACME_DIRECTORY_URL', nil)
|
120
|
+
if directory_url.nil?
|
121
|
+
raise ConfigurationError,
|
122
|
+
"The '#{name}' #{self.class} instance has a misconfigured `directory_url` value. " \
|
123
|
+
'It must be set to a string, or set the ACME_DIRECTORY_URL environment variable.'
|
124
|
+
end
|
125
|
+
|
126
|
+
directory_url
|
127
|
+
end
|
128
|
+
|
129
|
+
def prepare_external_account_binding(external_account_binding)
|
130
|
+
kid = ENV.fetch('ACME_KID', nil)
|
131
|
+
hmac_key = ENV.fetch('ACME_HMAC_KEY', nil)
|
132
|
+
|
133
|
+
if kid && hmac_key
|
134
|
+
external_account_binding = {
|
135
|
+
kid: kid,
|
136
|
+
hmac_key: hmac_key
|
137
|
+
}
|
138
|
+
end
|
139
|
+
external_account_binding
|
140
|
+
end
|
141
|
+
|
142
|
+
def prepare_renew_before_seconds(renew_before_seconds)
|
143
|
+
renew_before_seconds ||= ENV.fetch('ACME_RENEW_BEFORE_SECONDS', nil)
|
144
|
+
if renew_before_seconds
|
145
|
+
renew_before_seconds = renew_before_seconds.to_i
|
146
|
+
unless renew_before_seconds.positive?
|
147
|
+
raise ConfigurationError,
|
148
|
+
"The '#{name}' #{self.class} instance has a misconfigured `before_seconds` value. " \
|
149
|
+
'It must be set to an integer > 0, or set the ACME_RENEW_BEFORE_SECONDS environment variable.'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
renew_before_seconds
|
153
|
+
end
|
154
|
+
|
155
|
+
def prepare_renew_before_fraction(renew_before_fraction)
|
156
|
+
renew_before_fraction ||= ENV.fetch('ACME_RENEW_BEFORE_FRACTION', nil)
|
157
|
+
if renew_before_fraction
|
158
|
+
renew_before_fraction = renew_before_fraction.to_f
|
159
|
+
unless (0..1).cover?(renew_before_fraction)
|
160
|
+
raise ConfigurationError,
|
161
|
+
"The '#{name}' #{self.class} instance has a misconfigured `before_fraction` value. " \
|
162
|
+
'It must be set to a float > 0 and < 1, or set the ACME_RENEW_BEFORE_FRACTION environment variable.'
|
163
|
+
end
|
164
|
+
end
|
165
|
+
renew_before_fraction
|
166
|
+
end
|
167
|
+
|
168
|
+
def prepare_tos_acceptors(tos_acceptors)
|
169
|
+
tos_acceptors = Array(tos_acceptors)
|
170
|
+
|
171
|
+
if tos_acceptors.empty? || tos_acceptors.any? { |tos| !tos.respond_to?(:accept?) }
|
172
|
+
raise ConfigurationError,
|
173
|
+
"The '#{name}' #{self.class} instance has a misconfigured `tos_acceptors` value. " \
|
174
|
+
'It must be set to an object or an array of objects that respond to `accept?`.'
|
175
|
+
end
|
176
|
+
|
177
|
+
tos_acceptors
|
178
|
+
end
|
179
|
+
|
180
|
+
def prepare_work_dir(work_dir)
|
181
|
+
return nil if work_dir.nil?
|
182
|
+
|
183
|
+
work_dir = Pathname.new(work_dir) unless work_dir.is_a?(Pathname)
|
184
|
+
|
185
|
+
begin
|
186
|
+
work_dir.mkpath
|
187
|
+
rescue StandardError => e
|
188
|
+
raise ConfigurationError, "#{self.class}#work_dir : #{e.message}"
|
189
|
+
end
|
190
|
+
|
191
|
+
unless work_dir.directory? && work_dir.writable?
|
192
|
+
raise ConfigurationError, "#{self.class}#work_dir '#{work_dir}' must be a writable directory."
|
193
|
+
end
|
194
|
+
|
195
|
+
work_dir
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
#
|
6
|
+
# IdentifierPolicy is a class used to check that the identifiers used in
|
7
|
+
# certs would be valid.
|
8
|
+
#
|
9
|
+
# Each IdentierPolicy is initialized with a 'policy_description' which is used to
|
10
|
+
# derive the policy check.
|
11
|
+
#
|
12
|
+
# Current Policy Checks are:
|
13
|
+
# - ForHostname - checks that the identifier matches hostname exactly
|
14
|
+
# - ForWildcardHostname - checks that the identifier matches hostname with a wildcard prefix
|
15
|
+
# - ForIpAddress - checks that the identifier matches an IP address or subnet
|
16
|
+
#
|
17
|
+
class IdentifierPolicy
|
18
|
+
attr_reader :description, :check
|
19
|
+
|
20
|
+
# Given an individual, or an array of IdentifierPolicy or Strings build
|
21
|
+
# IdentifierPolicy objects
|
22
|
+
def self.build(policy_descriptions)
|
23
|
+
Array(policy_descriptions).map do |description|
|
24
|
+
IdentifierPolicy.new(description)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.new(description)
|
29
|
+
return description if description.is_a?(IdentifierPolicy)
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
# The list of policy checks that are available, the ordering here is
|
35
|
+
# important as the first one that matches is the one that is used for the
|
36
|
+
# check. So if a policy description would be matched by multiple checks,
|
37
|
+
# the one that it should match should be first.
|
38
|
+
def self.policy_checks
|
39
|
+
@policy_checks ||= [
|
40
|
+
PolicyCheck::ForIPAddr,
|
41
|
+
PolicyCheck::ForHostname,
|
42
|
+
PolicyCheck::ForWildcardHostname
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(description)
|
47
|
+
check_klass = self.class.policy_checks.find do |klass|
|
48
|
+
klass.handles?(description)
|
49
|
+
end
|
50
|
+
raise UnknownPolicyCheckError, "Unable to create a policy check based upon '#{description}'" if check_klass.nil?
|
51
|
+
|
52
|
+
@description = description
|
53
|
+
@check = check_klass.new(description)
|
54
|
+
end
|
55
|
+
|
56
|
+
def allow?(identifier)
|
57
|
+
raise ArgumentError, 'identifier must be a String' unless identifier.is_a?(String)
|
58
|
+
|
59
|
+
@check.allow?(identifier)
|
60
|
+
end
|
61
|
+
|
62
|
+
def deny?(identifier)
|
63
|
+
!allow?(identifier)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
require_relative 'policy_check'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Anchor
|
6
|
+
module AutoCert
|
7
|
+
# ManagedCertificate is a class that represents a certificate and its manager
|
8
|
+
# for renewal
|
9
|
+
class ManagedCertificate
|
10
|
+
attr_reader :cert_pem, :cert_path, :key_pem, :key_path, :key, :manager, :x509
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
def_delegators :@manager, :enabled?
|
14
|
+
def_delegators :@x509, :not_after, :not_before, :serial
|
15
|
+
|
16
|
+
def self.from(manager:, cert_path:, key_path:)
|
17
|
+
cert_pem = File.read(cert_path)
|
18
|
+
key_pem = File.read(key_path)
|
19
|
+
new(manager: manager, cert_pem: cert_pem, key_pem: key_pem)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(manager:, cert_pem:, key_pem:)
|
23
|
+
@manager = manager
|
24
|
+
@cert_pem = cert_pem
|
25
|
+
@key_pem = key_pem
|
26
|
+
@x509 = OpenSSL::X509::Certificate.new(cert_pem)
|
27
|
+
|
28
|
+
hex_serial_basename = hex_serial('-')
|
29
|
+
@cert_path = manager.work_dir / "#{hex_serial_basename}.crt"
|
30
|
+
@key_path = manager.work_dir / "#{hex_serial_basename}.key"
|
31
|
+
|
32
|
+
write_working_files
|
33
|
+
end
|
34
|
+
|
35
|
+
def hex_serial(joiner = ':')
|
36
|
+
x509.serial.to_s(16).scan(/.{2}/).join(joiner)
|
37
|
+
end
|
38
|
+
|
39
|
+
def needs_renewal?(now = Time.now.utc)
|
40
|
+
manager.needs_renewal?(cert: x509, now: now)
|
41
|
+
end
|
42
|
+
|
43
|
+
def identifiers
|
44
|
+
alt_names = x509&.extensions&.find { |ext| ext.oid == 'subjectAltName' }&.value&.split(', ') || []
|
45
|
+
alt_names.map { |name| name.sub(/^DNS:/, '') }
|
46
|
+
end
|
47
|
+
|
48
|
+
def common_name
|
49
|
+
x509.subject.to_a.find { |name, _, _| name == 'CN' }[1]
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_working_files
|
53
|
+
cert_path.open('w') { |f| f << cert_pem }
|
54
|
+
key_path.open('w') { |f| f << key_pem }
|
55
|
+
end
|
56
|
+
|
57
|
+
def purge_working_files
|
58
|
+
cert_path.delete if cert_path.exist?
|
59
|
+
key_path.delete if key_path.exist?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|