anchor-pki 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc0b9b7fe9893fc5d4f23157bbb2a18c4a15066e929024c197b610ef5f6ffd80
4
- data.tar.gz: 5cc3e63d7acbc16531c9abf60d04d205fe5ed0b638e595653a8aed74d8abc49a
3
+ metadata.gz: 30508321e074e903d3e8328cc7b2e32fad03b696a32dbb2f16014e92f7636ede
4
+ data.tar.gz: 7705acb22177288ac5de11a37c62080504efef41a82c7337673c84dadc90fd28
5
5
  SHA512:
6
- metadata.gz: 7d5d6bb50ee250800df59aa17ecca780cddc08293ff4bb609a4b944fcc611ef76ec916d5d6b75026694b0740addeabf592533c4117ea90cd3a1032c21c816f5e
7
- data.tar.gz: 4e2c6f2a836c59319a6cab2a6f48204d75ae7e8fb1b721e6177c2ec75be57c1f8271a18fef02afc847dc822b1db8e70c228c4cbd90ec023fd54bd8ecd620d87c
6
+ metadata.gz: 392139d7eea3518ba1d9f9915e628050818e3855494b55876e8fc63ffa4aff45e6434918f8fabae4bd6fe042210ac56fc7b1e0fe6093ee558013b85cb0d15551
7
+ data.tar.gz: e8f674f66082121873997074914a619205b9375608a6db2f1bd4afe6199d43c96f9874d3d584811f251143cd3c559f90b30ec5ca70ef1c42f01c043da4c3d5ef
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.3.0] - 2023-06-02
4
+
5
+ - improve gem packaging
6
+ - extract out a configuration class for Acme
7
+ - add tests
8
+
9
+ ## [0.2.0] - 2023-04-18
10
+
11
+ -
12
+
13
+ ## [0.1.0] - 2021-11-05
14
+
15
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in anchor-pki.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,94 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ anchor-pki (0.2.1)
5
+ acme-client (~> 2.0.13)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ acme-client (2.0.13)
11
+ faraday (>= 1.0, < 3.0.0)
12
+ faraday-retry (>= 1.0, < 3.0.0)
13
+ addressable (2.8.4)
14
+ public_suffix (>= 2.0.2, < 6.0)
15
+ ast (2.4.2)
16
+ crack (0.4.5)
17
+ rexml
18
+ diff-lcs (1.5.0)
19
+ faraday (2.7.5)
20
+ faraday-net_http (>= 2.0, < 3.1)
21
+ ruby2_keywords (>= 0.0.4)
22
+ faraday-net_http (3.0.2)
23
+ faraday-retry (2.2.0)
24
+ faraday (~> 2.0)
25
+ hashdiff (1.0.1)
26
+ json (2.6.3)
27
+ minitest (5.18.0)
28
+ parallel (1.23.0)
29
+ parser (3.2.2.1)
30
+ ast (~> 2.4.1)
31
+ public_suffix (5.0.1)
32
+ rainbow (3.1.1)
33
+ rake (13.0.6)
34
+ regexp_parser (2.8.0)
35
+ rexml (3.2.5)
36
+ rspec (3.12.0)
37
+ rspec-core (~> 3.12.0)
38
+ rspec-expectations (~> 3.12.0)
39
+ rspec-mocks (~> 3.12.0)
40
+ rspec-core (3.12.2)
41
+ rspec-support (~> 3.12.0)
42
+ rspec-expectations (3.12.3)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.12.0)
45
+ rspec-mocks (3.12.5)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.12.0)
48
+ rspec-support (3.12.0)
49
+ rubocop (1.51.0)
50
+ json (~> 2.3)
51
+ parallel (~> 1.10)
52
+ parser (>= 3.2.0.0)
53
+ rainbow (>= 2.2.2, < 4.0)
54
+ regexp_parser (>= 1.8, < 3.0)
55
+ rexml (>= 3.2.5, < 4.0)
56
+ rubocop-ast (>= 1.28.0, < 2.0)
57
+ ruby-progressbar (~> 1.7)
58
+ unicode-display_width (>= 2.4.0, < 3.0)
59
+ rubocop-ast (1.28.1)
60
+ parser (>= 3.2.1.0)
61
+ rubocop-capybara (2.18.0)
62
+ rubocop (~> 1.41)
63
+ rubocop-factory_bot (2.23.1)
64
+ rubocop (~> 1.33)
65
+ rubocop-rspec (2.22.0)
66
+ rubocop (~> 1.33)
67
+ rubocop-capybara (~> 2.17)
68
+ rubocop-factory_bot (~> 2.22)
69
+ ruby-progressbar (1.13.0)
70
+ ruby2_keywords (0.0.5)
71
+ unicode-display_width (2.4.2)
72
+ vcr (6.1.0)
73
+ webmock (3.18.1)
74
+ addressable (>= 2.8.0)
75
+ crack (>= 0.3.2)
76
+ hashdiff (>= 0.4.0, < 2.0.0)
77
+
78
+ PLATFORMS
79
+ aarch64-linux
80
+ arm64-darwin-21
81
+ x86_64-darwin-22
82
+
83
+ DEPENDENCIES
84
+ anchor-pki!
85
+ minitest (~> 5.14)
86
+ rake (~> 13.0)
87
+ rspec (~> 3.9)
88
+ rubocop (~> 1.50)
89
+ rubocop-rspec (~> 2.22)
90
+ vcr (~> 6.1)
91
+ webmock (~> 3.8)
92
+
93
+ BUNDLED WITH
94
+ 2.3.26
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Anchor Security, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Anchor
2
+
3
+ Ruby client for Anchor PKI. See https://anchor.dev/ for details.
4
+
5
+ ## Configuration
6
+
7
+ The Following environment variables are available to configure the default
8
+ [`AutoCert::Manager`](./lib/anchor/auto_cert/manager.rb).
9
+
10
+ * `HTTPS_PORT` - the TCP numerical port to bind SSL to.
11
+ * `ACME_ALLOW_IDENTIFIERS` - A comma separated list of hostnames for provisioning certs
12
+ * `ACME_DIRECTORY_URL` - the ACME provider's directory
13
+ * `ACME_KID` - your External Account Binding (EAB) KID for authenticating with the ACME directory above with an
14
+ * `ACME_HMAC_KEY` - your EAB HMAC_KEY for authenticating with the ACME directory above
15
+ * `ACME_RENEW_BEFORE_SECONDS` - **optional** Start a renewal this number number of seconds before the cert expires. This defaults to 30 days (2592000 seconds)
16
+ * `ACME_RENEW_BEFORE_FRACTION` - **optional** Start the renewal when this fraction of a cert's valid window is left. This defaults to 0.5, which means when the cert is in the last 50% of its lifespan a renewal is attempted.
17
+
18
+ If both `ACME_RENEW_BEFORE_SECONDS` and `ACME_RENEW_BEFORE_FRACTION` are set,
19
+ the one that causes the renewal to take place earlier is used.
20
+
21
+ Example:
22
+
23
+ * Cert start (not_before) moment is : `2023-05-24 20:53:11 UTC`
24
+ * Cert expiration (not_after) moment is : `2023-06-21 20:53:10 UTC`
25
+ * `ACME_RENEW_BEFORE_SECONDS` is `1209600` (14 days)
26
+ * `ACME_RENEW_BEFORE_FRACTION` is `0.25` - which equates to a before seconds value of `604799` (~7 days)
27
+
28
+ The possible moments to start renewing are:
29
+
30
+ * 14 days before expiration moment - `2023-06-07 20:53:10 UTC`
31
+ * when 25% of the valid time is left - `2023-06-14 20:53:11 UTC`
32
+
33
+ Currently the `AutoCert::Manager` will use whichever is earlier.
34
+
35
+ ### Example configuration
36
+
37
+ ```sh
38
+ HTTPS_PORT=44300
39
+ ACME_ALLOW_IDENTIFIERS=my.lcl.host,*.my.lcl.host
40
+ ACME_DIRECTORY_URL=https://acme-v02.api.letsencrypt.org/directory
41
+ ACME_KID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
42
+ ACME_HMAC_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
43
+ ```
44
+
45
+ ## License
46
+
47
+ The gem is available as open source under the terms of the [MIT
48
+ License](./LICENSE.txt)
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'minitest/test_task'
9
+ Minitest::TestTask.create(:test)
10
+
11
+ task default: %i[spec test]
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anchor
4
+ module AutoCert
5
+ # AutoCert Configuration provides a way to configure the AutoCert Manager.
6
+ #
7
+ class Configuration
8
+ DEFAULT_BEFORE_SECONDS = 60 * 60 * 24 * 30 # 30 days in seconds
9
+ DEFAULT_BEFORE_FRACTION = 0.5 # when in the last 50% of the validity window, renew
10
+
11
+ # Note - although it is possible to set change the name of a config, it is
12
+ # not recommended. The name is used as the key in the Registry, and if a
13
+ # Configuration is in the Registry, and its name is changed, it does not
14
+ # change its registry key.
15
+ attr_accessor :name,
16
+ :allow_identifiers,
17
+ :cache,
18
+ :contact,
19
+ :directory_url,
20
+ :external_account_binding,
21
+ :renew_before_fraction,
22
+ :renew_before_seconds,
23
+ :tos_acceptors,
24
+ :work_dir
25
+
26
+ # rubocop:disable Metrics/ParameterLists
27
+ # Data defined classes have all required parameters in the initializer, so
28
+ # override the default initializer to allow for optional parameters and
29
+ # to pull in the defaults form the environment
30
+ #
31
+ def initialize(name:,
32
+ allow_identifiers: nil,
33
+ cache: nil,
34
+ contact: nil,
35
+ directory_url: nil,
36
+ external_account_binding: nil,
37
+ renew_before_fraction: DEFAULT_BEFORE_FRACTION,
38
+ renew_before_seconds: DEFAULT_BEFORE_SECONDS,
39
+ tos_acceptors: nil,
40
+ work_dir: nil)
41
+
42
+ @name = name
43
+
44
+ @allow_identifiers = allow_identifiers
45
+ @cache = cache
46
+ @contact = contact
47
+ @directory_url = directory_url
48
+ @external_account_binding = external_account_binding
49
+ @renew_before_fraction = renew_before_fraction
50
+ @renew_before_seconds = renew_before_seconds
51
+ @tos_acceptors = tos_acceptors
52
+ @work_dir = work_dir
53
+ end
54
+ # rubocop:enable Metrics/ParameterLists
55
+
56
+ def account
57
+ {
58
+ contact: contact,
59
+ external_account_binding: external_account_binding
60
+ }
61
+ end
62
+
63
+ # Enabled means that the configuration has both allow_identifiers and a
64
+ # directory url set.
65
+ def enabled?
66
+ allow_identifiers && directory_url
67
+ end
68
+
69
+ def validate!
70
+ @allow_identifiers = prepare_allow_identifiers(@allow_identifiers)
71
+ @cache = prepare_cache(@cache)
72
+ @directory_url = prepare_directory_url(@directory_url)
73
+ @external_account_binding = prepare_external_account_binding(@external_account_binding)
74
+ @renew_before_fraction = prepare_renew_before_fraction(@renew_before_fraction)
75
+ @renew_before_seconds = prepare_renew_before_seconds(@renew_before_seconds)
76
+ @tos_acceptors = prepare_tos_acceptors(@tos_acceptors)
77
+ @work_dir = prepare_work_dir(@work_dir)
78
+
79
+ self
80
+ end
81
+
82
+ private
83
+
84
+ def prepare_allow_identifiers(allow_identifiers)
85
+ prepared = case allow_identifiers
86
+ when Array
87
+ allow_identifiers
88
+ when String
89
+ allow_identifiers.split(',')
90
+ when nil
91
+ ENV.fetch('ACME_ALLOW_IDENTIFIERS', nil)&.split(',')
92
+ end
93
+
94
+ if prepared.nil? || prepared.empty?
95
+ raise ConfigurationError,
96
+ "The '#{name}' #{self.class} instance has a misconfigured `allow_identifiers` value. " \
97
+ 'Set it to a string, or an array of strings, ' \
98
+ 'or set the ACME_ALLOW_IDENTIFIERS environment variable to a comma separated list of identifiers.'
99
+ end
100
+
101
+ prepared
102
+ end
103
+
104
+ def prepare_cache(cache)
105
+ return nil if cache.nil?
106
+
107
+ unless cache.respond_to?(:read) && cache.respond_to?(:write) && cache.respond_to?(:fetch)
108
+ raise ConfigurationError,
109
+ "The '#{name}' #{self.class} instance has a misconfigured `cache` value. " \
110
+ 'It must be set to an object that responds to `read`, `write`, and `fetch`.'
111
+ end
112
+
113
+ cache
114
+ end
115
+
116
+ def prepare_directory_url(directory_url)
117
+ directory_url ||= ENV.fetch('ACME_DIRECTORY_URL', nil)
118
+ if directory_url.nil?
119
+ raise ConfigurationError,
120
+ "The '#{name}' #{self.class} instance has a misconfigured `directory_url` value. " \
121
+ 'It must be set to a string, or set the ACME_DIRECTORY_URL environment variable.'
122
+ end
123
+
124
+ directory_url
125
+ end
126
+
127
+ def prepare_external_account_binding(external_account_binding)
128
+ kid = ENV.fetch('ACME_KID', nil)
129
+ hmac_key = ENV.fetch('ACME_HMAC_KEY', nil)
130
+
131
+ if kid && hmac_key
132
+ external_account_binding = {
133
+ kid: kid,
134
+ hmac_key: hmac_key
135
+ }
136
+ end
137
+ external_account_binding
138
+ end
139
+
140
+ def prepare_renew_before_seconds(renew_before_seconds)
141
+ renew_before_seconds ||= ENV.fetch('ACME_RENEW_BEFORE_SECONDS', nil)
142
+ if renew_before_seconds
143
+ renew_before_seconds = renew_before_seconds.to_i
144
+ unless renew_before_seconds.positive?
145
+ raise ConfigurationError,
146
+ "The '#{name}' #{self.class} instance has a misconfigured `before_seconds` value. " \
147
+ 'It must be set to an integer > 0, or set the ACME_RENEW_BEFORE_SECONDS environment variable.'
148
+ end
149
+ end
150
+ renew_before_seconds
151
+ end
152
+
153
+ def prepare_renew_before_fraction(renew_before_fraction)
154
+ renew_before_fraction ||= ENV.fetch('ACME_RENEW_BEFORE_FRACTION', nil)
155
+ if renew_before_fraction
156
+ renew_before_fraction = renew_before_fraction.to_f
157
+ unless (0..1).cover?(renew_before_fraction)
158
+ raise ConfigurationError,
159
+ "The '#{name}' #{self.class} instance has a misconfigured `before_fraction` value. " \
160
+ 'It must be set to a float > 0 and < 1, or set the ACME_RENEW_BEFORE_FRACTION environment variable.'
161
+ end
162
+ end
163
+ renew_before_fraction
164
+ end
165
+
166
+ def prepare_tos_acceptors(tos_acceptors)
167
+ tos_acceptors = Array(tos_acceptors)
168
+
169
+ if tos_acceptors.empty? || tos_acceptors.any? { |tos| !tos.respond_to?(:accept?) }
170
+ raise ConfigurationError,
171
+ "The '#{name}' #{self.class} instance has a misconfigured `tos_acceptors` value. " \
172
+ 'It must be set to an object or an array of objects that respond to `accept?`.'
173
+ end
174
+
175
+ tos_acceptors
176
+ end
177
+
178
+ def prepare_work_dir(work_dir)
179
+ return nil if work_dir.nil?
180
+
181
+ work_dir = Pathname.new(work_dir) unless work_dir.is_a?(Pathname)
182
+
183
+ begin
184
+ work_dir.mkpath
185
+ rescue StandardError => e
186
+ raise ConfigurationError, "#{self.class}#work_dir : #{e.message}"
187
+ end
188
+
189
+ unless work_dir.directory? && work_dir.writable?
190
+ raise ConfigurationError, "#{self.class}#work_dir '#{work_dir}' must be a writable directory."
191
+ end
192
+
193
+ work_dir
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,68 @@
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
+ raise UnknownPolicyCheckError, "Unable to create a policy check based upon '#{description}'" if check_klass.nil?
51
+
52
+ @description = description
53
+ @check = check_klass.new(description)
54
+ end
55
+
56
+ def allow?(identifier)
57
+ raise ArgumentError, 'identifier must be a String' unless identifier.is_a?(String)
58
+
59
+ @check.allow?(identifier)
60
+ end
61
+
62
+ def deny?(identifier)
63
+ !allow?(identifier)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ require_relative 'policy_check'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anchor
4
+ module AutoCert
5
+ module Integration
6
+ # Puma integration
7
+ class Puma
8
+ def self.ssl_bind_options(manager:, identifiers: nil)
9
+ identifiers ||= manager.configuration.allow_identifiers
10
+ cert, key = manager.certificate_paths(identifiers: identifiers)
11
+
12
+ { cert: cert, key: key }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Anchor
4
+ module AutoCert
5
+ # Integration module
6
+ module Integration
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative 'integration/puma'