autosign 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/danieldreier/autosign.svg?branch=master)](https://travis-ci.org/danieldreier/autosign) [![Code Climate](https://codeclimate.com/github/danieldreier/autosign/badges/gpa.svg)](https://codeclimate.com/github/danieldreier/autosign) [![Dependency Status](https://gemnasium.com/danieldreier/autosign.svg)](https://gemnasium.com/danieldreier/autosign) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/danieldreier/autosign) [![Inline docs](http://inch-ci.org/github/danieldreier/autosign.png)](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: []
|