anchor-pki 0.6.3 → 0.7.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 +5 -0
- data/Gemfile.lock +28 -7
- data/README.md +3 -5
- data/lib/anchor/auto_cert/configuration.rb +35 -219
- 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 +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,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Anchor
|
4
|
-
module AutoCert
|
5
|
-
class PolicyCheck
|
6
|
-
# Check the identifier by strict hostname name comparison
|
7
|
-
#
|
8
|
-
# Reference: http://www.faqs.org/rfcs/rfc2396.html
|
9
|
-
#
|
10
|
-
class ForHostname < PolicyCheck
|
11
|
-
ALPHA = '[a-zA-Z]'
|
12
|
-
ALPHA_NUMERIC = '[a-zA-Z0-9]'
|
13
|
-
ALPHA_NUMERIC_HYPHEN = '[-a-zA-Z0-9]'
|
14
|
-
DOMAIN_LABEL = "#{ALPHA_NUMERIC}#{ALPHA_NUMERIC_HYPHEN}*#{ALPHA_NUMERIC}"
|
15
|
-
TOP_LEVEL_DOMAIN = "#{ALPHA}#{ALPHA_NUMERIC_HYPHEN}*#{ALPHA_NUMERIC}"
|
16
|
-
|
17
|
-
REGEX = /
|
18
|
-
\A
|
19
|
-
(?<sub>(#{DOMAIN_LABEL}\.)+)
|
20
|
-
(?<tld>#{TOP_LEVEL_DOMAIN})
|
21
|
-
\z
|
22
|
-
/ix.freeze
|
23
|
-
|
24
|
-
def self.handles?(description)
|
25
|
-
description.is_a?(String) && description.match?(REGEX)
|
26
|
-
end
|
27
|
-
|
28
|
-
def initialize(description)
|
29
|
-
super
|
30
|
-
@hostname = description.downcase
|
31
|
-
end
|
32
|
-
|
33
|
-
# case insensitive comparison
|
34
|
-
def allow?(name)
|
35
|
-
name.is_a?(String) && (name.downcase == @hostname)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'ipaddr'
|
4
|
-
module Anchor
|
5
|
-
module AutoCert
|
6
|
-
class PolicyCheck
|
7
|
-
#
|
8
|
-
# A PolicyCheck that compares the given IP address to an ipaddress
|
9
|
-
# or subnet
|
10
|
-
#
|
11
|
-
# The description for an IPAddr policy check can be anything that is
|
12
|
-
# parsed by the ruby IPAddr.new() method. Generally this is something in
|
13
|
-
# the format an ipv4 address, an ipv6 address, or CIDR notation.
|
14
|
-
#
|
15
|
-
# - "192.168.42.1"
|
16
|
-
# - "192.168.42.0/24"
|
17
|
-
# - "3ffe:505:2::1"
|
18
|
-
# - "2001:db8::/32"
|
19
|
-
#
|
20
|
-
class ForIPAddr < PolicyCheck
|
21
|
-
def self.handles?(description)
|
22
|
-
return true if description.is_a?(IPAddr)
|
23
|
-
|
24
|
-
begin
|
25
|
-
IPAddr.new(description)
|
26
|
-
true
|
27
|
-
rescue IPAddr::Error
|
28
|
-
false
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def initialize(description)
|
33
|
-
super
|
34
|
-
|
35
|
-
@ipaddr = if description.is_a?(IPAddr)
|
36
|
-
description
|
37
|
-
else
|
38
|
-
IPAddr.new(description)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def allow?(name)
|
43
|
-
@ipaddr.include?(name)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Anchor
|
4
|
-
module AutoCert
|
5
|
-
class PolicyCheck
|
6
|
-
# Check that the identifier is allowed by wildcard hostname matching
|
7
|
-
#
|
8
|
-
# This does the same as the ForHostname check, but allows for an arbitrary
|
9
|
-
# prefixes, although the prefix itself also needs to match appropriate
|
10
|
-
# domain name rules.
|
11
|
-
#
|
12
|
-
# The description for a wildcard hostname check MUST be a string that starts
|
13
|
-
# with `*.` followed by a valid domainname (which matches the ForHostname
|
14
|
-
# check)
|
15
|
-
#
|
16
|
-
#
|
17
|
-
class ForWildcardHostname < PolicyCheck
|
18
|
-
DOMAIN_LABEL_REGEX = /\A#{ForHostname::DOMAIN_LABEL}*\z/i.freeze
|
19
|
-
SPLAT = '*'
|
20
|
-
|
21
|
-
def self.handles?(description)
|
22
|
-
return false unless description.is_a?(String)
|
23
|
-
|
24
|
-
parts = description.split('.')
|
25
|
-
return false unless parts[0] == SPLAT
|
26
|
-
|
27
|
-
suffix = parts[1..-1].join('.')
|
28
|
-
# reuse the hostname check here
|
29
|
-
ForHostname.handles?(suffix)
|
30
|
-
end
|
31
|
-
|
32
|
-
def initialize(description)
|
33
|
-
super
|
34
|
-
@parts = description.split('.')
|
35
|
-
@wildcard = @parts.shift # assumed SPLAT
|
36
|
-
@suffix = @parts.join('.').downcase
|
37
|
-
end
|
38
|
-
|
39
|
-
# An explicit '*.rest.of' is an allowable hostname for this check, even
|
40
|
-
# though it is not a valid domain name
|
41
|
-
#
|
42
|
-
def allow?(hostname)
|
43
|
-
return false unless hostname.is_a?(String)
|
44
|
-
|
45
|
-
parts = hostname.split('.')
|
46
|
-
prefix = parts.shift
|
47
|
-
|
48
|
-
return false unless (prefix == SPLAT) || DOMAIN_LABEL_REGEX.match?(prefix)
|
49
|
-
|
50
|
-
domain = parts.join('.').downcase
|
51
|
-
|
52
|
-
(domain == @suffix)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Anchor
|
4
|
-
module AutoCert
|
5
|
-
# Base class for PolicyCheck classes, these are utility classes used by the
|
6
|
-
# IdentifierPolicy class.
|
7
|
-
#
|
8
|
-
# The .handles? method is used to determine if an instance of this class
|
9
|
-
# could be created from the given description.
|
10
|
-
#
|
11
|
-
# Then the #allow? method is used to determine if the given identifier is allowed
|
12
|
-
# and the #deny? method is used to determine if the given identifier is denied.
|
13
|
-
#
|
14
|
-
class PolicyCheck
|
15
|
-
def self.handles?(description)
|
16
|
-
raise NotImplementedError, "#{self.class} must implement .handles?(description)"
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_reader :policy_description
|
20
|
-
|
21
|
-
def initialize(description)
|
22
|
-
@policy_description = description
|
23
|
-
end
|
24
|
-
|
25
|
-
def deny?(identifier)
|
26
|
-
!allow?(identifier)
|
27
|
-
end
|
28
|
-
|
29
|
-
def allow?(identifier)
|
30
|
-
raise NotImplementedError, "#{self.class} must implement #allow?(identifier)"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
require_relative 'policy_check/for_ipaddr'
|
36
|
-
require_relative 'policy_check/for_hostname'
|
37
|
-
require_relative 'policy_check/for_wildcard_hostname'
|
@@ -1,63 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Anchor
|
4
|
-
module AutoCert
|
5
|
-
# A global registry for storing AutoCert::Configuration objects
|
6
|
-
class Registry
|
7
|
-
@_registry = {}
|
8
|
-
@_mutex = Mutex.new
|
9
|
-
|
10
|
-
def self.store(name, configuration)
|
11
|
-
@_mutex.synchronize do
|
12
|
-
@_registry.store(name.to_s, configuration)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.fetch(name)
|
17
|
-
@_mutex.synchronize do
|
18
|
-
@_registry.fetch(name.to_s)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.key?(name)
|
23
|
-
@_mutex.synchronize do
|
24
|
-
@_registry.key?(name.to_s)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.delete(name)
|
29
|
-
@_mutex.synchronize do
|
30
|
-
@_registry.delete(name.to_s)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# helpers for creating an AutoCert::Manager instance from registered
|
35
|
-
# configuration
|
36
|
-
def self.manager_for(name)
|
37
|
-
manager_for!(name)
|
38
|
-
rescue KeyError
|
39
|
-
nil
|
40
|
-
end
|
41
|
-
|
42
|
-
# helpers for creating an AutoCert::Manager instance from registered
|
43
|
-
# configuration - raises KeyError if the named configuration does not
|
44
|
-
# exist
|
45
|
-
def self.manager_for!(name)
|
46
|
-
configuration = fetch(name)
|
47
|
-
AutoCert::Manager.new(configuration: configuration)
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.default_configuration
|
51
|
-
fetch(:default)
|
52
|
-
rescue KeyError
|
53
|
-
default = AutoCert::Configuration.new
|
54
|
-
store(:default, default)
|
55
|
-
default
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.default_manager
|
59
|
-
manager_for(:default)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Anchor
|
4
|
-
module AutoCert
|
5
|
-
# RenewalBusyWait is a class that will loop to check if a certificate needs to
|
6
|
-
# to be renewed, and if it does, return
|
7
|
-
#
|
8
|
-
# Generally this class should be used inside its own thread as it will sleep
|
9
|
-
# and loop until the pem file is able to be renewed.
|
10
|
-
#
|
11
|
-
# Every 'check_every' interval it will check if the certificate needs to be
|
12
|
-
# renewed. If it does, the loop will end and `wait_for_it` will return. If
|
13
|
-
# it does not need renewal, the block will be called, and if the result of
|
14
|
-
# the block is falsy, the loop will exit early
|
15
|
-
#
|
16
|
-
class RenewalBusyWait
|
17
|
-
ONE_HOUR = 60 * 60
|
18
|
-
|
19
|
-
def self.wait_for_it(manager:, managed_certificate:, check_every: ONE_HOUR, &keep_going)
|
20
|
-
waiter = new(manager: manager, managed_certificate: managed_certificate, check_every: check_every)
|
21
|
-
waiter.wait_for_it(&keep_going)
|
22
|
-
end
|
23
|
-
|
24
|
-
def initialize(manager:, managed_certificate:, check_every: ONE_HOUR)
|
25
|
-
@manager = manager
|
26
|
-
@managed_certificate = managed_certificate
|
27
|
-
@check_every = check_every
|
28
|
-
end
|
29
|
-
|
30
|
-
def wait_for_it
|
31
|
-
loop do
|
32
|
-
break if @manager.needs_renewal?(cert: @managed_certificate)
|
33
|
-
break unless yield
|
34
|
-
|
35
|
-
sleep @check_every
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Anchor
|
4
|
-
module AutoCert
|
5
|
-
# Any class that implements the following interface can be used as a
|
6
|
-
# Terms of Service Acceptor. The interface is the single method `#accept?`
|
7
|
-
# which is handed the terms of service URI as a String.
|
8
|
-
module TermsOfServiceAcceptor
|
9
|
-
def accept?(tos_uri)
|
10
|
-
raise NotImplementedError, "#{self.class} must implement #accept?(tos_uri)"
|
11
|
-
end
|
12
|
-
|
13
|
-
# Terms of Service Acceptor that will always match anything
|
14
|
-
class Any
|
15
|
-
include TermsOfServiceAcceptor
|
16
|
-
def accept?(_tos_uri)
|
17
|
-
true
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# Terms of Service Acceptor that matches based upon a regular expression
|
22
|
-
class Regex
|
23
|
-
include TermsOfServiceAcceptor
|
24
|
-
def initialize(regex)
|
25
|
-
@regex = regex
|
26
|
-
end
|
27
|
-
|
28
|
-
def accept?(tos_uri)
|
29
|
-
@regex.match?(tos_uri)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
data/lib/anchor/disk_store.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'pstore'
|
4
|
-
require 'tempfile'
|
5
|
-
|
6
|
-
module Anchor
|
7
|
-
# DiskStore is a simple key/value store that persists to disk using PStore.
|
8
|
-
#
|
9
|
-
class DiskStore
|
10
|
-
def initialize(dir: nil, basename: 'anchor-disk-store')
|
11
|
-
@dir = dir || Dir.mktmpdir
|
12
|
-
@basename = basename
|
13
|
-
@path = File.join(@dir, @basename)
|
14
|
-
@pstore = PStore.new(@path, true)
|
15
|
-
end
|
16
|
-
|
17
|
-
def [](key)
|
18
|
-
data = nil
|
19
|
-
@pstore.transaction do
|
20
|
-
data = @pstore[key]
|
21
|
-
end
|
22
|
-
data
|
23
|
-
end
|
24
|
-
|
25
|
-
def []=(key, value)
|
26
|
-
@pstore.transaction do
|
27
|
-
@pstore[key] = value
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|