anchor-pki 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'