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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +45 -21
- data/README.md +3 -5
- data/lib/anchor/auto_cert/configuration.rb +43 -218
- data/lib/anchor/auto_cert/railtie.rb +4 -63
- data/lib/anchor/auto_cert.rb +0 -16
- data/lib/anchor/pem_bundle.rb +2 -0
- data/lib/anchor/version.rb +1 -1
- data/lib/anchor.rb +7 -1
- data/lib/puma/dsl.rb +14 -9
- data/lib/puma/plugin/auto_cert.rb +108 -80
- metadata +4 -29
- data/lib/anchor/auto_cert/identifier_policy.rb +0 -71
- data/lib/anchor/auto_cert/managed_certificate.rb +0 -77
- data/lib/anchor/auto_cert/manager.rb +0 -260
- data/lib/anchor/auto_cert/policy_check/for_hostname.rb +0 -40
- data/lib/anchor/auto_cert/policy_check/for_ipaddr.rb +0 -48
- data/lib/anchor/auto_cert/policy_check/for_wildcard_hostname.rb +0 -57
- data/lib/anchor/auto_cert/policy_check.rb +0 -37
- data/lib/anchor/auto_cert/registry.rb +0 -63
- data/lib/anchor/auto_cert/renewal_busy_wait.rb +0 -40
- data/lib/anchor/auto_cert/terms_of_service_acceptor.rb +0 -34
- data/lib/anchor/disk_store.rb +0 -31
@@ -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
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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.
|
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-
|
11
|
+
date: 2024-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: acme
|
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.
|
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
|