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.
@@ -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)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anchor
4
+ VERSION = '0.4.0'
5
+ end
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
- require 'openssl'
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
- module Anchor
6
- def self.add_cert(pem)
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'