anchor-pki 0.6.3 → 0.8.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 +45 -21
- data/README.md +3 -5
- data/lib/anchor/auto_cert/configuration.rb +43 -218
- 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 +7 -1
- data/lib/puma/dsl.rb +14 -9
- data/lib/puma/plugin/auto_cert.rb +108 -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
|