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.
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.2.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-04-18 00:00:00.000000000 Z
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: support@anchor.dev
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-pki/auto_cert.rb
35
- homepage: https://anchor.dev/
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: '0'
172
+ version: '2.3'
48
173
  required_rubygems_version: !ruby/object:Gem::Requirement
49
174
  requirements:
50
175
  - - ">="
@@ -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