anchor-pki 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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', 'default')
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.2
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: 2023-12-20 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.3.26
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