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
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acme/client'
|
4
|
+
require 'uri'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'pathname'
|
7
|
+
require 'forwardable'
|
8
|
+
|
9
|
+
module Anchor
|
10
|
+
module AutoCert
|
11
|
+
# AutoCert Manager provides automatic access to certificates from Anchor, but
|
12
|
+
# theoretically it could work with Let's Encrypt or any other ACME-based CA.
|
13
|
+
class Manager
|
14
|
+
FALLBACK_RENEW_BEFORE_SECONDS = 24 * 60 * 60 # 1 day
|
15
|
+
|
16
|
+
extend Forwardable
|
17
|
+
def_delegators :@configuration,
|
18
|
+
:cache,
|
19
|
+
:contact,
|
20
|
+
:directory,
|
21
|
+
:external_account_binding,
|
22
|
+
:renew_before_fraction,
|
23
|
+
:renew_before_seconds
|
24
|
+
|
25
|
+
attr_reader :client,
|
26
|
+
:configuration,
|
27
|
+
:directory_url,
|
28
|
+
:identifier_policies,
|
29
|
+
:tos_acceptors,
|
30
|
+
:work_dir
|
31
|
+
|
32
|
+
def self.for(configuration)
|
33
|
+
new(configuration: configuration)
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(configuration:, client: nil)
|
37
|
+
configuration.validate!
|
38
|
+
@configuration = configuration
|
39
|
+
|
40
|
+
@identifier_policies = IdentifierPolicy.build(@configuration.allow_identifiers)
|
41
|
+
@tos_acceptors = Array(@configuration.tos_acceptors)
|
42
|
+
@directory_url = URI.parse(@configuration.directory_url)
|
43
|
+
@work_dir = Pathname.new(@configuration.work_dir || Dir.mktmpdir)
|
44
|
+
|
45
|
+
@account_opts = {
|
46
|
+
contact: @configuration.contact,
|
47
|
+
external_account_binding: @configuration.external_account_binding
|
48
|
+
}
|
49
|
+
|
50
|
+
@client = client || new_client(contact: @configuration.contact)
|
51
|
+
@enabled = true
|
52
|
+
end
|
53
|
+
|
54
|
+
# It is currently assumed that the common name is the first of the
|
55
|
+
# `identifiers` passed into this method. If that is not the case, then
|
56
|
+
# the `common_name` parameter needs to be set explicitly
|
57
|
+
def managed_certificate(identifiers:, algorithm: :ecdsa, common_name: Array(identifiers).first, **opts)
|
58
|
+
if (arr = denied_identifiers(identifiers)).size.positive?
|
59
|
+
raise IdentifierNotAllowedError, "denied identifiers'#{arr.join(',')}'"
|
60
|
+
end
|
61
|
+
|
62
|
+
key_pem = cache&.read("#{common_name}+#{algorithm}")
|
63
|
+
cert_pem = cache&.read(common_name)
|
64
|
+
|
65
|
+
if key_pem.nil? || cert_pem.nil?
|
66
|
+
cert_pem, key_pem = provision(identifiers: identifiers, algorithm: algorithm, common_name: common_name,
|
67
|
+
**opts)
|
68
|
+
|
69
|
+
cache&.write("#{common_name}+#{algorithm}", key_pem)
|
70
|
+
cache&.write(common_name, cert_pem)
|
71
|
+
end
|
72
|
+
|
73
|
+
ManagedCertificate.new(manager: self, cert_pem: cert_pem, key_pem: key_pem)
|
74
|
+
end
|
75
|
+
|
76
|
+
def needs_renewal?(cert:, now: Time.now.utc)
|
77
|
+
renew_after = [
|
78
|
+
renew_after_from_seconds(cert: cert),
|
79
|
+
renew_after_from_fraction(cert: cert),
|
80
|
+
renew_after_fallback(cert: cert),
|
81
|
+
cert.not_after # cert expired, get a new one
|
82
|
+
].compact.min
|
83
|
+
|
84
|
+
(now > renew_after)
|
85
|
+
end
|
86
|
+
|
87
|
+
def disable
|
88
|
+
@enabled = false
|
89
|
+
end
|
90
|
+
|
91
|
+
# if the manager is enabled && the configuration is enabled
|
92
|
+
def enabled?
|
93
|
+
@enabled && configuration.enabled?
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def provision(identifiers:, algorithm:, common_name:, **opts)
|
99
|
+
identifiers = Array(identifiers)
|
100
|
+
load_or_build_account
|
101
|
+
key_pem ||= new_key(algorithm).to_pem
|
102
|
+
csr = Acme::Client::CertificateRequest.new(private_key: parse_key_pem(key_pem), common_name: common_name,
|
103
|
+
names: identifiers)
|
104
|
+
|
105
|
+
order = @client.new_order(identifiers: identifiers, **opts)
|
106
|
+
order.finalize(csr: csr)
|
107
|
+
# TODO: loop over order.authorizations and process the challenges
|
108
|
+
|
109
|
+
while order.status == 'processing'
|
110
|
+
sleep(1)
|
111
|
+
order.reload
|
112
|
+
end
|
113
|
+
|
114
|
+
[order.certificate, key_pem]
|
115
|
+
end
|
116
|
+
|
117
|
+
def account
|
118
|
+
@account ||= build_account(**@account_opts)
|
119
|
+
end
|
120
|
+
alias load_or_build_account account
|
121
|
+
|
122
|
+
def build_account(contact: nil, external_account_binding: nil, terms_of_service_agreed: false, **)
|
123
|
+
terms_of_service_agreed ||= @tos_acceptors.any? { |a| a.accept?(@client.terms_of_service) }
|
124
|
+
|
125
|
+
@client.new_account(contact: contact, terms_of_service_agreed: terms_of_service_agreed,
|
126
|
+
external_account_binding: external_account_binding)
|
127
|
+
end
|
128
|
+
|
129
|
+
def renew_after_from_seconds(cert:, before_seconds: renew_before_seconds)
|
130
|
+
return nil unless before_seconds
|
131
|
+
|
132
|
+
renew_after = (cert.not_after - before_seconds)
|
133
|
+
|
134
|
+
# invalid if the renewal time is outside the certificate validity window
|
135
|
+
return nil unless (cert.not_before..cert.not_after).cover?(renew_after)
|
136
|
+
|
137
|
+
renew_after
|
138
|
+
end
|
139
|
+
|
140
|
+
def renew_after_from_fraction(cert:, before_fraction: renew_before_fraction)
|
141
|
+
return nil unless (0..1).cover?(before_fraction)
|
142
|
+
|
143
|
+
valid_span = cert.not_after - cert.not_before
|
144
|
+
before_seconds = (valid_span * before_fraction).floor
|
145
|
+
|
146
|
+
renew_after_from_seconds(cert: cert, before_seconds: before_seconds)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Fallback timestamp, in case there is some corner case that is not covered
|
150
|
+
def renew_after_fallback(cert:)
|
151
|
+
renew_after_from_seconds(cert: cert, before_seconds: FALLBACK_RENEW_BEFORE_SECONDS)
|
152
|
+
end
|
153
|
+
|
154
|
+
def new_client(account_key: nil, contact: nil, **)
|
155
|
+
account_key ||= fetch_account_key(contact) { new_key(:ecdsa) }
|
156
|
+
|
157
|
+
Acme::Client.new(private_key: account_key, directory: @directory_url)
|
158
|
+
end
|
159
|
+
|
160
|
+
def new_key(algorithm)
|
161
|
+
case algorithm
|
162
|
+
when :ecdsa then OpenSSL::PKey::EC.generate('prime256v1')
|
163
|
+
else
|
164
|
+
raise UnknownAlgorithmError, "unknown key algorithm '#{algorithm}'"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def fetch_account_key(contact)
|
169
|
+
id = "#{contact || 'default'}+#{@directory_url.host}+key"
|
170
|
+
parse_key_pem(cache&.fetch(id) { parse_key_pem(yield.to_pem) } || yield.to_pem)
|
171
|
+
end
|
172
|
+
|
173
|
+
def parse_key_pem(data)
|
174
|
+
key_pem = parse_rsa_pem(data) || parse_ecdsa_pem(data)
|
175
|
+
raise UnknownKeyFormatError unless key_pem
|
176
|
+
|
177
|
+
key_pem
|
178
|
+
end
|
179
|
+
|
180
|
+
def parse_rsa_pem(data)
|
181
|
+
OpenSSL::PKey::RSA.new(data)
|
182
|
+
rescue StandardError
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
|
186
|
+
def parse_ecdsa_pem(data)
|
187
|
+
OpenSSL::PKey::EC.new(data)
|
188
|
+
rescue StandardError
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
# returns all those identifiers that were not accepted by any policy
|
193
|
+
def denied_identifiers(identifiers)
|
194
|
+
Array(identifiers).reject do |identifier|
|
195
|
+
@identifier_policies.any? { |policy| policy.allow?(identifier) }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
class PolicyCheck
|
6
|
+
# Check the identifier by strict hostname name comparison
|
7
|
+
#
|
8
|
+
# Reference: http://www.faqs.org/rfcs/rfc2396.html
|
9
|
+
#
|
10
|
+
class ForHostname < PolicyCheck
|
11
|
+
ALPHA = '[a-zA-Z]'
|
12
|
+
ALPHA_NUMERIC = '[a-zA-Z0-9]'
|
13
|
+
ALPHA_NUMERIC_HYPHEN = '[-a-zA-Z0-9]'
|
14
|
+
DOMAIN_LABEL = "#{ALPHA_NUMERIC}#{ALPHA_NUMERIC_HYPHEN}*#{ALPHA_NUMERIC}"
|
15
|
+
TOP_LEVEL_DOMAIN = "#{ALPHA}#{ALPHA_NUMERIC_HYPHEN}*#{ALPHA_NUMERIC}"
|
16
|
+
|
17
|
+
REGEX = /
|
18
|
+
\A
|
19
|
+
(?<sub>(#{DOMAIN_LABEL}\.)+)
|
20
|
+
(?<tld>#{TOP_LEVEL_DOMAIN})
|
21
|
+
\z
|
22
|
+
/ix.freeze
|
23
|
+
|
24
|
+
def self.handles?(description)
|
25
|
+
description.is_a?(String) && description.match?(REGEX)
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(description)
|
29
|
+
super
|
30
|
+
@hostname = description.downcase
|
31
|
+
end
|
32
|
+
|
33
|
+
# case insensitive comparison
|
34
|
+
def allow?(name)
|
35
|
+
name.is_a?(String) && (name.downcase == @hostname)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
module Anchor
|
5
|
+
module AutoCert
|
6
|
+
class PolicyCheck
|
7
|
+
#
|
8
|
+
# A PolicyCheck that compares the given IP address to an ipaddress
|
9
|
+
# or subnet
|
10
|
+
#
|
11
|
+
# The description for an IPAddr policy check can be anything that is
|
12
|
+
# parsed by the ruby IPAddr.new() method. Generally this is something in
|
13
|
+
# the format an ipv4 address, an ipv6 address, or CIDR notation.
|
14
|
+
#
|
15
|
+
# - "192.168.42.1"
|
16
|
+
# - "192.168.42.0/24"
|
17
|
+
# - "3ffe:505:2::1"
|
18
|
+
# - "2001:db8::/32"
|
19
|
+
#
|
20
|
+
class ForIPAddr < PolicyCheck
|
21
|
+
def self.handles?(description)
|
22
|
+
return true if description.is_a?(IPAddr)
|
23
|
+
|
24
|
+
begin
|
25
|
+
IPAddr.new(description)
|
26
|
+
true
|
27
|
+
rescue IPAddr::Error
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(description)
|
33
|
+
super
|
34
|
+
|
35
|
+
@ipaddr = if description.is_a?(IPAddr)
|
36
|
+
description
|
37
|
+
else
|
38
|
+
IPAddr.new(description)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def allow?(name)
|
43
|
+
@ipaddr.include?(name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
class PolicyCheck
|
6
|
+
# Check that the identifier is allowed by wildcard hostname matching
|
7
|
+
#
|
8
|
+
# This does the same as the ForHostname check, but allows for an arbitrary
|
9
|
+
# prefixes, although the prefix itself also needs to match appropriate
|
10
|
+
# domain name rules.
|
11
|
+
#
|
12
|
+
# The description for a wildcard hostname check MUST be a string that starts
|
13
|
+
# with `*.` followed by a valid domainname (which matches the ForHostname
|
14
|
+
# check)
|
15
|
+
#
|
16
|
+
#
|
17
|
+
class ForWildcardHostname < PolicyCheck
|
18
|
+
DOMAIN_LABEL_REGEX = /\A#{ForHostname::DOMAIN_LABEL}*\z/i.freeze
|
19
|
+
SPLAT = '*'
|
20
|
+
|
21
|
+
def self.handles?(description)
|
22
|
+
return false unless description.is_a?(String)
|
23
|
+
|
24
|
+
parts = description.split('.')
|
25
|
+
return false unless parts[0] == SPLAT
|
26
|
+
|
27
|
+
suffix = parts[1..-1].join('.')
|
28
|
+
# reuse the hostname check here
|
29
|
+
ForHostname.handles?(suffix)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(description)
|
33
|
+
super
|
34
|
+
@parts = description.split('.')
|
35
|
+
@wildcard = @parts.shift # assumed SPLAT
|
36
|
+
@suffix = @parts.join('.').downcase
|
37
|
+
end
|
38
|
+
|
39
|
+
# An explicit '*.rest.of' is an allowable hostname for this check, even
|
40
|
+
# though it is not a valid domain name
|
41
|
+
#
|
42
|
+
def allow?(hostname)
|
43
|
+
return false unless hostname.is_a?(String)
|
44
|
+
|
45
|
+
parts = hostname.split('.')
|
46
|
+
prefix = parts.shift
|
47
|
+
|
48
|
+
return false unless (prefix == SPLAT) || DOMAIN_LABEL_REGEX.match?(prefix)
|
49
|
+
|
50
|
+
domain = parts.join('.').downcase
|
51
|
+
|
52
|
+
(domain == @suffix)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
# Base class for PolicyCheck classes, these are utility classes used by the
|
6
|
+
# IdentifierPolicy class.
|
7
|
+
#
|
8
|
+
# The .handles? method is used to determine if an instance of this class
|
9
|
+
# could be created from the given description.
|
10
|
+
#
|
11
|
+
# Then the #allow? method is used to determine if the given identifier is allowed
|
12
|
+
# and the #deny? method is used to determine if the given identifier is denied.
|
13
|
+
#
|
14
|
+
class PolicyCheck
|
15
|
+
def self.handles?(description)
|
16
|
+
raise NotImplementedError, "#{self.class} must implement .handles?(description)"
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :policy_description
|
20
|
+
|
21
|
+
def initialize(description)
|
22
|
+
@policy_description = description
|
23
|
+
end
|
24
|
+
|
25
|
+
def deny?(identifier)
|
26
|
+
!allow?(identifier)
|
27
|
+
end
|
28
|
+
|
29
|
+
def allow?(identifier)
|
30
|
+
raise NotImplementedError, "#{self.class} must implement #allow?(identifier)"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
require_relative 'policy_check/for_ipaddr'
|
36
|
+
require_relative 'policy_check/for_hostname'
|
37
|
+
require_relative 'policy_check/for_wildcard_hostname'
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
# AutoCert Railtie
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
# Initialize the configuration with a blank configuration, ensuring
|
8
|
+
# the configuration exists, even if it is not used.
|
9
|
+
config.auto_cert = ::Anchor::AutoCert::Configuration.new(name: :rails)
|
10
|
+
|
11
|
+
# Make sure the auto cert configuration is valid before the app boots
|
12
|
+
# This will run after every code reload in development and after boot in
|
13
|
+
# production
|
14
|
+
config.to_prepare do
|
15
|
+
if Rails.configuration.auto_cert.enabled?
|
16
|
+
Rails.configuration.auto_cert.validate!
|
17
|
+
|
18
|
+
# register the configuration under its name so that it can
|
19
|
+
# be discovered by other parts of the application
|
20
|
+
auto_cert_config = Rails.configuration.auto_cert
|
21
|
+
unless ::Anchor::AutoCert::Registry.key?(auto_cert_config.name)
|
22
|
+
::Anchor::AutoCert::Registry.store(auto_cert_config.name, auto_cert_config)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# this needs to be after the load_config_initializers so that the
|
28
|
+
# application can override the :rails auto_cert configuration
|
29
|
+
#
|
30
|
+
initializer 'auto_cert.configure_rails_initialization', after: :load_config_initializers do |app|
|
31
|
+
auto_cert_config = Railtie.determine_configuration(app)
|
32
|
+
app.config.auto_cert = auto_cert_config
|
33
|
+
|
34
|
+
# Update the app.config.hosts with the allow_identifiers if we are NOT
|
35
|
+
# in the test environment.
|
36
|
+
#
|
37
|
+
# In the test environment `config.hosts` is normally empty, and as a
|
38
|
+
# result HostAuthorization is not used. If we add the allow_identifiers
|
39
|
+
# to the `config.hosts` then HostAuthorization will be used, and tests
|
40
|
+
# will break.
|
41
|
+
unless Rails.env.test?
|
42
|
+
auto_cert_config&.allow_identifiers&.each do |identifier|
|
43
|
+
# need to convert an identifier into a host matcher, which is just
|
44
|
+
# strip off a leading '*' if it exists so that all subdomains match.
|
45
|
+
#
|
46
|
+
# https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization
|
47
|
+
host = identifier.to_s.sub(/^\*/, '')
|
48
|
+
app.config.hosts << host
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.determine_configuration(app)
|
54
|
+
auto_cert_config = app.config.auto_cert
|
55
|
+
|
56
|
+
# If no configuration is set, then try to lookup one under the :rails
|
57
|
+
# key or create a default one.
|
58
|
+
begin
|
59
|
+
auto_cert_config ||= ::Anchor::AutoCert::Registry.fetch(:rails)
|
60
|
+
rescue KeyError
|
61
|
+
auto_cert_config = Railtie.try_to_create_default_configuration
|
62
|
+
end
|
63
|
+
|
64
|
+
return nil unless auto_cert_config
|
65
|
+
|
66
|
+
# Set some reasonable defaults for a scratch locations if they are not
|
67
|
+
# set explicitly.
|
68
|
+
acme_scratch_dir = app.root / 'tmp' / 'acme'
|
69
|
+
acme_scratch_dir.mkpath
|
70
|
+
auto_cert_config.cache ||= ActiveSupport::Cache::FileStore.new(acme_scratch_dir / 'cache')
|
71
|
+
auto_cert_config.work_dir ||= (acme_scratch_dir / 'work')
|
72
|
+
|
73
|
+
auto_cert_config
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.try_to_create_default_configuration
|
77
|
+
# If it doesn't exist, create a new one - now this may raise an error
|
78
|
+
# if the configuration is not setup correctly
|
79
|
+
::Anchor::AutoCert::Configuration.new(name: :rails)
|
80
|
+
rescue ConfigurationError => e
|
81
|
+
# its fine to not have a coniguration, just log the error and move on
|
82
|
+
msg = "[AutoCert] Unable to create the :rails configuration : #{e.message}"
|
83
|
+
Rails.logger.error(msg)
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
# A global registry for storing AutoCert::Configuration objects
|
6
|
+
class Registry
|
7
|
+
@_registry = {}
|
8
|
+
@_mutex = Mutex.new
|
9
|
+
|
10
|
+
def self.store(name, configuration)
|
11
|
+
@_mutex.synchronize do
|
12
|
+
@_registry.store(name.to_s, configuration)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.fetch(name)
|
17
|
+
@_mutex.synchronize do
|
18
|
+
@_registry.fetch(name.to_s)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.key?(name)
|
23
|
+
@_mutex.synchronize do
|
24
|
+
@_registry.key?(name.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.delete(name)
|
29
|
+
@_mutex.synchronize do
|
30
|
+
@_registry.delete(name.to_s)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# helpers for creating an AutoCert::Manager instance from registered
|
35
|
+
# configuration
|
36
|
+
def self.manager_for(name)
|
37
|
+
manager_for!(name)
|
38
|
+
rescue KeyError
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# helpers for creating an AutoCert::Manager instance from registered
|
43
|
+
# configuration - raises KeyError if the named configuration does not
|
44
|
+
# exist
|
45
|
+
def self.manager_for!(name)
|
46
|
+
configuration = fetch(name)
|
47
|
+
AutoCert::Manager.new(configuration: configuration)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.default_configuration
|
51
|
+
fetch(:default)
|
52
|
+
rescue KeyError
|
53
|
+
default = AutoCert::Configuration.new
|
54
|
+
store(:default, default)
|
55
|
+
default
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.default_manager
|
59
|
+
manager_for(:default)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
# RenewalBusyWait is a class that will loop to check if a certificate needs to
|
6
|
+
# to be renewed, and if it does, return
|
7
|
+
#
|
8
|
+
# Generally this class should be used inside its own thread as it will sleep
|
9
|
+
# and loop until the pem file is able to be renewed.
|
10
|
+
#
|
11
|
+
# Every 'check_every' interval it will check if the certificate needs to be
|
12
|
+
# renewed. If it does, the loop will end and `wait_for_it` will return. If
|
13
|
+
# it does not need renewal, the block will be called, and if the result of
|
14
|
+
# the block is falsy, the loop will exit early
|
15
|
+
#
|
16
|
+
class RenewalBusyWait
|
17
|
+
ONE_HOUR = 60 * 60
|
18
|
+
|
19
|
+
def self.wait_for_it(managed_certificate:, check_every: ONE_HOUR, &keep_going)
|
20
|
+
waiter = new(managed_certificate: managed_certificate, check_every: check_every)
|
21
|
+
waiter.wait_for_it(&keep_going)
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(managed_certificate:, check_every: ONE_HOUR)
|
25
|
+
@managed_certificate = managed_certificate
|
26
|
+
@check_every = check_every
|
27
|
+
end
|
28
|
+
|
29
|
+
def wait_for_it
|
30
|
+
loop do
|
31
|
+
break if @managed_certificate.needs_renewal?
|
32
|
+
break unless yield
|
33
|
+
|
34
|
+
sleep @check_every
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
# Any class that implements the following interface can be used as a
|
6
|
+
# Terms of Service Acceptor. The interface is the single method `#accept?`
|
7
|
+
# which is handed the terms of service URI as a String.
|
8
|
+
module TermsOfServiceAcceptor
|
9
|
+
def accept?(tos_uri)
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #accept?(tos_uri)"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Terms of Service Acceptor that will always match anything
|
14
|
+
class Any
|
15
|
+
include TermsOfServiceAcceptor
|
16
|
+
def accept?(_tos_uri)
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Terms of Service Acceptor that matches based upon a regular expression
|
22
|
+
class Regex
|
23
|
+
include TermsOfServiceAcceptor
|
24
|
+
def initialize(regex)
|
25
|
+
@regex = regex
|
26
|
+
end
|
27
|
+
|
28
|
+
def accept?(tos_uri)
|
29
|
+
@regex.match?(tos_uri)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Anchor
|
4
|
+
module AutoCert
|
5
|
+
class Error < StandardError; end
|
6
|
+
class IdentifierNotAllowedError < Error; end
|
7
|
+
class ConfigurationError < Error; end
|
8
|
+
class UnknownPolicyCheckError < Error; end
|
9
|
+
class UnknownAlgorithmError < Error; end
|
10
|
+
class UnknownKeyFormatError < Error; end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
require_relative 'auto_cert/terms_of_service_acceptor'
|
14
|
+
require_relative 'auto_cert/configuration'
|
15
|
+
require_relative 'auto_cert/manager'
|
16
|
+
require_relative 'auto_cert/managed_certificate'
|
17
|
+
require_relative 'auto_cert/identifier_policy'
|
18
|
+
require_relative 'auto_cert/registry'
|
19
|
+
require_relative 'auto_cert/renewal_busy_wait'
|
20
|
+
|
21
|
+
require_relative 'auto_cert/railtie' if defined?(Rails::Railtie)
|
data/lib/anchor-pki.rb
CHANGED
@@ -1,17 +1,9 @@
|
|
1
|
+
# rubocop:disable Naming/FileName
|
2
|
+
#
|
1
3
|
# frozen_string_literal: true
|
2
4
|
|
3
|
-
|
5
|
+
# this file is named anchor-pki.rb to match the gem name and to be consistent
|
6
|
+
# with the other anchor modules for other languages
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
(@certs ||= []) << OpenSSL::X509::Certificate.new(pem)
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.cert_store
|
11
|
-
@ssl_cert_store ||= OpenSSL::X509::Store.new.tap do |store|
|
12
|
-
(@certs || []).each do |cert|
|
13
|
-
store.add_cert(cert)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
8
|
+
require_relative './anchor'
|
9
|
+
# rubocop:enable Naming/FileName
|
data/lib/anchor.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
#
|
6
|
+
# Anchor module is the top-level namespace for the Anchor PKI client.
|
7
|
+
#
|
8
|
+
module Anchor
|
9
|
+
def self.add_cert(pem)
|
10
|
+
(@certs ||= []) << OpenSSL::X509::Certificate.new(pem)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.cert_store
|
14
|
+
@cert_store ||= OpenSSL::X509::Store.new.tap do |store|
|
15
|
+
(@certs || []).each do |cert|
|
16
|
+
store.add_cert(cert)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require_relative './anchor/version'
|
23
|
+
require_relative './anchor/auto_cert'
|