autosign 0.1.4 → 1.0.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/.rubocop.yml +12 -0
- data/.rubocop_todo.yml +659 -0
- data/.travis.yml +4 -4
- data/CHANGELOG.md +53 -0
- data/Gemfile.lock +66 -34
- data/README.md +37 -0
- data/Rakefile +22 -22
- data/autosign.gemspec +23 -18
- data/bin/autosign +10 -6
- data/bin/autosign-validator +5 -5
- data/lib/autosign/config.rb +66 -52
- data/lib/autosign/validator.rb +33 -196
- data/lib/autosign/{validators → validator}/jwt.rb +41 -42
- data/lib/autosign/{validators → validator}/multiplexer.rb +24 -32
- data/lib/autosign/{validators → validator}/passwordlist.rb +16 -17
- data/lib/autosign/validator/validator_base.rb +168 -0
- data/lib/autosign/version.rb +1 -1
- metadata +78 -75
- data/features/autosign.feature +0 -93
- data/features/step_definitions/autosign_steps.rb +0 -44
- data/features/support/env.rb +0 -17
- data/features/validate.feature +0 -22
- data/fixtures/i-7672fe81.pem +0 -34
- data/spec/spec_helper.rb +0 -102
- data/spec/specs/config_spec.rb +0 -20
- data/spec/specs/decoder_spec.rb +0 -24
- data/spec/specs/journal_spec.rb +0 -41
- data/spec/specs/token_spec.rb +0 -102
- data/spec/specs/validators/jwt_spec.rb +0 -69
- data/spec/specs/validators/passwordlist_spec.rb +0 -51
@@ -1,20 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'autosign/validator/validator_base'
|
3
|
+
|
1
4
|
module Autosign
|
2
|
-
module
|
5
|
+
module Validator
|
3
6
|
# Validate certificate signing requests using JSON Web Tokens (JWT).
|
4
7
|
# This is the expected primary validator when using the autosign gem.
|
5
8
|
# Validation requires that the shared secret used to generate the JWT is
|
6
9
|
# the same as on the validating system. The validator also checks that the
|
7
10
|
# token has not expired, and that one-time (non-reusable) tokens have not
|
8
11
|
# been previously used.
|
9
|
-
class JWT < Autosign::Validator
|
10
|
-
|
11
|
-
# set the user-friendly name of the JWT validator.
|
12
|
-
# This name is used to specify that configuration should come from the
|
13
|
-
# [jwt_token] section of the autosign.conf file.
|
14
|
-
# @return [String] name of the validator
|
15
|
-
def name
|
16
|
-
"jwt_token"
|
17
|
-
end
|
12
|
+
class JWT < Autosign::Validator::ValidatorBase
|
13
|
+
NAME = 'jwt_token'
|
18
14
|
|
19
15
|
private
|
20
16
|
|
@@ -26,15 +22,19 @@ module Autosign
|
|
26
22
|
# @param certname [String] the certname being requested in the certificate signing request
|
27
23
|
# @param raw_csr [String] Raw CSR; not used in this validator.
|
28
24
|
# @return [True, False] returns true to indicate successful validation, and false to indicate failure to validate
|
29
|
-
def perform_validation(token, certname,
|
30
|
-
@log.info "attempting to validate
|
31
|
-
|
32
|
-
|
33
|
-
|
25
|
+
def perform_validation(token, certname, _raw_csr)
|
26
|
+
@log.info "attempting to validate with #{name}"
|
27
|
+
unless Autosign::Token.validate(certname, token, settings['secret'])
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
|
31
|
+
@log.info 'validated JWT token'
|
32
|
+
@log.debug 'validated JWT token, checking reusability'
|
34
33
|
|
35
34
|
return true if is_reusable?(token)
|
36
35
|
return true if add_to_journal(token)
|
37
|
-
|
36
|
+
|
37
|
+
false
|
38
38
|
end
|
39
39
|
|
40
40
|
def is_reusable?(token)
|
@@ -49,16 +49,16 @@ module Autosign
|
|
49
49
|
def add_to_journal(token)
|
50
50
|
validated_token = Autosign::Token.from_token(token, settings['secret'])
|
51
51
|
@log.debug 'add_to_journal settings: ' + settings.to_s
|
52
|
-
journal = Autosign::Journal.new(
|
52
|
+
journal = Autosign::Journal.new('journalfile' => settings['journalfile'])
|
53
53
|
token_expiration = Autosign::Token.token_validto(token, settings['secret'])
|
54
54
|
|
55
55
|
# adding will return false if the token is already in the journal
|
56
56
|
if journal.add(validated_token.uuid, token_expiration, validated_token.to_hash)
|
57
57
|
@log.info "added token with UUID '#{validated_token.uuid}' to journal"
|
58
|
-
|
58
|
+
true
|
59
59
|
else
|
60
|
-
@log.warn
|
61
|
-
|
60
|
+
@log.warn 'journal cannot validate one-time token; may already have been used'
|
61
|
+
false
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
@@ -75,34 +75,33 @@ module Autosign
|
|
75
75
|
#
|
76
76
|
# This should probably be done differently at some point.
|
77
77
|
def get_override_settings
|
78
|
-
if (ENV[
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
78
|
+
if (ENV['AUTOSIGN_TESTMODE'] == 'true') &&
|
79
|
+
!ENV['AUTOSIGN_TEST_SECRET'].nil? &&
|
80
|
+
!ENV['AUTOSIGN_TEST_JOURNALFILE'].nil?
|
81
|
+
{
|
82
|
+
'secret' => ENV['AUTOSIGN_TEST_SECRET'].to_s,
|
83
|
+
'journalfile' => ENV['AUTOSIGN_TEST_JOURNALFILE'].to_s
|
84
|
+
}
|
85
85
|
else
|
86
86
|
{}
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
90
|
+
# Validate that the settings hash contains a secret.
|
91
|
+
# The validator cannot function without a secret, so there's no point
|
92
|
+
# in continuing to run if it was configured without a secret.
|
93
|
+
# @param settings [Hash] settings hash
|
94
|
+
# @return [True, False] return true if settings are valid, false if config is unusable
|
95
|
+
def validate_settings(settings)
|
96
|
+
@log.debug 'validating settings: ' + settings.to_s
|
97
|
+
if settings['secret'].is_a?(String)
|
98
|
+
@log.info "validated settings successfully for #{name}"
|
99
|
+
true
|
100
|
+
else
|
101
|
+
@log.error 'no secret setting found'
|
102
|
+
false
|
103
|
+
end
|
103
104
|
end
|
104
105
|
end
|
105
|
-
|
106
|
-
end
|
107
106
|
end
|
108
107
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'autosign/validator/validator_base'
|
3
3
|
|
4
|
+
module Autosign
|
5
|
+
module Validator
|
4
6
|
# The multiplexer validator sends the same request received by the autosign
|
5
7
|
# executable to one or more external executables. The purpose is to allow
|
6
8
|
# one or more existing autosign scripts to be used in conjunction with the
|
@@ -25,15 +27,8 @@ module Autosign
|
|
25
27
|
# external_policy_executable = /usr/local/bin/another-autosign-script.rb
|
26
28
|
# # requests will only be validated by the multiplexer validator if they
|
27
29
|
# # are validated by both external policy executables.
|
28
|
-
class Multiplexer < Autosign::Validator
|
29
|
-
|
30
|
-
# set the user-friendly name of the Multiplexer validator.
|
31
|
-
# This name is used to specify that configuration should come from the
|
32
|
-
# [multiplexer] section of the autosign.conf file.
|
33
|
-
# @return [String] name of the validator
|
34
|
-
def name
|
35
|
-
"multiplexer"
|
36
|
-
end
|
30
|
+
class Multiplexer < Autosign::Validator::ValidatorBase
|
31
|
+
NAME = 'multiplexer'
|
37
32
|
|
38
33
|
private
|
39
34
|
|
@@ -42,25 +37,24 @@ module Autosign
|
|
42
37
|
# @param certname [String] certname requested in the CSR
|
43
38
|
# @param raw_csr [String] X509 certificate signing request as received by the policy executable
|
44
39
|
# @return [True, False] returns true to indicate successful validation, and false to indicate failure to validate
|
45
|
-
def perform_validation(
|
40
|
+
def perform_validation(_token, certname, raw_csr)
|
46
41
|
results = []
|
47
|
-
@log.debug
|
48
|
-
policy_executables.each
|
49
|
-
@log.debug "attempting to validate using #{executable
|
50
|
-
results << IO.popen(executable + ' ' + certname.to_s, 'r+') {|obj| obj.puts raw_csr; obj.close_write; obj.read; obj.close;
|
51
|
-
@log.debug "exit code from #{executable
|
52
|
-
|
53
|
-
bool_results = results.map {|val| val == 0}
|
54
|
-
|
42
|
+
@log.debug 'validating using multiplexed external executables'
|
43
|
+
policy_executables.each do |executable|
|
44
|
+
@log.debug "attempting to validate using #{executable}"
|
45
|
+
results << IO.popen(executable + ' ' + certname.to_s, 'r+') { |obj| obj.puts raw_csr; obj.close_write; obj.read; obj.close; $CHILD_STATUS.to_i }
|
46
|
+
@log.debug "exit code from #{executable}: #{results.last}"
|
47
|
+
end
|
48
|
+
bool_results = results.map { |val| val == 0 }
|
49
|
+
validate_using_strategy(bool_results)
|
55
50
|
end
|
56
51
|
|
57
|
-
|
58
52
|
# set the default validation strategy to "any", succeeding if any one
|
59
53
|
# external autosign script succeeds.
|
60
54
|
# @return [Hash] config hash to be merged in with config file settings and overrides.
|
61
55
|
def default_settings
|
62
56
|
{
|
63
|
-
'strategy' => 'any'
|
57
|
+
'strategy' => 'any'
|
64
58
|
}
|
65
59
|
end
|
66
60
|
|
@@ -72,13 +66,13 @@ module Autosign
|
|
72
66
|
case settings['strategy']
|
73
67
|
when 'any'
|
74
68
|
@log.debug "validating using 'any' strategy"
|
75
|
-
|
69
|
+
array.any?
|
76
70
|
when 'all'
|
77
71
|
@log.debug "validating using 'all' strategy"
|
78
|
-
|
72
|
+
array.all?
|
79
73
|
else
|
80
|
-
@log.error
|
81
|
-
|
74
|
+
@log.error 'unable to validate; unknown strategy'
|
75
|
+
false
|
82
76
|
end
|
83
77
|
end
|
84
78
|
|
@@ -86,25 +80,23 @@ module Autosign
|
|
86
80
|
# or an empty array if none are specified.
|
87
81
|
# @return [Array] of policy executables.
|
88
82
|
def policy_executables
|
89
|
-
|
83
|
+
Array(settings['external_policy_executable'])
|
90
84
|
end
|
91
85
|
|
92
|
-
|
93
86
|
# validate that settins are reasonable. Validation strategy must be
|
94
87
|
# either any or all.
|
95
88
|
# @param settings [Hash] config settings hash
|
96
89
|
# @return [True, False] true if settings validate successfully, false otherwise
|
97
90
|
def validate_settings(settings)
|
98
|
-
@log.debug
|
99
|
-
unless [
|
91
|
+
@log.debug 'validating settings: ' + settings.to_s
|
92
|
+
unless %w[any all].include? settings['strategy']
|
100
93
|
@log.error "strategy setting must be set to 'any' or 'all'"
|
101
94
|
return false
|
102
95
|
end
|
103
96
|
|
104
|
-
@log.debug
|
97
|
+
@log.debug 'done validating settings'
|
105
98
|
true
|
106
99
|
end
|
107
|
-
|
108
100
|
end
|
109
101
|
end
|
110
102
|
end
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'autosign/validator/validator_base'
|
1
3
|
module Autosign
|
2
|
-
module
|
4
|
+
module Validator
|
3
5
|
# Validate certificate signing requests using a simple password list.
|
4
6
|
# This is not a very secure or flexible validation scheme, but is provided
|
5
7
|
# because so many existing autosign policy scripts implement it.
|
@@ -11,35 +13,32 @@ module Autosign
|
|
11
13
|
# password = opensesame
|
12
14
|
# password = CPE1704TKS
|
13
15
|
#
|
14
|
-
class Passwordlist < Autosign::Validator
|
15
|
-
|
16
|
-
"password_list"
|
17
|
-
end
|
16
|
+
class Passwordlist < Autosign::Validator::ValidatorBase
|
17
|
+
NAME = 'password_list'
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
def perform_validation(password,
|
22
|
-
@log.debug
|
23
|
-
@log.debug
|
21
|
+
def perform_validation(password, _certname, _raw_csr)
|
22
|
+
@log.debug 'validating against simple password list'
|
23
|
+
@log.debug 'passwords: ' + settings.to_s
|
24
24
|
result = validate_password(password.to_s)
|
25
|
-
@log.debug
|
26
|
-
|
25
|
+
@log.debug 'validation result: ' + result.to_s
|
26
|
+
result
|
27
27
|
end
|
28
28
|
|
29
29
|
def validate_password(password)
|
30
|
-
@log.debug
|
30
|
+
@log.debug 'Checking if password list includes password'
|
31
31
|
password_list.include?(password.to_s)
|
32
32
|
end
|
33
33
|
|
34
34
|
def password_list
|
35
|
-
|
35
|
+
Array(settings['password'])
|
36
36
|
end
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
def validate_settings(settings)
|
39
|
+
@log.debug 'validating settings: ' + settings.to_s
|
40
|
+
true
|
41
|
+
end
|
43
42
|
end
|
44
43
|
end
|
45
44
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logging'
|
4
|
+
|
5
|
+
module Autosign
|
6
|
+
module Validator
|
7
|
+
# Parent class for validation backends. Validator take the
|
8
|
+
# challenge_password and common name from a certificate signing request,
|
9
|
+
# and perform some action to determine whether the request is valid.
|
10
|
+
#
|
11
|
+
# Validator also get the raw X509 CSR in case the extracted information
|
12
|
+
# is insufficient for future, more powerful validators.
|
13
|
+
#
|
14
|
+
# All validators must inherit from this class, and must override several
|
15
|
+
# methods in order to function. At a minimum, the name and perform_validation
|
16
|
+
# methods must be implemented by child classes.
|
17
|
+
#
|
18
|
+
# @return Autosign::Validator::ValidatorBase instance of the Autosign::Validator::ValidatorBase class
|
19
|
+
class ValidatorBase
|
20
|
+
NAME = 'base'
|
21
|
+
attr_reader :config_file_settings
|
22
|
+
|
23
|
+
def initialize(config_file_settings = nil)
|
24
|
+
@config_file_settings = config_file_settings
|
25
|
+
start_logging
|
26
|
+
settings # just run to validate settings
|
27
|
+
setup
|
28
|
+
# call name to ensure that the class fails immediately if child classes
|
29
|
+
# do not implement it.
|
30
|
+
name
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String] name of the validator. Do not use special characters.
|
34
|
+
# You must set the NAME constant in the sublcass
|
35
|
+
def name
|
36
|
+
self.class::NAME
|
37
|
+
end
|
38
|
+
|
39
|
+
# define how a validator actually validates the request.
|
40
|
+
# This must be implemented by validators which inherit from the
|
41
|
+
# Autosign::Validator class.
|
42
|
+
#
|
43
|
+
# @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.
|
44
|
+
# @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.
|
45
|
+
# @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.
|
46
|
+
# @return [True, False] return true if the certificate should be signed, and false if you cannot validate the request successfully.
|
47
|
+
def perform_validation(_challenge_password, _certname, _raw_csr)
|
48
|
+
# override this after inheriting
|
49
|
+
# should return true to indicate success validating
|
50
|
+
# or false to indicate that the validator was unable to validate
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
# wrapper method that wraps input validation and logging around the perform_validation method.
|
55
|
+
# Do not override or use this class in child classes. This is the class that gets called
|
56
|
+
# on validator objects.
|
57
|
+
def validate(challenge_password, certname, raw_csr)
|
58
|
+
raise unless challenge_password.is_a?(String)
|
59
|
+
raise unless certname.is_a?(String)
|
60
|
+
|
61
|
+
case perform_validation(challenge_password, certname, raw_csr)
|
62
|
+
when true
|
63
|
+
@log.debug 'validated successfully'
|
64
|
+
@log.info "Validated '#{certname}' using '#{name}' validator"
|
65
|
+
true
|
66
|
+
when false
|
67
|
+
@log.debug 'validation failed'
|
68
|
+
@log.debug "Unable to validate '#{certname}' using '#{name}' validator"
|
69
|
+
false
|
70
|
+
else
|
71
|
+
@log.error 'perform_validation returned a non-boolean result'
|
72
|
+
raise 'perform_validation returned a non-boolean result'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# this is automatically called when the class is initialized; do not
|
79
|
+
# override it in child classes.
|
80
|
+
def start_logging
|
81
|
+
@log = Logging.logger[self.class]
|
82
|
+
@log.debug 'starting autosign validator: ' + name.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
# (optionally) override this method in validator child classes to perform any additional
|
86
|
+
# setup during class initialization prior to beginning validation.
|
87
|
+
# If you need to create a database connection, this would be a good place to do it.
|
88
|
+
# @return [True, False] return true if setup succeeded, or false if setup failed and the validation should not continue
|
89
|
+
def setup
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
# provide a merged settings hash of default settings for a validator,
|
94
|
+
# config file settings for the validator, and override settings defined in
|
95
|
+
# the validator.
|
96
|
+
#
|
97
|
+
# Do not override this in child classes. If you need to set
|
98
|
+
# custom config settings, override the get_override_settings method.
|
99
|
+
# The section of the config file this reads from is the same as the name
|
100
|
+
# method returns.
|
101
|
+
#
|
102
|
+
# @return [Hash] of config settings
|
103
|
+
def settings
|
104
|
+
@settings ||= begin
|
105
|
+
@log.debug "merging settings for #{name} validator"
|
106
|
+
setting_sources = [get_override_settings, load_config, default_settings]
|
107
|
+
merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash, {:overwrite_arrays => true}) }
|
108
|
+
@log.debug 'using merged settings: ' + merged_settings.to_s
|
109
|
+
@log.debug 'validating merged settings'
|
110
|
+
if validate_settings(merged_settings)
|
111
|
+
@log.debug 'successfully validated merged settings'
|
112
|
+
merged_settings
|
113
|
+
else
|
114
|
+
@log.warn 'validation of merged settings failed'
|
115
|
+
@log.warn "unable to validate settings in #{name} validator"
|
116
|
+
raise 'settings validation error'
|
117
|
+
end
|
118
|
+
merged_settings
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# (optionally) override this from a child class to set config defaults.
|
123
|
+
# These will be overridden by config file settings.
|
124
|
+
#
|
125
|
+
# Override this when inheriting if you need to set config defaults.
|
126
|
+
# For example, if you want to pull settings from zookeeper, this would
|
127
|
+
# be a good place to do that.
|
128
|
+
#
|
129
|
+
# @return [Hash] of config settings
|
130
|
+
def default_settings
|
131
|
+
{}
|
132
|
+
end
|
133
|
+
|
134
|
+
# (optionally) override this to perform validation checks on the merged
|
135
|
+
# config hash of default settings, config file settings, and override
|
136
|
+
# settings.
|
137
|
+
# @return [True, False]
|
138
|
+
def validate_settings(settings)
|
139
|
+
settings.is_a?(Hash)
|
140
|
+
end
|
141
|
+
|
142
|
+
# load any required configuration from the config file.
|
143
|
+
# Do not override this in child classes.
|
144
|
+
# @return [Hash] configuration settings from the validator's section of the config file
|
145
|
+
def load_config
|
146
|
+
@log.debug 'loading validator-specific configuration'
|
147
|
+
config_settings = @config_file_settings ||= Autosign::Config.new.settings
|
148
|
+
if config_settings.to_hash[name].nil?
|
149
|
+
@log.warn 'Unable to load validator-specific configuration'
|
150
|
+
@log.warn "Cannot load configuration section named '#{name}'"
|
151
|
+
{}
|
152
|
+
else
|
153
|
+
@log.debug 'Set validator-specific settings from config file: ' + config_settings.to_hash[name].to_s
|
154
|
+
config_settings.to_hash[name]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# (optionally) override this from child classes to get custom configuration
|
159
|
+
# from a validator.
|
160
|
+
#
|
161
|
+
# This is how you override defaults and config file settings.
|
162
|
+
# @return [Hash] configuration settings
|
163
|
+
def get_override_settings
|
164
|
+
{}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|