anchor-pki 0.6.3 → 0.8.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.
@@ -1,106 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../anchor'
3
4
  require_relative '../dsl'
5
+
6
+ require 'puma/acme'
7
+
4
8
  module Puma
5
9
  class Plugin
6
- # This is a plugin for Puma that will automatically renew a certificate
7
- #
8
- # This module is here in order to communicate plugin configuration options
9
- # to the plugin since the plugin is created dynamically and it is loaded and
10
- # initialized without any configuration options.
11
- module AutoCert
10
+ # This is a plugin for Puma that will automatically provision & renew certificates based
11
+ # on options loaded from a framework plugin or the environment. Only the foreground acme
12
+ # mode is supported.
13
+ class AutoCert < Puma::Acme::Plugin
14
+ Plugins.register('auto_cert', self)
15
+
16
+ class Error < StandardError; end
17
+ class PortMissingError < Error; end
18
+ class ServerNameMissingError < Error; end
19
+
12
20
  class << self
13
- def ssl_bind_options(managed_certificate:)
14
- {
15
- cert: managed_certificate.cert_path,
16
- key: managed_certificate.private_key_path
17
- }
18
- end
21
+ attr_accessor :start_hooks
19
22
  end
20
23
 
21
- # Instance methods that are included in the dynamic Puma Plugin class when
22
- # a plugin is created
23
- module PluginInstanceMethods
24
- attr_accessor :managed_certificate
25
- attr_reader :manager, :port
26
-
27
- def config(dsl)
28
- @port = dsl.auto_cert_port || ENV.fetch('HTTPS_PORT', nil)
29
- name = dsl.auto_cert_name || ENV.fetch('AUTO_CERT_NAME', 'anchor')
30
- configuration = ::Anchor::AutoCert::Registry.fetch(name)
31
- identifiers = configuration.allow_identifiers
32
- @manager = ::Anchor::AutoCert::Manager.new(configuration: configuration)
33
-
34
- @managed_certificate = manager.managed_certificate(identifiers: identifiers)
35
- rescue StandardError => _e
36
- @manager = nil
37
- @managed_certificate = nil
38
- end
24
+ def self.add_start_hook(&block)
25
+ (self.start_hooks ||= []) << block
26
+ end
39
27
 
40
- def start(launcher)
41
- @launcher = launcher
42
- unless manager&.enabled? && managed_certificate
43
- log_writer.log 'AutoCert >> Not enabled - skipping certificate renewal process'
44
- return
45
- end
28
+ def start(launcher, env: ENV)
29
+ # puma.rb > framework config > ENV
30
+ config = ConfigLookup.new(puma: launcher.options, app: framework_config, env: env)
46
31
 
47
- options = ::Puma::Plugin::AutoCert.ssl_bind_options(managed_certificate: managed_certificate)
48
- launcher.config.configure do |_user_config, file_config|
49
- file_config.ssl_bind '[::]', port, options
32
+ enabled = config.first(:enabled)
33
+
34
+ https_port = config.first(:port, puma: :auto_cert_port, env: 'HTTPS_PORT')
35
+ if https_port.nil?
36
+ if enabled
37
+ raise PortMissingError, 'AutoCert was enabled, but no https port number provided'
50
38
  end
51
39
 
52
- managed_certificate.identifiers.each do |identifier|
53
- log_writer.log "AutoCert >> Available at https://#{identifier}:#{port}/"
40
+ launcher.log_writer.log 'AutoCert >> Not enabled, no HTTPS_PORT'
41
+ return
42
+ end
43
+
44
+ server_names = [*config.all(:server_names, env: Anchor::ENV_VARS[:server_names])]
45
+ .map { |val| val.split(/[ ,]/) }.flatten.uniq
46
+
47
+ if server_names.empty?
48
+ if enabled
49
+ raise ServerNameMissingError, 'AutoCert was enabled, but no server name(s) provided'
54
50
  end
55
51
 
56
- check_every = launcher.config.options[:auto_cert_check_every] ||
57
- ENV.fetch('AUTO_CERT_CHECK_EVERY', nil) ||
58
- ::Anchor::AutoCert::RenewalBusyWait::ONE_HOUR
59
-
60
- in_background do
61
- Anchor::AutoCert::RenewalBusyWait.wait_for_it(manager: manager,
62
- managed_certificate: managed_certificate,
63
- check_every: check_every) do
64
- dump_cert_info
65
-
66
- # if ssl server is up, then it has already read the local working
67
- # files, which means we can purge them - if there's a disk cache, those still
68
- # probably exist
69
- ssl_server = launcher.binder.ios.find { |io| io.instance_of?(Puma::MiniSSL::Server) }
70
- managed_certificate.purge_working_files if ssl_server
71
-
72
- true
73
- end
74
- log_writer.log 'AutoCert >> Restarting Puma in order to renew certificate'
75
- @launcher.restart
52
+ launcher.log_writer.log 'AutoCert >> Not enabled, no ACME_SERVER_NAME(S)'
53
+ return
54
+ end
55
+
56
+ launcher.options[:acme_server_names] = server_names
57
+
58
+ tcp_hosts(launcher.options[:binds]).each do |host|
59
+ launcher.options[:binds] << "acme://#{host}:#{https_port}"
60
+ end
61
+
62
+ %i[algorithm cache_dir contact tos_agreed].each do |key|
63
+ if (val = config.first(key))
64
+ launcher.options[:"acme_#{key}"] ||= val
76
65
  end
77
- rescue StandardError => e
78
- log_writer.log "AutoCert >> Error - #{e.message}"
79
66
  end
80
67
 
81
- private
68
+ launcher.options[:acme_directory] ||= config.first(:directory, env: Anchor::ENV_VARS[:directory])
69
+
70
+ launcher.options[:acme_eab_kid] ||= config.first(:eab_kid, env: Anchor::ENV_VARS[:eab_kid])
71
+ launcher.options[:acme_eab_hmac_key] ||= config.first(:eab_hmac_key,
72
+ env: Anchor::ENV_VARS[:eab_hmac_key])
73
+
74
+ launcher.options[:acme_mode] ||= config.first(:mode) || :foreground
82
75
 
83
- def dump_cert_info
84
- log_writer.debug "AutoCert >> Bound cert : #{managed_certificate.hex_serial}"
85
- log_writer.debug "AutoCert >> common name : #{managed_certificate.common_name}"
86
- log_writer.debug "AutoCert >> identifiers : #{managed_certificate.identifiers.join(', ')}"
87
- log_writer.debug "AutoCert >> not before : #{managed_certificate.not_before}"
88
- log_writer.debug "AutoCert >> not after : #{managed_certificate.not_after}"
76
+ # TODO: unify with config, check/use :renew_interval
77
+ launcher.options[:acme_renew_at] ||= renew_before
78
+
79
+ super(launcher)
80
+ end
81
+
82
+ protected
83
+
84
+ def renew_before
85
+ if (renew_at = ENV.fetch('ACME_RENEW_BEFORE_SECONDS', nil))
86
+ renew_at.to_i
87
+ elsif (renew_at = ENV.fetch('ACME_RENEW_BEFORE_FRACTION', nil))
88
+ renew_at.to_f
89
+ else
90
+ 0.5
91
+ end
92
+ end
93
+
94
+ def framework_config
95
+ # TODO(benburkert): for the next framework plugin, make this generic
96
+ Rails.application.config.auto_cert if defined?(Rails)
97
+ end
98
+
99
+ def tcp_hosts(binds)
100
+ binds.select { |bind| bind.start_with?('tcp://') }
101
+ .map { |bind| URI.parse(bind).host }.uniq
102
+ end
103
+
104
+ # puma.rb > app framework config > ENV
105
+ ConfigLookup = Struct.new(:puma, :app, :env, keyword_init: true) do
106
+ alias_method :puma_config, :puma
107
+ alias_method :app_config, :app
108
+ alias_method :env_config, :env
109
+
110
+ def first(key, puma: :"acme_#{key}", env: puma.upcase.to_s)
111
+ lookup(key, puma: puma, env: env) { |val| return val }
112
+ nil
113
+ end
114
+
115
+ def all(key, puma: :"acme_#{key}", env: puma.upcase)
116
+ values = []
117
+ lookup(key, puma: puma, env: env) { |val| values << val }
118
+ values
89
119
  end
90
120
 
91
- def log_writer
92
- if Gem::Version.new(Puma::Const::PUMA_VERSION) >= Gem::Version.new(6)
93
- @launcher.log_writer
94
- else
95
- @launcher.events
121
+ protected
122
+
123
+ def lookup(key, puma:, env:)
124
+ yield(puma_config[puma]) if puma_config&.fetch(puma, nil)
125
+ yield(app_config[key]) if app_config&.to_h&.fetch(key, nil)
126
+
127
+ [*env].each do |k|
128
+ yield(env_config[k]) if env_config&.fetch(k, nil)
96
129
  end
97
130
  end
98
131
  end
99
132
  end
100
133
  end
101
134
  end
102
-
103
- # This is the entry point for the plugin
104
- Puma::Plugin.create do
105
- include Puma::Plugin::AutoCert::PluginInstanceMethods
106
- end
metadata CHANGED
@@ -1,31 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anchor-pki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anchor Security, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-10 00:00:00.000000000 Z
11
+ date: 2024-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: acme-client
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: 2.0.13
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: 2.0.13
27
- - !ruby/object:Gem::Dependency
28
- name: pstore
14
+ name: puma-acme
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - "~>"
@@ -169,18 +155,7 @@ files:
169
155
  - lib/anchor.rb
170
156
  - lib/anchor/auto_cert.rb
171
157
  - lib/anchor/auto_cert/configuration.rb
172
- - lib/anchor/auto_cert/identifier_policy.rb
173
- - lib/anchor/auto_cert/managed_certificate.rb
174
- - lib/anchor/auto_cert/manager.rb
175
- - lib/anchor/auto_cert/policy_check.rb
176
- - lib/anchor/auto_cert/policy_check/for_hostname.rb
177
- - lib/anchor/auto_cert/policy_check/for_ipaddr.rb
178
- - lib/anchor/auto_cert/policy_check/for_wildcard_hostname.rb
179
158
  - lib/anchor/auto_cert/railtie.rb
180
- - lib/anchor/auto_cert/registry.rb
181
- - lib/anchor/auto_cert/renewal_busy_wait.rb
182
- - lib/anchor/auto_cert/terms_of_service_acceptor.rb
183
- - lib/anchor/disk_store.rb
184
159
  - lib/anchor/oid.rb
185
160
  - lib/anchor/pem_bundle.rb
186
161
  - lib/anchor/version.rb
@@ -207,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
182
  - !ruby/object:Gem::Version
208
183
  version: '0'
209
184
  requirements: []
210
- rubygems_version: 3.5.4
185
+ rubygems_version: 3.4.22
211
186
  signing_key:
212
187
  specification_version: 4
213
188
  summary: Ruby client for Anchor PKI. See https://anchor.dev/ for details.
@@ -1,71 +0,0 @@
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
- if check_klass.nil?
51
- raise UnknownPolicyCheckError,
52
- "Unable to create a policy check based upon '#{description}'"
53
- end
54
-
55
- @description = description
56
- @check = check_klass.new(description)
57
- end
58
-
59
- def allow?(identifier)
60
- raise ArgumentError, 'identifier must be a String' unless identifier.is_a?(String)
61
-
62
- @check.allow?(identifier)
63
- end
64
-
65
- def deny?(identifier)
66
- !allow?(identifier)
67
- end
68
- end
69
- end
70
- end
71
- require_relative 'policy_check'
@@ -1,77 +0,0 @@
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
8
- # for renewal
9
- class ManagedCertificate
10
- attr_reader :cert_pem, :key_pem, :x509, :persist_dir, :cert_path, :private_key_path
11
-
12
- extend Forwardable
13
- def_delegators :@x509, :not_after, :not_before
14
-
15
- def initialize(cert_pem:, key_pem:, persist_dir: nil)
16
- @cert_pem = cert_pem
17
- @key_pem = key_pem
18
- @persist_dir = Pathname.new(persist_dir) if persist_dir
19
- @x509 = OpenSSL::X509::Certificate.new(cert_pem)
20
- @cert_path = nil
21
- @private_key_path = nil
22
- persist_pems
23
- end
24
-
25
- def serial
26
- x509.serial.to_i
27
- end
28
-
29
- def hex_serial(joiner = ':')
30
- x509.serial.to_s(16).scan(/.{2}/).join(joiner)
31
- end
32
-
33
- def expired?(now: Time.now.utc)
34
- not_after <= now
35
- end
36
-
37
- # For the moment, the only items in subjectAltName we care about are DNS:
38
- # entries.
39
- def identifiers
40
- alt_names = x509&.extensions&.find { |ext| ext.oid == 'subjectAltName' }&.value&.split(', ') || []
41
- alt_names.select { |name| name.start_with?('DNS:') }
42
- .map { |name| name.sub(/^DNS:/, '') }
43
- end
44
-
45
- def common_name
46
- x509.subject.to_a.find { |name, _, _| name == 'CN' }[1]
47
- end
48
-
49
- def all_names
50
- non_common_identifiers = identifiers.reject { |name| name == common_name }
51
- [common_name, *non_common_identifiers.sort]
52
- end
53
-
54
- def persist_pems
55
- return unless persist_dir
56
- return nil unless persist_dir.directory? && persist_dir.writable?
57
-
58
- @cert_path = persist_dir.join("#{serial}.crt")
59
- @cert_path.write(cert_pem)
60
-
61
- @private_key_path = persist_dir.join("#{serial}.key")
62
- @private_key_path.write(key_pem)
63
- end
64
-
65
- def purge_working_files
66
- return unless persist_dir
67
- return nil unless persist_dir.directory? && persist_dir.writable?
68
-
69
- [@cert_path, @private_key_path].each do |path|
70
- next unless path&.exist? && path&.writable?
71
-
72
- path.delete
73
- end
74
- end
75
- end
76
- end
77
- end
@@ -1,260 +0,0 @@
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
- :check_every_seconds,
19
- :cache_dir,
20
- :contact,
21
- :directory,
22
- :external_account_binding,
23
- :fallback_identifier,
24
- :renew_before_fraction,
25
- :renew_before_seconds,
26
- :work_dir
27
-
28
- attr_reader :disk_store,
29
- :client,
30
- :configuration,
31
- :directory_url,
32
- :identifier_policies,
33
- :tos_acceptors
34
-
35
- def self.for(configuration)
36
- new(configuration: configuration)
37
- end
38
-
39
- def initialize(configuration:, client: nil)
40
- configuration.validate!
41
- @configuration = configuration
42
-
43
- # disk store early since other things may use it
44
- @disk_store = DiskStore.new(dir: @configuration.cache_dir, basename: 'autocert-manager')
45
-
46
- @identifier_policies = IdentifierPolicy.build(@configuration.allow_identifiers)
47
- @tos_acceptors = Array(@configuration.tos_acceptors)
48
- @directory_url = URI.parse(@configuration.directory_url)
49
-
50
- @account_opts = {
51
- contact: @configuration.contact,
52
- external_account_binding: @configuration.external_account_binding
53
- }
54
-
55
- @client = client || new_client(contact: @configuration.contact)
56
- @enabled = true
57
- @managed_certificates = {}
58
- end
59
-
60
- # It is currently assumed that the common name is the first of the
61
- # `identifiers` passed into this method. If that is not the case, then
62
- # the `common_name` parameter needs to be set explicitly
63
- def managed_certificate(identifiers:, algorithm: :ecdsa, common_name: Array(identifiers).first,
64
- now: Time.now.utc, **opts)
65
- full_ids = consolidate_identifiers(common_name: common_name, identifiers: identifiers)
66
- denied_ids = denied_identifiers(full_ids)
67
-
68
- # Fallback to a configured identifier if the requested one(s) are denied
69
- if denied_ids.any?
70
- common_name = fallback_identifier
71
- identifiers = []
72
- end
73
-
74
- # first look and see if its memory
75
- managed_certificate = @managed_certificates[common_name]
76
- if managed_certificate && !needs_renewal?(cert: managed_certificate, now: now)
77
- return managed_certificate
78
- end
79
-
80
- # then look into the disk cache
81
- if @disk_store
82
- key_pem = @disk_store["#{common_name}.key.pem"]
83
- cert_pem = @disk_store["#{common_name}.cert.pem"]
84
- end
85
-
86
- if !key_pem.nil? && !cert_pem.nil?
87
- managed_certificate = ManagedCertificate.new(cert_pem: cert_pem,
88
- key_pem: key_pem,
89
- persist_dir: work_dir)
90
- if managed_certificate && !needs_renewal?(cert: managed_certificate, now: now)
91
- return managed_certificate
92
- end
93
- end
94
-
95
- # and then provision a new one
96
- cert_pem, key_pem = provision_or_fallback(
97
- identifiers: identifiers, algorithm: algorithm,
98
- common_name: common_name,
99
- **opts
100
- )
101
-
102
- managed_certificate = ManagedCertificate.new(
103
- cert_pem: cert_pem, key_pem: key_pem, persist_dir: work_dir
104
- )
105
-
106
- @managed_certificates[common_name] = managed_certificate
107
-
108
- if @disk_store
109
- @disk_store["#{common_name}.key.pem"] = key_pem
110
- @disk_store["#{common_name}.cert.pem"] = cert_pem
111
- end
112
-
113
- managed_certificate
114
- end
115
-
116
- def needs_renewal?(cert:, now: Time.now.utc)
117
- renew_after = [
118
- renew_after_from_seconds(cert: cert),
119
- renew_after_from_fraction(cert: cert),
120
- renew_after_fallback(cert: cert),
121
- cert.not_after # cert expired, get a new one
122
- ].compact.min
123
-
124
- (now > renew_after)
125
- end
126
-
127
- def disable
128
- @enabled = false
129
- end
130
-
131
- # if the manager is enabled && the configuration is enabled
132
- def enabled?
133
- @enabled && configuration.enabled?
134
- end
135
-
136
- private
137
-
138
- def provision_or_fallback(identifiers:, algorithm:, common_name:, **opts)
139
- cert_pem = nil
140
- key_pem = nil
141
- begin
142
- cert_pem, key_pem = provision(
143
- identifiers: identifiers, algorithm: algorithm, common_name: common_name,
144
- **opts
145
- )
146
- rescue StandardError => _e
147
- cert_pem, key_pem = provision(
148
- identifiers: [], algorithm: algorithm, common_name: fallback_identifier,
149
- **opts
150
- )
151
- end
152
- [cert_pem, key_pem]
153
- end
154
-
155
- def provision(identifiers:, algorithm:, common_name:, **opts)
156
- identifiers = consolidate_identifiers(common_name: common_name, identifiers: identifiers)
157
- load_or_build_account
158
- key_pem ||= new_key(algorithm).to_pem
159
- csr = Acme::Client::CertificateRequest.new(
160
- common_name: common_name, names: identifiers,
161
- private_key: parse_key_pem(key_pem)
162
- )
163
-
164
- order = @client.new_order(identifiers: identifiers, **opts)
165
- order.finalize(csr: csr)
166
- # TODO: loop over order.authorizations and process the challenges
167
-
168
- while order.status == 'processing'
169
- sleep(1)
170
- order.reload
171
- end
172
-
173
- [order.certificate, key_pem]
174
- end
175
-
176
- def account
177
- @account ||= build_account(**@account_opts)
178
- end
179
- alias load_or_build_account account
180
-
181
- def build_account(contact: nil, external_account_binding: nil, terms_of_service_agreed: false, **)
182
- terms_of_service_agreed ||= @tos_acceptors.any? { |a| a.accept?(@client.terms_of_service) }
183
-
184
- @client.new_account(contact: contact, terms_of_service_agreed: terms_of_service_agreed,
185
- external_account_binding: external_account_binding)
186
- end
187
-
188
- def renew_after_from_seconds(cert:, before_seconds: renew_before_seconds)
189
- return nil unless before_seconds
190
-
191
- renew_after = (cert.not_after - before_seconds)
192
-
193
- # invalid if the renewal time is outside the certificate validity window
194
- return nil unless (cert.not_before..cert.not_after).cover?(renew_after)
195
-
196
- renew_after
197
- end
198
-
199
- def renew_after_from_fraction(cert:, before_fraction: renew_before_fraction)
200
- return nil unless (0..1).cover?(before_fraction)
201
-
202
- valid_span = cert.not_after - cert.not_before
203
- before_seconds = (valid_span * before_fraction).floor
204
-
205
- renew_after_from_seconds(cert: cert, before_seconds: before_seconds)
206
- end
207
-
208
- # Fallback timestamp, in case there is some corner case that is not covered
209
- def renew_after_fallback(cert:)
210
- renew_after_from_seconds(cert: cert, before_seconds: FALLBACK_RENEW_BEFORE_SECONDS)
211
- end
212
-
213
- def new_client(account_key: nil, contact: nil, **)
214
- account_key ||= account_key_for(contact)
215
-
216
- Acme::Client.new(private_key: account_key, directory: @directory_url)
217
- end
218
-
219
- # currently only using ecdsa algorithm
220
- def new_key(algorithm = :ecdsa)
221
- case algorithm
222
- when :ecdsa then OpenSSL::PKey::EC.generate('prime256v1')
223
- else
224
- raise UnknownAlgorithmError, "unknown key algorithm '#{algorithm}'"
225
- end
226
- end
227
-
228
- def account_key_for(contact)
229
- return new_key unless @disk_store
230
-
231
- account_key_id = "#{contact || 'default'}+#{@directory_url}+key"
232
- pem = @disk_store[account_key_id]
233
- return parse_key_pem(pem) if pem
234
-
235
- raw_key = new_key
236
- @disk_store[account_key_id] = raw_key.to_pem
237
- raw_key
238
- end
239
-
240
- def parse_key_pem(data)
241
- OpenSSL::PKey::EC.new(data)
242
- rescue StandardError
243
- nil
244
- end
245
-
246
- # returns all those identifiers that were not accepted by any policy
247
- def denied_identifiers(identifiers)
248
- Array(identifiers).reject do |identifier|
249
- @identifier_policies.any? { |policy| policy.allow?(identifier) }
250
- end
251
- end
252
-
253
- # return a list of identifiers with duplicates removed
254
- # preserving order with the common_name first
255
- def consolidate_identifiers(common_name:, identifiers: [])
256
- [common_name, *identifiers].compact.uniq
257
- end
258
- end
259
- end
260
- end