anchor-pki 0.2.0 → 0.4.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 +22 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +94 -0
- data/LICENSE.txt +21 -0
- data/README.md +50 -0
- data/Rakefile +11 -0
- data/lib/anchor/auto_cert/configuration.rb +199 -0
- data/lib/anchor/auto_cert/identifier_policy.rb +68 -0
- data/lib/anchor/auto_cert/managed_certificate.rb +63 -0
- data/lib/anchor/auto_cert/manager.rb +200 -0
- data/lib/anchor/auto_cert/policy_check/for_hostname.rb +40 -0
- data/lib/anchor/auto_cert/policy_check/for_ipaddr.rb +48 -0
- data/lib/anchor/auto_cert/policy_check/for_wildcard_hostname.rb +57 -0
- data/lib/anchor/auto_cert/policy_check.rb +37 -0
- data/lib/anchor/auto_cert/railtie.rb +88 -0
- data/lib/anchor/auto_cert/registry.rb +63 -0
- data/lib/anchor/auto_cert/renewal_busy_wait.rb +39 -0
- data/lib/anchor/auto_cert/terms_of_service_acceptor.rb +34 -0
- data/lib/anchor/auto_cert.rb +21 -0
- data/lib/anchor/version.rb +5 -0
- data/lib/anchor-pki.rb +6 -14
- data/lib/anchor.rb +23 -0
- data/lib/puma/dsl.rb +28 -0
- data/lib/puma/plugin/auto_cert.rb +97 -0
- metadata +134 -9
- data/lib/anchor-pki/auto_cert.rb +0 -134
data/lib/puma/dsl.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Extend the ::Puma::DSL module with the configuration options we want for
|
5
|
+
# autocert
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'puma/dsl'
|
9
|
+
|
10
|
+
module Puma
|
11
|
+
# Extend the ::Puma::DSL module with the configuration options we want
|
12
|
+
class DSL
|
13
|
+
def auto_cert_name(name = nil)
|
14
|
+
@options[:auto_cert_name] = name if name
|
15
|
+
@options[:auto_cert_name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def auto_cert_port(port = nil)
|
19
|
+
@options[:auto_cert_port] = port if port
|
20
|
+
@options[:auto_cert_port]
|
21
|
+
end
|
22
|
+
|
23
|
+
def auto_cert_check_every(check_every = nil)
|
24
|
+
@options[:auto_cert_check_every] = check_every if check_every
|
25
|
+
@options[:auto_cert_check_every]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../dsl'
|
4
|
+
module Puma
|
5
|
+
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
|
12
|
+
class << self
|
13
|
+
def ssl_bind_options(managed_certificate:)
|
14
|
+
{
|
15
|
+
cert: managed_certificate.cert_path,
|
16
|
+
key: managed_certificate.key_path
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
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
|
+
|
26
|
+
def config(dsl)
|
27
|
+
port = dsl.auto_cert_port || ENV.fetch('HTTPS_PORT', nil)
|
28
|
+
name = dsl.auto_cert_name || ENV.fetch('AUTO_CERT_NAME', 'default')
|
29
|
+
configuration = ::Anchor::AutoCert::Registry.fetch(name)
|
30
|
+
identifiers = configuration.allow_identifiers
|
31
|
+
manager = ::Anchor::AutoCert::Manager.new(configuration: configuration)
|
32
|
+
|
33
|
+
@managed_certificate = manager.managed_certificate(identifiers: identifiers)
|
34
|
+
|
35
|
+
options = ::Puma::Plugin::AutoCert.ssl_bind_options(managed_certificate: @managed_certificate)
|
36
|
+
|
37
|
+
dsl.ssl_bind '[::]', port, options
|
38
|
+
rescue StandardError
|
39
|
+
@managed_certificate = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def start(launcher)
|
43
|
+
@launcher = launcher
|
44
|
+
unless managed_certificate&.enabled?
|
45
|
+
log_writer.log 'AutoCert >> Not enabled - skipping certificate renewal process'
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
log_writer.log "AutoCert >> Configured for #{managed_certificate.identifiers.join(', ')}"
|
50
|
+
check_every = launcher.config.options[:auto_cert_check_every] ||
|
51
|
+
ENV.fetch('AUTO_CERT_CHECK_EVERY', nil) ||
|
52
|
+
::Anchor::AutoCert::RenewalBusyWait::ONE_HOUR
|
53
|
+
|
54
|
+
in_background do
|
55
|
+
Anchor::AutoCert::RenewalBusyWait.wait_for_it(managed_certificate: managed_certificate,
|
56
|
+
check_every: check_every) do
|
57
|
+
dump_cert_info
|
58
|
+
|
59
|
+
# if ssl server is up, then it has already read the local working
|
60
|
+
# files, which means we can purge them - if there's a disk cache, those still
|
61
|
+
# probably exist
|
62
|
+
ssl_server = launcher.binder.ios.find { |io| io.instance_of?(Puma::MiniSSL::Server) }
|
63
|
+
managed_certificate.purge_working_files if ssl_server
|
64
|
+
|
65
|
+
true
|
66
|
+
end
|
67
|
+
log_writer.log 'AutoCert >> Restarting Puma in order to renew certificate'
|
68
|
+
@launcher.restart
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def dump_cert_info
|
75
|
+
log_writer.debug "AutoCert >> Bound cert : #{managed_certificate.hex_serial}"
|
76
|
+
log_writer.debug "AutoCert >> common name : #{managed_certificate.common_name}"
|
77
|
+
log_writer.debug "AutoCert >> identifiers : #{managed_certificate.identifiers.join(', ')}"
|
78
|
+
log_writer.debug "AutoCert >> not before : #{managed_certificate.not_before}"
|
79
|
+
log_writer.debug "AutoCert >> not after : #{managed_certificate.not_after}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def log_writer
|
83
|
+
if Gem::Version.new(Puma::Const::PUMA_VERSION) >= Gem::Version.new(6)
|
84
|
+
@launcher.log_writer
|
85
|
+
else
|
86
|
+
@launcher.events
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# This is the entry point for the plugin
|
95
|
+
Puma::Plugin.create do
|
96
|
+
include Puma::Plugin::AutoCert::PluginInstanceMethods
|
97
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anchor-pki
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Anchor
|
7
|
+
- Anchor Security, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acme-client
|
@@ -24,18 +24,143 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 2.0.13
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.14'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.14'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.9'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.50'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.50'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.22'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.22'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: vcr
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '6.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '6.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: webmock
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.8'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.8'
|
27
125
|
description: Anchor is a hosted PKI platform for your internal organization.
|
28
|
-
email:
|
126
|
+
email:
|
29
127
|
executables: []
|
30
128
|
extensions: []
|
31
|
-
extra_rdoc_files:
|
129
|
+
extra_rdoc_files:
|
130
|
+
- LICENSE.txt
|
131
|
+
- README.md
|
132
|
+
- CHANGELOG.md
|
32
133
|
files:
|
134
|
+
- CHANGELOG.md
|
135
|
+
- Gemfile
|
136
|
+
- Gemfile.lock
|
137
|
+
- LICENSE.txt
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
33
140
|
- lib/anchor-pki.rb
|
34
|
-
- lib/anchor
|
35
|
-
|
141
|
+
- lib/anchor.rb
|
142
|
+
- lib/anchor/auto_cert.rb
|
143
|
+
- lib/anchor/auto_cert/configuration.rb
|
144
|
+
- lib/anchor/auto_cert/identifier_policy.rb
|
145
|
+
- lib/anchor/auto_cert/managed_certificate.rb
|
146
|
+
- lib/anchor/auto_cert/manager.rb
|
147
|
+
- lib/anchor/auto_cert/policy_check.rb
|
148
|
+
- lib/anchor/auto_cert/policy_check/for_hostname.rb
|
149
|
+
- lib/anchor/auto_cert/policy_check/for_ipaddr.rb
|
150
|
+
- lib/anchor/auto_cert/policy_check/for_wildcard_hostname.rb
|
151
|
+
- lib/anchor/auto_cert/railtie.rb
|
152
|
+
- lib/anchor/auto_cert/registry.rb
|
153
|
+
- lib/anchor/auto_cert/renewal_busy_wait.rb
|
154
|
+
- lib/anchor/auto_cert/terms_of_service_acceptor.rb
|
155
|
+
- lib/anchor/version.rb
|
156
|
+
- lib/puma/dsl.rb
|
157
|
+
- lib/puma/plugin/auto_cert.rb
|
158
|
+
homepage: https://anchor.dev
|
36
159
|
licenses:
|
37
160
|
- MIT
|
38
|
-
metadata:
|
161
|
+
metadata:
|
162
|
+
homepage_uri: https://anchor.dev
|
163
|
+
rubygems_mfa_required: 'true'
|
39
164
|
post_install_message:
|
40
165
|
rdoc_options: []
|
41
166
|
require_paths:
|
@@ -44,7 +169,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
169
|
requirements:
|
45
170
|
- - ">="
|
46
171
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
172
|
+
version: '2.3'
|
48
173
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
174
|
requirements:
|
50
175
|
- - ">="
|
data/lib/anchor-pki/auto_cert.rb
DELETED
@@ -1,134 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'acme/client'
|
4
|
-
require 'uri'
|
5
|
-
|
6
|
-
module Anchor
|
7
|
-
# AutoCert provides automatic access to certificates from Anchor, but
|
8
|
-
# theoretically it could work with Let's Encrypt and any other ACME-based CA.
|
9
|
-
class AutoCert
|
10
|
-
ACCEPT_ANY_TOS = [//].freeze
|
11
|
-
|
12
|
-
def initialize(name_policies, tos_acceptor, directory, account: {}, **opts)
|
13
|
-
@name_policies = name_policies
|
14
|
-
@tos_acceptor = tos_acceptor
|
15
|
-
@directory_url = URI.parse(directory)
|
16
|
-
|
17
|
-
@renew_before = opts[:renew_before]
|
18
|
-
@work_dir = Pathname(opts[:work_dir] || Dir.mktmpdir)
|
19
|
-
@cache = opts[:cache]
|
20
|
-
@client = opts[:client]
|
21
|
-
|
22
|
-
@client ||= new_client(**account)
|
23
|
-
@account_opts = account
|
24
|
-
end
|
25
|
-
|
26
|
-
def certificate(*names, algo: :ecdsa, common_name: names.first, **opts)
|
27
|
-
if (arr = unmatched_names(names)).size.positive?
|
28
|
-
raise StandardError, "unallowed names '#{arr.join(',')}'"
|
29
|
-
end
|
30
|
-
|
31
|
-
key_pem = @cache&.read("#{common_name}+#{algo}")
|
32
|
-
cert_pem = @cache&.read(common_name)
|
33
|
-
|
34
|
-
if key_pem.nil? || cert_pem.nil? || needs_renewal?(cert_pem)
|
35
|
-
cert_pem, key_pem = provision(names, algo, common_name, **opts)
|
36
|
-
|
37
|
-
@cache&.write("#{common_name}+#{algo}", key_pem)
|
38
|
-
@cache&.write(common_name, cert_pem)
|
39
|
-
end
|
40
|
-
|
41
|
-
cert = (@work_dir / common_name).open('w') { |f| f << cert_pem }.path
|
42
|
-
key = (@work_dir / "#{common_name}+#{algo}").open('w') { |f| f << key_pem }.path
|
43
|
-
|
44
|
-
[cert, key]
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def provision(names, algo, common_name, **opts)
|
50
|
-
load_or_build_account
|
51
|
-
|
52
|
-
order = @client.new_order(identifiers: names, **opts)
|
53
|
-
|
54
|
-
key_pem ||= new_key(algo).to_pem
|
55
|
-
csr = Acme::Client::CertificateRequest.new(private_key: parse_key_pem(key_pem), common_name: common_name, names: names)
|
56
|
-
|
57
|
-
order.finalize(csr: csr)
|
58
|
-
# TODO: loop over order.authorizations and process the challenges
|
59
|
-
|
60
|
-
while order.status == 'processing'
|
61
|
-
sleep(1)
|
62
|
-
order.reload
|
63
|
-
end
|
64
|
-
|
65
|
-
return order.certificate, key_pem
|
66
|
-
end
|
67
|
-
|
68
|
-
def load_or_build_account
|
69
|
-
@account ||= build_account(**@account_opts)
|
70
|
-
end
|
71
|
-
|
72
|
-
def build_account(contact: nil, external_account_binding: nil, terms_of_service_agreed: false, **)
|
73
|
-
terms_of_service_agreed ||= @tos_acceptor.any? { |a| a.match?(@client.terms_of_service) }
|
74
|
-
|
75
|
-
@client.new_account(contact: contact, terms_of_service_agreed: terms_of_service_agreed, external_account_binding: external_account_binding)
|
76
|
-
end
|
77
|
-
|
78
|
-
def needs_renewal?(cert_pem)
|
79
|
-
cert = OpenSSL::X509::Certificate.new(cert_pem)
|
80
|
-
|
81
|
-
(Time.zone.now + @renew_before.to_i) > cert.not_after
|
82
|
-
end
|
83
|
-
|
84
|
-
def new_client(account_key: nil, contact: nil, **)
|
85
|
-
account_key ||= fetch_account_key(contact) { new_key(:ecdsa) }
|
86
|
-
|
87
|
-
Acme::Client.new(private_key: account_key, directory: @directory_url)
|
88
|
-
end
|
89
|
-
|
90
|
-
def new_key(algo)
|
91
|
-
case algo
|
92
|
-
when :ecdsa then OpenSSL::PKey::EC.generate('prime256v1')
|
93
|
-
else
|
94
|
-
raise "unknown key algo '#{algo}'"
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def fetch_account_key(contact)
|
99
|
-
id = "#{contact || 'default'}+#{@directory_url.host}+key"
|
100
|
-
parse_key_pem(@cache&.fetch(id) { parse_key_pem(yield.to_pem) } || yield.to_pem)
|
101
|
-
end
|
102
|
-
|
103
|
-
def parse_key_pem(data)
|
104
|
-
begin
|
105
|
-
OpenSSL::PKey::RSA.new(data)
|
106
|
-
rescue StandardError
|
107
|
-
nil
|
108
|
-
end ||
|
109
|
-
begin
|
110
|
-
OpenSSL::PKey::EC.new(data)
|
111
|
-
rescue StandardError
|
112
|
-
nil
|
113
|
-
end ||
|
114
|
-
(raise 'unknown key data format')
|
115
|
-
end
|
116
|
-
|
117
|
-
def unmatched_names(names)
|
118
|
-
names.reject do |name|
|
119
|
-
@name_policies.any? do |policy|
|
120
|
-
case policy
|
121
|
-
when String then name == policy
|
122
|
-
when Regexp then policy.match?(name)
|
123
|
-
when IPAddr
|
124
|
-
begin
|
125
|
-
policy.include?(name)
|
126
|
-
rescue IPAddr::Error
|
127
|
-
nil
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|