anchor-pki 0.6.3 → 0.7.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,139 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../dsl'
4
+
5
+ require 'puma/acme'
6
+
4
7
  module Puma
5
8
  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
9
+ # This is a plugin for Puma that will automatically provision & renew certificates based
10
+ # on options loaded from a framework plugin or the environment. Only the foreground acme
11
+ # mode is supported.
12
+ class AutoCert < Puma::Acme::Plugin
13
+ Plugins.register('auto_cert', self)
14
+
15
+ class Error < StandardError; end
16
+ class PortMissingError < Error; end
17
+ class ServerNameMissingError < Error; end
18
+
12
19
  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
20
+ attr_accessor :start_hooks
19
21
  end
20
22
 
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
23
+ ENV_VARS = {
24
+ server_names: %w[ACME_SERVER_NAME ACME_SERVER_NAMES SERVER_NAME SERVER_NAMES ACME_ALLOW_IDENTIFIERS],
25
+ directory: %w[ACME_DIRECTORY ACME_DIRECTORY_URL],
26
+ eab_kid: %w[ACME_KID ACME_EAB_KID],
27
+ eab_hmac_key: %w[ACME_HMAC_KEY ACME_EAB_HMAC_KEY]
28
+ }.freeze
39
29
 
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
30
+ def self.add_start_hook(&block)
31
+ (self.start_hooks ||= []) << block
32
+ end
33
+
34
+ def start(launcher, env: ENV)
35
+ # puma.rb > framework config > ENV
36
+ config = ConfigLookup.new(puma: launcher.options, app: framework_config, env: env)
37
+
38
+ enabled = config.first(:enabled)
46
39
 
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
40
+ https_port = config.first(:port, puma: :auto_cert_port, env: 'HTTPS_PORT')
41
+ if https_port.nil?
42
+ if enabled
43
+ raise PortMissingError, 'AutoCert was enabled, but no https port number provided'
50
44
  end
51
45
 
52
- managed_certificate.identifiers.each do |identifier|
53
- log_writer.log "AutoCert >> Available at https://#{identifier}:#{port}/"
46
+ launcher.log_writer.log 'AutoCert >> Not enabled, no HTTPS_PORT'
47
+ return
48
+ end
49
+
50
+ server_names = [*config.all(:server_names, env: ENV_VARS[:server_names])]
51
+ .map { |val| val.split(/[ ,]/) }.flatten.uniq
52
+
53
+ if server_names.empty?
54
+ if enabled
55
+ raise ServerNameMissingError, 'AutoCert was enabled, but no server name(s) provided'
54
56
  end
55
57
 
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
58
+ launcher.log_writer.log 'AutoCert >> Not enabled, no ACME_SERVER_NAME(S)'
59
+ return
60
+ end
61
+
62
+ launcher.options[:acme_server_names] = server_names
63
+
64
+ tcp_hosts(launcher.options[:binds]).each do |host|
65
+ launcher.options[:binds] << "acme://#{host}:#{https_port}"
66
+ end
67
+
68
+ %i[algorithm cache_dir contact tos_agreed].each do |key|
69
+ if (val = config.first(key))
70
+ launcher.options[:"acme_#{key}"] ||= val
76
71
  end
77
- rescue StandardError => e
78
- log_writer.log "AutoCert >> Error - #{e.message}"
79
72
  end
80
73
 
81
- private
74
+ launcher.options[:acme_directory] ||= config.first(:directory, env: ENV_VARS[:directory])
75
+
76
+ launcher.options[:acme_eab_kid] ||= config.first(:eab_kid, env: ENV_VARS[:eab_kid])
77
+ launcher.options[:acme_eab_hmac_key] ||= config.first(:eab_hmac_key, env: ENV_VARS[:eab_hmac_key])
78
+
79
+ launcher.options[:acme_mode] ||= config.first(:mode) || :foreground
80
+
81
+ # TODO: unify with config, check/use :renew_interval
82
+ launcher.options[:acme_renew_at] ||= renew_before
83
+
84
+ super(launcher)
85
+ end
86
+
87
+ protected
88
+
89
+ def renew_before
90
+ if (renew_at = ENV.fetch('ACME_RENEW_BEFORE_SECONDS', nil))
91
+ renew_at.to_i
92
+ elsif (renew_at = ENV.fetch('ACME_RENEW_BEFORE_FRACTION', nil))
93
+ renew_at.to_f
94
+ else
95
+ 0.5
96
+ end
97
+ end
98
+
99
+ def framework_config
100
+ # TODO(benburkert): for the next framework plugin, make this generic
101
+ Rails.application.config.auto_cert if defined?(Rails)
102
+ end
82
103
 
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}"
104
+ def tcp_hosts(binds)
105
+ binds.select { |bind| bind.start_with?('tcp://') }
106
+ .map { |bind| URI.parse(bind).host }.uniq
107
+ end
108
+
109
+ # puma.rb > app framework config > ENV
110
+ ConfigLookup = Struct.new(:puma, :app, :env, keyword_init: true) do
111
+ alias_method :puma_config, :puma
112
+ alias_method :app_config, :app
113
+ alias_method :env_config, :env
114
+
115
+ def first(key, puma: :"acme_#{key}", env: puma.upcase.to_s)
116
+ lookup(key, puma: puma, env: env) { |val| return val }
117
+ nil
118
+ end
119
+
120
+ def all(key, puma: :"acme_#{key}", env: puma.upcase)
121
+ values = []
122
+ lookup(key, puma: puma, env: env) { |val| values << val }
123
+ values
89
124
  end
90
125
 
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
126
+ protected
127
+
128
+ def lookup(key, puma:, env:)
129
+ yield(puma_config[puma]) if puma_config&.fetch(puma, nil)
130
+ yield(app_config[key]) if app_config&.to_h&.fetch(key, nil)
131
+
132
+ [*env].each do |k|
133
+ yield(env_config[k]) if env_config&.fetch(k, nil)
96
134
  end
97
135
  end
98
136
  end
99
137
  end
100
138
  end
101
139
  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.7.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-01-11 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.10
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