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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +29 -8
- data/README.md +3 -5
- data/lib/anchor/auto_cert/configuration.rb +35 -228
- data/lib/anchor/auto_cert/railtie.rb +4 -59
- 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 +0 -1
- data/lib/puma/dsl.rb +14 -9
- data/lib/puma/plugin/auto_cert.rb +113 -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,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
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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.
|
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:
|
11
|
+
date: 2024-01-11 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.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
|