autosign 0.0.1 → 0.0.2
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/.travis.yml +13 -0
- data/Gemfile.lock +4 -11
- data/README.md +8 -0
- data/autosign.gemspec +3 -0
- data/bin/autosign-validator +3 -2
- data/features/validate.feature +3 -1
- data/lib/autosign.rb +2 -0
- data/lib/autosign/config.rb +51 -11
- data/lib/autosign/decoder.rb +4 -2
- data/lib/autosign/journal.rb +35 -46
- data/lib/autosign/token.rb +71 -30
- data/lib/autosign/validator.rb +107 -23
- data/lib/autosign/validators/jwt.rb +13 -2
- data/lib/autosign/validators/multiplexer.rb +65 -0
- data/lib/autosign/version.rb +1 -1
- metadata +47 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df93f6a505859c956b527410ffd01c67f3aad762
|
4
|
+
data.tar.gz: f9a1c7f7e857e6f596e6a50b4f534b772a1e62af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86adf30615877ec93e1038b51bf59b73026d01e842c93cdcc7b834965f7de546b8cded69e23d10917a393b0633650899c0460d3b3b60fcd1bb9cf9f5e76e39ff
|
7
|
+
data.tar.gz: 0d2b232814a18fef940e3605d83148a45d724c22ca3f7e490dbf14550ef4d67fbf0ef6eb3281ef35e9ad9fce8ff429ca88bb9d546071208d54fef405bd72229b
|
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
@@ -5,14 +5,15 @@ PATH
|
|
5
5
|
deep_merge
|
6
6
|
gli (~> 2)
|
7
7
|
iniparse (~> 1)
|
8
|
+
json
|
8
9
|
jwt (~> 1)
|
9
10
|
logging
|
10
11
|
require_all
|
12
|
+
yard
|
11
13
|
|
12
14
|
GEM
|
13
15
|
remote: https://rubygems.org/
|
14
16
|
specs:
|
15
|
-
CFPropertyList (2.2.8)
|
16
17
|
aruba (0.6.2)
|
17
18
|
childprocess (>= 0.3.6)
|
18
19
|
cucumber (>= 1.1.1)
|
@@ -31,17 +32,12 @@ GEM
|
|
31
32
|
gherkin (~> 2.12.0)
|
32
33
|
deep_merge (1.0.1)
|
33
34
|
diff-lcs (1.2.5)
|
34
|
-
facter (2.2.0)
|
35
|
-
CFPropertyList (~> 2.2.6)
|
36
35
|
ffi (1.9.9)
|
37
36
|
gherkin (2.12.2)
|
38
37
|
multi_json (~> 1.3)
|
39
38
|
gli (2.13.1)
|
40
|
-
hiera (1.3.4)
|
41
|
-
json_pure
|
42
39
|
iniparse (1.4.0)
|
43
40
|
json (1.8.3)
|
44
|
-
json_pure (1.8.1)
|
45
41
|
jwt (1.5.1)
|
46
42
|
little-plugger (1.1.3)
|
47
43
|
logging (2.0.0)
|
@@ -49,10 +45,6 @@ GEM
|
|
49
45
|
multi_json (~> 1.10)
|
50
46
|
multi_json (1.11.1)
|
51
47
|
multi_test (0.1.2)
|
52
|
-
puppet (3.7.0)
|
53
|
-
facter (> 1.6, < 3)
|
54
|
-
hiera (~> 1.0)
|
55
|
-
json_pure
|
56
48
|
rake (10.4.2)
|
57
49
|
rdoc (4.2.0)
|
58
50
|
json (~> 1.4)
|
@@ -61,6 +53,7 @@ GEM
|
|
61
53
|
diff-lcs (>= 1.2.0, < 2.0)
|
62
54
|
rspec-support (~> 3.3.0)
|
63
55
|
rspec-support (3.3.0)
|
56
|
+
yard (0.8.7.6)
|
64
57
|
|
65
58
|
PLATFORMS
|
66
59
|
ruby
|
@@ -68,6 +61,6 @@ PLATFORMS
|
|
68
61
|
DEPENDENCIES
|
69
62
|
aruba
|
70
63
|
autosign!
|
71
|
-
|
64
|
+
cucumber
|
72
65
|
rake
|
73
66
|
rdoc
|
data/README.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# autosign
|
2
|
+
[](https://travis-ci.org/danieldreier/autosign) [](https://codeclimate.com/github/danieldreier/autosign) [](https://gemnasium.com/danieldreier/autosign) [](http://rubydoc.info/github/danieldreier/autosign) [](http://inch-ci.org/github/danieldreier/autosign)
|
3
|
+
|
4
|
+
Tooling to make puppet autosigning easy, secure, and extensible
|
5
|
+
|
6
|
+
### Introduction
|
7
|
+
|
8
|
+
This tool provides a CLI for performing puppet policy-based autosigning using JWT tokens. Read more at https://danieldreier.github.io/autosign.
|
data/autosign.gemspec
CHANGED
@@ -19,11 +19,14 @@ spec = Gem::Specification.new do |s|
|
|
19
19
|
s.add_development_dependency('rake')
|
20
20
|
s.add_development_dependency('rdoc')
|
21
21
|
s.add_development_dependency('aruba')
|
22
|
+
s.add_development_dependency('cucumber')
|
22
23
|
s.add_development_dependency('puppet')
|
23
24
|
s.add_runtime_dependency('gli','~> 2')
|
24
25
|
s.add_runtime_dependency('jwt','~> 1')
|
25
26
|
s.add_runtime_dependency('iniparse','~> 1')
|
26
27
|
s.add_runtime_dependency('logging')
|
28
|
+
s.add_runtime_dependency('json')
|
27
29
|
s.add_runtime_dependency('deep_merge')
|
28
30
|
s.add_runtime_dependency('require_all')
|
31
|
+
s.add_runtime_dependency('yard')
|
29
32
|
end
|
data/bin/autosign-validator
CHANGED
@@ -16,14 +16,15 @@ certname = ARGV[0]
|
|
16
16
|
@logger.debug "certname is " + certname
|
17
17
|
|
18
18
|
@logger.debug "reading CSR from stdin"
|
19
|
-
|
19
|
+
raw_csr = $stdin.read
|
20
|
+
csr = Autosign::Decoder.decode_csr(raw_csr)
|
20
21
|
exit 1 unless csr.is_a?(Hash)
|
21
22
|
|
22
23
|
@logger.debug "CSR: " + csr.to_s
|
23
24
|
### End Inputs
|
24
25
|
|
25
26
|
### validate token
|
26
|
-
token_validation = Autosign::Validator.any_validator(csr[:challenge_password].to_s, certname.to_s)
|
27
|
+
token_validation = Autosign::Validator.any_validator(csr[:challenge_password].to_s, certname.to_s, raw_csr)
|
27
28
|
### end validation
|
28
29
|
|
29
30
|
### Exit with correct exit status
|
data/features/validate.feature
CHANGED
@@ -9,7 +9,9 @@ Feature: Validate autosign key
|
|
9
9
|
| AUTOSIGN_TESTMODE | true |
|
10
10
|
| AUTOSIGN_TEST_SECRET | secret |
|
11
11
|
| AUTOSIGN_TEST_LOGLEVEL | info |
|
12
|
-
|
12
|
+
| AUTOSIGN_TEST_JOURNALFILE | /tmp/autosign_journal |
|
13
|
+
When I run `rm -f /tmp/autosign_journal`
|
14
|
+
And I run `autosign-validator i-7672fe81` interactively
|
13
15
|
And I pipe in the file "../../fixtures/i-7672fe81.pem"
|
14
16
|
Then the output should contain "token validated successfully"
|
15
17
|
Then the exit status should be 0
|
data/lib/autosign.rb
CHANGED
data/lib/autosign/config.rb
CHANGED
@@ -1,45 +1,73 @@
|
|
1
|
+
require 'iniparse'
|
2
|
+
require 'rbconfig'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'deep_merge'
|
5
|
+
|
1
6
|
module Autosign
|
7
|
+
# Exceptions namespace for Autosign class
|
2
8
|
module Exceptions
|
9
|
+
# Exception representing a general failure during validation
|
3
10
|
class Validation < Exception
|
4
11
|
end
|
12
|
+
# Exception representing a missing file during config validation
|
5
13
|
class NotFound < Exception
|
6
14
|
end
|
15
|
+
# Exception representing a permissions error during config validation
|
7
16
|
class Permissions < Exception
|
8
17
|
end
|
18
|
+
# Exception representing errors that Autosign does not know how to handle
|
9
19
|
class Error < Exception
|
10
20
|
end
|
11
21
|
end
|
12
|
-
|
13
|
-
require 'rbconfig'
|
14
|
-
require 'securerandom'
|
15
|
-
require 'deep_merge'
|
22
|
+
|
16
23
|
class Config
|
17
|
-
|
18
|
-
|
19
|
-
|
24
|
+
# Create a config instance to interact with configuration settings
|
25
|
+
# To specify a configuration file, settings_param should include something like:
|
26
|
+
# {'config_file' => '/usr/local/etc/autosign.conf'}
|
27
|
+
#
|
28
|
+
# If no defaults are provided, the class checks several common locations for config file path.
|
29
|
+
#
|
30
|
+
# @param settings_param [Hash] config settings that should override defaults and config file settings
|
31
|
+
# @return [Autosign::Config] instance of the Autosign::Config class
|
32
|
+
def initialize(settings_param = {})
|
20
33
|
# set up logging
|
21
34
|
@log = Logging.logger['Autosign::Config']
|
22
35
|
@log.debug "initializing Autosign::Config"
|
23
36
|
|
24
37
|
# validate parameter
|
25
|
-
raise 'settings is not a hash' unless
|
38
|
+
raise 'settings is not a hash' unless settings_param.is_a?(Hash)
|
26
39
|
|
27
40
|
# look in the following places for a config file
|
28
41
|
@config_file_paths = ['/etc/autosign.conf', '/usr/local/etc/autosign.conf', File.join(Dir.home, '.autosign.conf')]
|
29
|
-
@config_file_paths = [
|
42
|
+
@config_file_paths = [ settings_param['config_file'] ] unless settings_param['config_file'].nil?
|
30
43
|
|
31
|
-
@settings =
|
44
|
+
@settings = settings_param
|
32
45
|
@log.debug "Using merged settings hash: " + @settings.to_s
|
33
46
|
end
|
34
47
|
|
48
|
+
# Return a merged settings hash of defaults, config file settings
|
49
|
+
# and passed in settings (such as from the CLI)
|
50
|
+
#
|
51
|
+
# @return [Hash] deep merged settings hash
|
35
52
|
def settings
|
36
53
|
@log.debug "merging settings"
|
37
54
|
setting_sources = [default_settings, configfile, @settings]
|
38
|
-
setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
|
55
|
+
merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
|
56
|
+
@log.debug "using merged settings: " + merged_settings.to_s
|
57
|
+
return merged_settings
|
39
58
|
end
|
40
59
|
|
41
60
|
private
|
42
61
|
|
62
|
+
# default settings hash for the whole autosign gem
|
63
|
+
# the format must map to something an ini file can represent, so the
|
64
|
+
# structure should have a top-level key named "general" and key-value pairs
|
65
|
+
# below it, with strings as keys and strings or integers as values.
|
66
|
+
#
|
67
|
+
# validator settings should not be here; put those in the validator's
|
68
|
+
# default settings method.
|
69
|
+
#
|
70
|
+
# @return [Hash] default configuration settings
|
43
71
|
def default_settings
|
44
72
|
{ 'general' =>
|
45
73
|
{
|
@@ -54,6 +82,10 @@ module Autosign
|
|
54
82
|
}
|
55
83
|
end
|
56
84
|
|
85
|
+
# Locate the configuration file, parse it from INI-format, and return
|
86
|
+
# the results as a hash. Returns an empty hash if no config file is found.
|
87
|
+
#
|
88
|
+
# @return [Hash] configuration settings loaded from INI file
|
57
89
|
def configfile
|
58
90
|
@log.debug "Finding config file"
|
59
91
|
@config_file_paths.each { |file|
|
@@ -71,6 +103,11 @@ module Autosign
|
|
71
103
|
return {}
|
72
104
|
end
|
73
105
|
|
106
|
+
# Validate configuration file
|
107
|
+
# Raises an exception if the config file cannot be validated
|
108
|
+
#
|
109
|
+
# @param configfile [String] the absolute path of the config file to validate
|
110
|
+
# @return [String] the absolute path of the config file
|
74
111
|
def validate_config_file(configfile = location)
|
75
112
|
@log.debug "validating config file"
|
76
113
|
unless File.file?(configfile)
|
@@ -87,6 +124,9 @@ module Autosign
|
|
87
124
|
configfile
|
88
125
|
end
|
89
126
|
|
127
|
+
# Generate a default configuration file
|
128
|
+
# As a convenience for the user, we can generate a default config file
|
129
|
+
# This class is currently too tightly coupled with the JWT token validator
|
90
130
|
def self.generate_default()
|
91
131
|
os_defaults = (
|
92
132
|
case RbConfig::CONFIG['host_os']
|
data/lib/autosign/decoder.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
require 'logging'
|
2
|
-
|
3
1
|
module Autosign
|
4
2
|
class Decoder
|
3
|
+
# Extract common name and challenge_password OID from X509 SSL Certificate signing requests
|
4
|
+
#
|
5
|
+
# @param csr[String] X509 format CSR
|
6
|
+
# @return [Hash] hash containing :challenge_password and :common_name keys
|
5
7
|
def self.decode_csr(csr)
|
6
8
|
@log = Logging.logger['Autosign::Decoder']
|
7
9
|
@log.debug "decoding CSR"
|
data/lib/autosign/journal.rb
CHANGED
@@ -1,73 +1,45 @@
|
|
1
1
|
require 'logging'
|
2
2
|
require 'yaml/store'
|
3
3
|
|
4
|
-
# Autosign::Journal checks for one-time keys that have been used before
|
5
4
|
|
6
5
|
module Autosign
|
6
|
+
# Autosign::Journal tracks one-time keys to prevent key re-use.
|
7
|
+
# Keys are stored in the journal file by UUID.
|
8
|
+
# The journal uses ruby's yaml/store, which is a YAML version of the PStore
|
9
|
+
# data store. It is multi-process safe, and blocks until transactions in other
|
10
|
+
# processes are complete.
|
7
11
|
class Journal
|
12
|
+
#@return [Hash] settings of the autosign journal instance, such as the location of the journal file
|
8
13
|
attr_accessor :settings
|
14
|
+
|
15
|
+
# @param settings [Hash] config settings for the new journal instance
|
16
|
+
# @return [Autosign::Journal] instance of the Autosign::Journal class
|
9
17
|
def initialize(settings = {})
|
10
18
|
@log = Logging.logger['Autosign::Journal']
|
11
19
|
@log.debug "initializing Autosign::Journal"
|
12
20
|
@settings = settings
|
13
|
-
fail unless
|
14
|
-
end
|
15
|
-
|
16
|
-
def setup
|
17
|
-
journalfile = self.settings['journalfile']
|
18
|
-
store = YAML::Store.new(journalfile, true)
|
19
|
-
store.ultra_safe = true
|
20
|
-
return store
|
21
|
-
end
|
22
|
-
|
23
|
-
# Check whether a UUID already exists in the journal
|
24
|
-
#
|
25
|
-
# ==== Attributes
|
26
|
-
#
|
27
|
-
# * +uuid+ - Unique journal entry identifier
|
28
|
-
#
|
29
|
-
# ==== Examples
|
30
|
-
#
|
31
|
-
# To exit if a token has already been used:
|
32
|
-
#
|
33
|
-
# journal = Autosign::Journal.new({journalfile = '/etc/autosign/journal')
|
34
|
-
# exit 1 if journal.check('d2e601c8-93df-4459-be18-1877eaf00920')
|
35
|
-
def check(uuid)
|
36
|
-
fail unless validate_uuid(uuid)
|
37
|
-
true
|
21
|
+
fail unless setup
|
38
22
|
end
|
39
23
|
|
40
|
-
def delete(uuid)
|
41
|
-
fail unless validate_uuid(uuid)
|
42
|
-
true
|
43
|
-
end
|
44
24
|
|
45
25
|
|
46
|
-
# Add a new token to the journal
|
47
|
-
#
|
48
|
-
# ==== Attributes
|
49
|
-
#
|
50
|
-
# * +uuid+ - Unique journal entry identifier
|
51
|
-
# * +validto+ - Integer seconds unix timestamp that the token will be valid until
|
52
|
-
# * +data+ - Arbitrary hash that will be serialized and stored in the journal for auditing purposes
|
26
|
+
# Add a new token to the journal. Only succeeds if the token is not in the journal already.
|
53
27
|
#
|
54
|
-
#
|
28
|
+
# @param uuid [String] RFC4122 v4 UUID functioning as unique journal entry identifier
|
29
|
+
# @param validto [Integer] POSIX timestamp in seconds since epoch that the token will be valid until
|
30
|
+
# @param data [Hash] Arbitrary hash that will be serialized and stored in the journal for auditing purposes
|
55
31
|
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# exit 1 if journal.add('d2e601c8-93df-4459-be18-1877eaf00920')
|
32
|
+
# @example attempt adding a token to the journal
|
33
|
+
# journal = Autosign::Journal.new({journalfile = '/etc/autosign/journal')
|
34
|
+
# fail unless journal.add('d2e601c8-93df-4459-be18-1877eaf00920')
|
60
35
|
#
|
61
36
|
# This will only succeed if the token has not previously been added
|
62
37
|
# This is the primary way this class is expected to be used
|
63
38
|
def add(uuid, validto, data = {})
|
64
39
|
@log.debug "attempting to add UUID: '#{uuid.to_s}' which is valid to '#{Time.at(validto.to_i)}' with data #{data.to_s}"
|
65
40
|
puts validate_uuid(uuid).to_s
|
66
|
-
#fail unless validate_uuid(uuid)
|
67
|
-
#fail unless validate_timestamp(validto)
|
68
|
-
#fail unless validate_data(data)
|
69
41
|
|
70
|
-
store =
|
42
|
+
store = setup
|
71
43
|
# wrap the change in a transaction because multiple autosign instances
|
72
44
|
# may try to run simultaneously. This will block until another process
|
73
45
|
# releases the transaction lock.
|
@@ -86,6 +58,22 @@ module Autosign
|
|
86
58
|
return !!result
|
87
59
|
end
|
88
60
|
|
61
|
+
private
|
62
|
+
|
63
|
+
# Create a new journal file, or load an existing one if it already exists.
|
64
|
+
# @return [YAML::Store] instance of YAML::Store using the configured journal file.
|
65
|
+
def setup
|
66
|
+
@log.debug "using journalfile: " + self.settings['journalfile']
|
67
|
+
journalfile = self.settings['journalfile']
|
68
|
+
store = YAML::Store.new(journalfile, true)
|
69
|
+
store.ultra_safe = true
|
70
|
+
return store
|
71
|
+
end
|
72
|
+
|
73
|
+
# Verify that a string is a V4 UUID
|
74
|
+
#
|
75
|
+
# @param uuid [String] RFC4122 v4 UUID
|
76
|
+
# @return [Boolean] true if the uuid string is a valid UUID, false if not a valid UUID
|
89
77
|
def validate_uuid(uuid)
|
90
78
|
unless uuid.is_a?(String)
|
91
79
|
@log.error "UUID is not a string"
|
@@ -96,6 +84,7 @@ module Autosign
|
|
96
84
|
@log.error "UUID is not a valid V4 UUID"
|
97
85
|
return false
|
98
86
|
end
|
87
|
+
return true
|
99
88
|
end
|
100
89
|
|
101
90
|
def validate_data(data)
|
data/lib/autosign/token.rb
CHANGED
@@ -1,17 +1,36 @@
|
|
1
1
|
module Autosign
|
2
2
|
require 'jwt'
|
3
|
-
require '
|
3
|
+
require 'json'
|
4
4
|
require 'securerandom'
|
5
5
|
|
6
|
+
# Class modeling JSON Web Tokens as credentials for certificate auto signing.
|
7
|
+
# See http://jwt.io for more information about JSON web tokens in general.
|
8
|
+
#
|
9
|
+
# @return [Autosign::Token] instance of the Autosign::Token class
|
6
10
|
class Token
|
11
|
+
# @return [Integer, String] seconds that the token will be valid for after being issued
|
7
12
|
attr_reader :validfor
|
13
|
+
# @return [String] common name or regex of common names for which this token is valid
|
8
14
|
attr_reader :certname
|
15
|
+
# @return [True, False] true if the token can be used multiple times, false if the token is intended as a one-time credential
|
9
16
|
attr_reader :reusable
|
17
|
+
# @return [String] arbitrary string identifying the person or machine that generated the token initially
|
10
18
|
attr_reader :requester
|
19
|
+
# @return [String] shared HMAC secret used to sign or validate tokens
|
11
20
|
attr_reader :secret
|
21
|
+
# @return [Integer, String] POSIX seconds since epoch that the token is valid until
|
12
22
|
attr_accessor :validto
|
23
|
+
# @return [String] RFC4122 v4 UUID functioning as unique identifier for the token
|
13
24
|
attr_accessor :uuid
|
14
25
|
|
26
|
+
# Create a token instance to model an individual JWT token
|
27
|
+
#
|
28
|
+
# @param certname [String] common name or regex of common names for which this token is valid
|
29
|
+
# @param reusable [True, False] true if the token can be used multiple times, false if the token is intended as a one-time credential
|
30
|
+
# @param validfor [Integer, String] seconds that the token will be valid for, starting at the current time
|
31
|
+
# @param requester [String] arbitrary string identifying the person or machine that generated the token initially
|
32
|
+
# @param secret [String] shared HMAC secret used to sign or validate tokens
|
33
|
+
# @return [Autosign::Config] instance of the Autosign::Config class
|
15
34
|
def initialize(certname, reusable=false, validfor=7200, requester, secret)
|
16
35
|
# set up logging
|
17
36
|
@log = Logging.logger['Autosign::Token']
|
@@ -26,6 +45,16 @@ module Autosign
|
|
26
45
|
@validto = Time.now.to_i + self.validfor
|
27
46
|
end
|
28
47
|
|
48
|
+
# Validate an existing JSON Web Token.
|
49
|
+
#
|
50
|
+
# 1. Use the HMAC secret to validate the data in the token
|
51
|
+
# 2. Compare the expiration time in the token to the current time to determine if it's valid
|
52
|
+
# 3. compare the certname or regex of certnames in the token to the requested common name from the certificate signing request
|
53
|
+
#
|
54
|
+
# @param requested_certname [String] common name coming from the certificate signing request. This is the common name the requester wants.
|
55
|
+
# @param token [String] JSON Web Token coming from the certificate signing request
|
56
|
+
# @param hmac_secret [String] Password that the token was (hopefully) originally signed with.
|
57
|
+
# @return [True, False] returns true if the token can be validated, or false if the token cannot be validated.
|
29
58
|
def self.validate(requested_certname, token, hmac_secret)
|
30
59
|
@log = Logging.logger['Autosign::Token.validate']
|
31
60
|
@log.debug "attempting to validate token"
|
@@ -69,15 +98,29 @@ module Autosign
|
|
69
98
|
raise Autosign::Token::ValidationError
|
70
99
|
end
|
71
100
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
101
|
+
# check if the token is reusable or a one-time use token
|
102
|
+
# @return [True, False] return true if the token can be used multiple times, false if the token can only be used once
|
103
|
+
def reusable
|
104
|
+
!!@reusable
|
105
|
+
end
|
106
|
+
|
107
|
+
# convert the token to a hash
|
108
|
+
# @return [Hash{String => String}]
|
109
|
+
def to_hash
|
110
|
+
{
|
111
|
+
"certname" => certname,
|
112
|
+
"requester" => requester,
|
113
|
+
"reusable" => reusable,
|
114
|
+
"validfor" => validfor,
|
115
|
+
"uuid" => uuid
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
# Sign the token with HMAC using a SHA-512 hash
|
120
|
+
# @return [String] signed, serialized JSON web token
|
121
|
+
def sign()
|
122
|
+
exp_payload = { :data => to_json, :exp => validto.to_s}
|
123
|
+
JWT.encode exp_payload, secret, 'HS512'
|
81
124
|
end
|
82
125
|
|
83
126
|
def self.from_token(token, hmac_secret)
|
@@ -100,6 +143,23 @@ module Autosign
|
|
100
143
|
return new_token
|
101
144
|
end
|
102
145
|
|
146
|
+
# Extract the expiration time, in seconds since epoch, from a signed token. Uses HMAC secret to validate the expiration time.
|
147
|
+
# @param token [String] Serialized JSON web token
|
148
|
+
# @param hmac_secret [String] Password that the token was (hopefully) originally signed with.
|
149
|
+
# @return [Integer] POSIX time (seconds since epoch) that the token is valid until
|
150
|
+
def self.token_validto(token, hmac_secret)
|
151
|
+
begin
|
152
|
+
decoded = JWT.decode(token, hmac_secret)[0]
|
153
|
+
rescue JWT::ExpiredSignature
|
154
|
+
raise Autosign::Token::ExpiredToken
|
155
|
+
rescue
|
156
|
+
raise Autosign::Token::Invalid
|
157
|
+
end
|
158
|
+
return decoded['exp'].to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
103
163
|
def certname=(str)
|
104
164
|
@name = str
|
105
165
|
end
|
@@ -108,10 +168,6 @@ module Autosign
|
|
108
168
|
@reusable = !!bool
|
109
169
|
end
|
110
170
|
|
111
|
-
def reusable
|
112
|
-
!!@reusable
|
113
|
-
end
|
114
|
-
|
115
171
|
def requester=(str)
|
116
172
|
@requester = str
|
117
173
|
end
|
@@ -120,23 +176,8 @@ module Autosign
|
|
120
176
|
@secret = str
|
121
177
|
end
|
122
178
|
|
123
|
-
def to_hash
|
124
|
-
{
|
125
|
-
"certname" => self.certname,
|
126
|
-
"requester" => self.requester,
|
127
|
-
"reusable" => self.reusable,
|
128
|
-
"validfor" => self.validfor,
|
129
|
-
"uuid" => self.uuid
|
130
|
-
}
|
131
|
-
end
|
132
|
-
|
133
179
|
def to_json
|
134
|
-
JSON.generate
|
135
|
-
end
|
136
|
-
|
137
|
-
def sign()
|
138
|
-
exp_payload = { :data => self.to_json, :exp => self.validto.to_s}
|
139
|
-
JWT.encode exp_payload, self.secret, 'HS512'
|
180
|
+
JSON.generate to_hash
|
140
181
|
end
|
141
182
|
|
142
183
|
end
|
data/lib/autosign/validator.rb
CHANGED
@@ -2,13 +2,44 @@ require 'logging'
|
|
2
2
|
require 'require_all'
|
3
3
|
|
4
4
|
module Autosign
|
5
|
+
# Parent class for validation backends. Validators take the
|
6
|
+
# challenge_password and common name from a certificate signing request,
|
7
|
+
# and perform some action to determine whether the request is valid.
|
8
|
+
#
|
9
|
+
# Validators also get the raw X509 CSR in case the extracted information
|
10
|
+
# is insufficient for future, more powerful validators.
|
11
|
+
#
|
12
|
+
# All validators must inherit from this class, and must override several
|
13
|
+
# methods in order to function. At a minimum, the name and perform_validation
|
14
|
+
# methods must be implemented by child classes.
|
15
|
+
#
|
16
|
+
# @return [Autosign::Validator] instance of the Autosign::Validator class
|
5
17
|
class Validator
|
6
18
|
def initialize()
|
7
|
-
|
19
|
+
start_logging()
|
8
20
|
settings() # just run to validate settings
|
9
21
|
setup()
|
22
|
+
# call name to ensure that the class fails immediately if child classes
|
23
|
+
# do not implement it.
|
24
|
+
name()
|
10
25
|
end
|
11
26
|
|
27
|
+
# Name of the validator. This must be implemented by validators which
|
28
|
+
# inherit from the Autosign::Validator class. The name is used to identify
|
29
|
+
# the validator in friendly messages and to determine which configuration
|
30
|
+
# file section settings will be loaded from.
|
31
|
+
#
|
32
|
+
# @example set the name of a child class validator to "example"
|
33
|
+
# module Autosign
|
34
|
+
# module Validators
|
35
|
+
# class Example < Autosign::Validator
|
36
|
+
# def name
|
37
|
+
# "example"
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# @return [String] name of the validator. Do not use special characters.
|
12
43
|
def name
|
13
44
|
# override this after inheriting
|
14
45
|
# should return a string with no spaces
|
@@ -16,12 +47,30 @@ module Autosign
|
|
16
47
|
raise NotImplementedError
|
17
48
|
end
|
18
49
|
|
19
|
-
|
50
|
+
# define how a validator actually validates the request.
|
51
|
+
# This must be implemented by validators which inherit from the
|
52
|
+
# Autosign::Validator class.
|
53
|
+
#
|
54
|
+
# @param challenge_password [String] the challenge_password OID from the certificate signing request. The challenge_password field is the same setting as the "challengePassword" field in a `csr_attributes.yaml` file when the CSR is generated. In a request using a JSON web token, this would be the serialized token.
|
55
|
+
# @param certname [String] the common name being requested in the certificate signing request. Treat the certname as untrusted. This is user-submitted data that you must validate.
|
56
|
+
# @param raw_csr [String] the encoded X509 certificate signing request, as received by the autosign policy executable. This is provided as an optional extension point, but your validator may not need to use it.
|
57
|
+
# @return [True, False] return true if the certificate should be signed, and false if you cannot validate the request successfully.
|
58
|
+
def perform_validation(challenge_password, certname, raw_csr)
|
59
|
+
# override this after inheriting
|
60
|
+
# should return true to indicate success validating
|
61
|
+
# or false to indicate that the validator was unable to validate
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
64
|
+
|
65
|
+
# wrapper method that wraps input validation and logging around the perform_validation method.
|
66
|
+
# Do not override or use this class in child classes. This is the class that gets called
|
67
|
+
# on validator objects.
|
68
|
+
def validate(challenge_password, certname, raw_csr)
|
20
69
|
@log.debug "running validate"
|
21
70
|
fail unless challenge_password.is_a?(String)
|
22
71
|
fail unless certname.is_a?(String)
|
23
72
|
|
24
|
-
case perform_validation(challenge_password, certname)
|
73
|
+
case perform_validation(challenge_password, certname, raw_csr)
|
25
74
|
when true
|
26
75
|
@log.debug "validated successfully"
|
27
76
|
@log.info "Validated '#{certname}' using '#{name}' validator"
|
@@ -36,18 +85,27 @@ module Autosign
|
|
36
85
|
end
|
37
86
|
end
|
38
87
|
|
39
|
-
|
88
|
+
# Class method to attempt validation of a request against all validators which inherit from this class.
|
89
|
+
# The request is considered to be validated if any one validator succeeds.
|
90
|
+
# @param challenge_password [String] the challenge_password OID from the certificate signing request
|
91
|
+
# @param certname [String] the common name being requested in the certificate signing request
|
92
|
+
# @param raw_csr [String] the encoded X509 certificate signing request, as received by the autosign policy executable
|
93
|
+
# @return [True, False] return true if the certificate should be signed, and false if it cannot be validated
|
94
|
+
def self.any_validator(challenge_password, certname, raw_csr)
|
40
95
|
@log = Logging.logger[self.name]
|
41
96
|
# iterate over all known validators and attempt to validate using them
|
97
|
+
results_by_validator = {}
|
42
98
|
results = self.descendants.map {|c|
|
43
99
|
validator = c.new()
|
44
100
|
@log.debug "attempting to validate using #{validator.name}"
|
45
|
-
result = validator.validate(challenge_password, certname)
|
101
|
+
result = validator.validate(challenge_password, certname, raw_csr)
|
102
|
+
results_by_validator[validator.name] = result
|
46
103
|
@log.debug "result: #{result.to_s}"
|
47
104
|
result
|
48
105
|
}
|
49
106
|
@log.debug "validator results: " + results.to_s
|
50
|
-
|
107
|
+
@log.info "results by validator: " + results_by_validator.to_s
|
108
|
+
success = results.any?{|result| result == true}
|
51
109
|
if success
|
52
110
|
@log.info "successfully validated using one or more validators"
|
53
111
|
return true
|
@@ -58,14 +116,18 @@ module Autosign
|
|
58
116
|
end
|
59
117
|
|
60
118
|
private
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
119
|
+
|
120
|
+
# this is automatically called when the class is initialized; do not
|
121
|
+
# override it in child classes.
|
122
|
+
def start_logging
|
123
|
+
@log = Logging.logger["Autosign::Validator::" + self.name.to_s]
|
124
|
+
@log.debug "starting autosign validator: " + self.name.to_s
|
66
125
|
end
|
67
126
|
|
68
|
-
#
|
127
|
+
# (optionally) override this method in validator child classes to perform any additional
|
128
|
+
# setup during class initialization prior to beginning validation.
|
129
|
+
# If you need to create a database connection, this would be a good place to do it.
|
130
|
+
# @return [True, False] return true if setup succeeded, or false if setup failed and the validation should not continue
|
69
131
|
def setup
|
70
132
|
true
|
71
133
|
end
|
@@ -74,9 +136,19 @@ module Autosign
|
|
74
136
|
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
75
137
|
end
|
76
138
|
|
139
|
+
# provide a merged settings hash of default settings for a validator,
|
140
|
+
# config file settings for the validator, and override settings defined in
|
141
|
+
# the validator.
|
142
|
+
#
|
143
|
+
# Do not override this in child classes. If you need to set
|
144
|
+
# custom config settings, override the get_override_settings method.
|
145
|
+
# The section of the config file this reads from is the same as the name
|
146
|
+
# method returns.
|
147
|
+
#
|
148
|
+
# @return [Hash] of config settings
|
77
149
|
def settings
|
78
150
|
@log.debug "merging settings"
|
79
|
-
setting_sources = [
|
151
|
+
setting_sources = [get_override_settings, load_config, default_settings]
|
80
152
|
merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
|
81
153
|
@log.debug "using merged settings: " + merged_settings.to_s
|
82
154
|
@log.debug "validating merged settings"
|
@@ -90,28 +162,37 @@ module Autosign
|
|
90
162
|
end
|
91
163
|
end
|
92
164
|
|
93
|
-
# this
|
94
|
-
#
|
165
|
+
# (optionally) override this from a child class to set config defaults.
|
166
|
+
# These will be overridden by config file settings.
|
167
|
+
#
|
168
|
+
# Override this when inheriting if you need to set config defaults.
|
169
|
+
# For example, if you want to pull settings from zookeeper, this would
|
170
|
+
# be a good place to do that.
|
171
|
+
#
|
172
|
+
# @return [Hash] of config settings
|
95
173
|
def default_settings
|
96
174
|
{}
|
97
175
|
end
|
98
176
|
|
99
177
|
|
100
|
-
# override this to perform validation checks on the merged
|
101
|
-
# of default settings, config file settings, and override
|
102
|
-
#
|
178
|
+
# (optionally) override this to perform validation checks on the merged
|
179
|
+
# config hash of default settings, config file settings, and override
|
180
|
+
# settings.
|
181
|
+
# @return [True, False]
|
103
182
|
def validate_settings(settings)
|
104
183
|
settings.is_a?(Hash)
|
105
184
|
end
|
106
185
|
|
107
|
-
# load any required configuration
|
186
|
+
# load any required configuration from the config file.
|
187
|
+
# Do not override this in child classes.
|
188
|
+
# @return [Hash] configuration settings from the validator's section of the config file
|
108
189
|
def load_config
|
109
190
|
@log.debug "loading validator-specific configuration"
|
110
191
|
config = Autosign::Config.new
|
111
192
|
|
112
193
|
if config.settings.to_hash[self.name].nil?
|
113
|
-
@log.
|
114
|
-
@log.
|
194
|
+
@log.warn "Unable to load validator-specific configuration"
|
195
|
+
@log.warn "Cannot load configuration section named '#{self.name}'"
|
115
196
|
return {}
|
116
197
|
else
|
117
198
|
@log.debug "Set validator-specific settings from config file: " + config.settings.to_hash[self.name].to_s
|
@@ -119,8 +200,11 @@ module Autosign
|
|
119
200
|
end
|
120
201
|
end
|
121
202
|
|
122
|
-
# this
|
123
|
-
#
|
203
|
+
# (optionally) override this from child classes to get custom configuration
|
204
|
+
# from a validator.
|
205
|
+
#
|
206
|
+
# This is how you override defaults and config file settings.
|
207
|
+
# @return [Hash] configuration settings
|
124
208
|
def get_override_settings
|
125
209
|
{}
|
126
210
|
end
|
@@ -7,7 +7,7 @@ module Autosign
|
|
7
7
|
|
8
8
|
private
|
9
9
|
|
10
|
-
def perform_validation(token, certname)
|
10
|
+
def perform_validation(token, certname, raw_csr)
|
11
11
|
puts "attempting to validate JWT token"
|
12
12
|
return false unless Autosign::Token.validate(certname, token, settings['secret'])
|
13
13
|
puts "validated JWT token"
|
@@ -24,6 +24,7 @@ module Autosign
|
|
24
24
|
|
25
25
|
def add_to_journal(token)
|
26
26
|
validated_token = Autosign::Token.from_token(token, settings['secret'])
|
27
|
+
@log.debug 'add_to_journal settings: ' + settings.to_s
|
27
28
|
journal = Autosign::Journal.new({'journalfile' => settings['journalfile']})
|
28
29
|
token_expiration = Autosign::Token.token_validto(token, settings['secret'])
|
29
30
|
|
@@ -44,7 +45,17 @@ module Autosign
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def get_override_settings
|
47
|
-
|
48
|
+
# this is a hack to make testing easier
|
49
|
+
if (ENV["AUTOSIGN_TESTMODE"] == "true" and
|
50
|
+
!ENV["AUTOSIGN_TEST_SECRET"].nil? and
|
51
|
+
!ENV["AUTOSIGN_TEST_JOURNALFILE"].nil? )
|
52
|
+
{
|
53
|
+
'secret' => ENV["AUTOSIGN_TEST_SECRET"].to_s,
|
54
|
+
'journalfile' => ENV["AUTOSIGN_TEST_JOURNALFILE"].to_s
|
55
|
+
}
|
56
|
+
else
|
57
|
+
{}
|
58
|
+
end
|
48
59
|
end
|
49
60
|
|
50
61
|
def validate_settings(settings)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Autosign
|
2
|
+
module Validators
|
3
|
+
class Multiplexer < Autosign::Validator
|
4
|
+
def name
|
5
|
+
"multiplexer"
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def perform_validation(token, certname, raw_csr)
|
11
|
+
results = []
|
12
|
+
@log.debug "validating using multiplexed external executables"
|
13
|
+
policy_executables.each {|executable|
|
14
|
+
@log.debug "attempting to validate using #{executable.to_s}"
|
15
|
+
results << IO.popen(executable, 'r+') {|obj| obj.puts raw_csr; obj.close_write; obj.read; obj.close; $?.to_i }
|
16
|
+
@log.debug "exit code from #{executable.to_s}: #{results.last}"
|
17
|
+
}
|
18
|
+
bool_results = results.map {|val| val == 0}
|
19
|
+
return validate_using_strategy(bool_results)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def default_settings
|
24
|
+
{
|
25
|
+
'strategy' => 'any',
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_using_strategy(array)
|
30
|
+
case settings['strategy']
|
31
|
+
when 'any'
|
32
|
+
@log.debug "validating using 'any' strategy"
|
33
|
+
return array.any?
|
34
|
+
when 'all'
|
35
|
+
@log.debug "validating using 'all' strategy"
|
36
|
+
return array.all?
|
37
|
+
else
|
38
|
+
@log.error "unable to validate; unknown strategy"
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def policy_executables
|
44
|
+
return [] if settings['external_policy_executable'].nil?
|
45
|
+
exec_list = settings['external_policy_executable']
|
46
|
+
return [exec_list] if exec_list.is_a?(String)
|
47
|
+
return exec_list if exec_list.is_a?(Array)
|
48
|
+
return []
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def validate_settings(settings)
|
53
|
+
@log.debug "validating settings: " + settings.to_s
|
54
|
+
unless ['any', 'all'].include? settings['strategy']
|
55
|
+
@log.error "strategy setting must be set to 'any' or 'all'"
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
@log.debug "done validating settings"
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/autosign/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: autosign
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Your Name Here
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-07-
|
11
|
+
date: 2015-07-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: cucumber
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: puppet
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +136,20 @@ dependencies:
|
|
122
136
|
- - ">="
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: json
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
125
153
|
- !ruby/object:Gem::Dependency
|
126
154
|
name: deep_merge
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,6 +178,20 @@ dependencies:
|
|
150
178
|
- - ">="
|
151
179
|
- !ruby/object:Gem::Version
|
152
180
|
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: yard
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :runtime
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
153
195
|
description:
|
154
196
|
email: your@email.address.com
|
155
197
|
executables:
|
@@ -159,8 +201,10 @@ extra_rdoc_files:
|
|
159
201
|
- README.rdoc
|
160
202
|
- autosign.rdoc
|
161
203
|
files:
|
204
|
+
- ".travis.yml"
|
162
205
|
- Gemfile
|
163
206
|
- Gemfile.lock
|
207
|
+
- README.md
|
164
208
|
- README.rdoc
|
165
209
|
- Rakefile
|
166
210
|
- autosign.gemspec
|
@@ -180,6 +224,7 @@ files:
|
|
180
224
|
- lib/autosign/token.rb
|
181
225
|
- lib/autosign/validator.rb
|
182
226
|
- lib/autosign/validators/jwt.rb
|
227
|
+
- lib/autosign/validators/multiplexer.rb
|
183
228
|
- lib/autosign/version.rb
|
184
229
|
homepage: http://your.website.com
|
185
230
|
licenses: []
|