autosign 0.1.1 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +12 -0
- data/.rubocop_todo.yml +659 -0
- data/.travis.yml +4 -5
- data/CHANGELOG.md +56 -0
- data/Gemfile.lock +107 -89
- data/LICENSE +201 -0
- data/README.md +37 -0
- data/Rakefile +22 -22
- data/autosign.gemspec +24 -20
- data/bin/autosign +23 -15
- data/bin/autosign-validator +14 -6
- data/lib/autosign.rb +1 -1
- data/lib/autosign/config.rb +71 -56
- data/lib/autosign/decoder.rb +7 -3
- data/lib/autosign/journal.rb +2 -2
- data/lib/autosign/token.rb +7 -7
- data/lib/autosign/validator.rb +34 -197
- 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 -74
- 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 -16
- 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
data/lib/autosign/decoder.rb
CHANGED
@@ -9,7 +9,7 @@ module Autosign
|
|
9
9
|
# @param csr[String] X509 format CSR
|
10
10
|
# @return [Hash] hash containing :challenge_password and :common_name keys
|
11
11
|
def self.decode_csr(csr)
|
12
|
-
@log = Logging.logger[
|
12
|
+
@log = Logging.logger[self.class]
|
13
13
|
@log.debug "decoding CSR"
|
14
14
|
|
15
15
|
begin
|
@@ -23,8 +23,12 @@ module Autosign
|
|
23
23
|
end
|
24
24
|
|
25
25
|
# extract challenge password
|
26
|
-
|
27
|
-
challenge_password =
|
26
|
+
challenge_attr = csr.attributes.find { |a| a.oid == 'challengePassword' }
|
27
|
+
challenge_password = if challenge_attr
|
28
|
+
challenge_attr.value.value.first.value.to_s
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
28
32
|
|
29
33
|
# extract common name
|
30
34
|
common_name = /^\/CN=(\S*)$/.match(csr.subject.to_s)[1]
|
data/lib/autosign/journal.rb
CHANGED
@@ -15,8 +15,8 @@ module Autosign
|
|
15
15
|
# @param settings [Hash] config settings for the new journal instance
|
16
16
|
# @return [Autosign::Journal] instance of the Autosign::Journal class
|
17
17
|
def initialize(settings = {})
|
18
|
-
@log = Logging.logger[
|
19
|
-
@log.debug "initializing
|
18
|
+
@log = Logging.logger[self.class]
|
19
|
+
@log.debug "initializing #{self.class.name}"
|
20
20
|
@settings = settings
|
21
21
|
fail unless setup
|
22
22
|
end
|
data/lib/autosign/token.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Autosign
|
2
2
|
require 'jwt'
|
3
|
-
require '
|
3
|
+
require 'multi_json'
|
4
4
|
require 'securerandom'
|
5
5
|
|
6
6
|
# Class modeling JSON Web Tokens as credentials for certificate auto signing.
|
@@ -33,8 +33,8 @@ module Autosign
|
|
33
33
|
# @return [Autosign::Config] instance of the Autosign::Config class
|
34
34
|
def initialize(certname, reusable=false, validfor=7200, requester, secret)
|
35
35
|
# set up logging
|
36
|
-
@log = Logging.logger[
|
37
|
-
@log.debug "initializing"
|
36
|
+
@log = Logging.logger[self.class]
|
37
|
+
@log.debug "initializing #{self.class.name}"
|
38
38
|
|
39
39
|
@validfor = validfor
|
40
40
|
@certname = certname
|
@@ -56,13 +56,13 @@ module Autosign
|
|
56
56
|
# @param hmac_secret [String] Password that the token was (hopefully) originally signed with.
|
57
57
|
# @return [True, False] returns true if the token can be validated, or false if the token cannot be validated.
|
58
58
|
def self.validate(requested_certname, token, hmac_secret)
|
59
|
-
@log = Logging.logger[
|
59
|
+
@log = Logging.logger[self.class]
|
60
60
|
@log.debug "attempting to validate token"
|
61
61
|
@log.info "attempting to validate token for: #{requested_certname.to_s}"
|
62
62
|
errors = []
|
63
63
|
begin
|
64
64
|
@log.debug "Decoding and parsing token"
|
65
|
-
data =
|
65
|
+
data = MultiJson.load(JWT.decode(token, hmac_secret)[0]["data"])
|
66
66
|
rescue JWT::ExpiredSignature
|
67
67
|
@log.warn "Token has an expired signature"
|
68
68
|
errors << "Expired Signature"
|
@@ -142,7 +142,7 @@ module Autosign
|
|
142
142
|
rescue
|
143
143
|
raise Autosign::Token::Invalid
|
144
144
|
end
|
145
|
-
cert_data =
|
145
|
+
cert_data = MultiJson.load(decoded["data"])
|
146
146
|
new_token = self.new(cert_data["certname"], cert_data["reusable"], cert_data["validfor"],
|
147
147
|
cert_data["requester"], hmac_secret)
|
148
148
|
|
@@ -186,7 +186,7 @@ module Autosign
|
|
186
186
|
end
|
187
187
|
|
188
188
|
def to_json
|
189
|
-
|
189
|
+
MultiJson.dump to_hash
|
190
190
|
end
|
191
191
|
|
192
192
|
end
|
data/lib/autosign/validator.rb
CHANGED
@@ -1,221 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'logging'
|
2
|
-
require 'require_all'
|
3
4
|
|
4
5
|
module Autosign
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
class Validator
|
18
|
-
def initialize()
|
19
|
-
start_logging()
|
20
|
-
settings() # just run to validate settings
|
21
|
-
setup()
|
22
|
-
# call name to ensure that the class fails immediately if child classes
|
23
|
-
# do not implement it.
|
24
|
-
name()
|
25
|
-
end
|
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.
|
43
|
-
def name
|
44
|
-
# override this after inheriting
|
45
|
-
# should return a string with no spaces
|
46
|
-
# this is the name used to reference the validator in config files
|
47
|
-
raise NotImplementedError
|
48
|
-
end
|
49
|
-
|
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)
|
69
|
-
@log.debug "running validate"
|
70
|
-
fail unless challenge_password.is_a?(String)
|
71
|
-
fail unless certname.is_a?(String)
|
72
|
-
|
73
|
-
case perform_validation(challenge_password, certname, raw_csr)
|
74
|
-
when true
|
75
|
-
@log.debug "validated successfully"
|
76
|
-
@log.info "Validated '#{certname}' using '#{name}' validator"
|
77
|
-
return true
|
78
|
-
when false
|
79
|
-
@log.debug "validation failed"
|
80
|
-
@log.debug "Unable to validate '#{certname}' using '#{name}' validator"
|
81
|
-
return false
|
82
|
-
else
|
83
|
-
@log.error "perform_validation returned a non-boolean result"
|
84
|
-
raise "perform_validation returned a non-boolean result"
|
6
|
+
module Validator
|
7
|
+
|
8
|
+
# @return [Array] - A list of all the validator classes
|
9
|
+
# @param list [Array] - a list of validators to use, uses the settings list by default
|
10
|
+
# This returns a list of validators that were specified by the user and the exact
|
11
|
+
# order they want the validation to procede.
|
12
|
+
def self.validation_order(settings = Autosign::Config.new.settings, list = nil)
|
13
|
+
validation_order = list || settings['general']['validation_order']
|
14
|
+
# create a key pair where the key is the name of the validator and value is the class
|
15
|
+
validator_list = validator_classes.each_with_object({}) do |klass, acc|
|
16
|
+
acc[klass::NAME] = klass
|
17
|
+
acc
|
85
18
|
end
|
19
|
+
# filter out validators that do not exist
|
20
|
+
order = validation_order.map { |v| validator_list.fetch(v, nil) }.compact
|
21
|
+
@log = Logging.logger[self.class]
|
22
|
+
@log.debug("Validator order: #{order.inspect}")
|
23
|
+
order
|
86
24
|
end
|
87
25
|
|
26
|
+
# @summary
|
88
27
|
# Class method to attempt validation of a request against all validators which inherit from this class.
|
89
28
|
# The request is considered to be validated if any one validator succeeds.
|
29
|
+
# The first validator to pass shorts the validation process so other validators are not called.
|
90
30
|
# @param challenge_password [String] the challenge_password OID from the certificate signing request
|
91
31
|
# @param certname [String] the common name being requested in the certificate signing request
|
92
32
|
# @param raw_csr [String] the encoded X509 certificate signing request, as received by the autosign policy executable
|
93
|
-
# @return [
|
94
|
-
def self.any_validator(challenge_password, certname, raw_csr)
|
95
|
-
@log = Logging.logger[self.
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
result = validator.validate(challenge_password, certname, raw_csr)
|
102
|
-
results_by_validator[validator.name] = result
|
103
|
-
@log.debug "result: #{result.to_s}"
|
104
|
-
result
|
105
|
-
}
|
106
|
-
@log.debug "validator results: " + results.to_s
|
107
|
-
@log.info "results by validator: " + results_by_validator.to_s
|
108
|
-
success = results.any?{|result| result == true}
|
109
|
-
if success
|
110
|
-
@log.info "successfully validated using one or more validators"
|
111
|
-
return true
|
33
|
+
# @return [Boolean] return true if the certificate should be signed, and false if it cannot be validated
|
34
|
+
def self.any_validator(challenge_password, certname, raw_csr, settings = Autosign::Config.new.settings)
|
35
|
+
@log = Logging.logger[self.class]
|
36
|
+
# find the first validator that passes and return the class
|
37
|
+
validator = validation_order(settings).find { |c| c.new(settings).validate(challenge_password, certname, raw_csr) }
|
38
|
+
if validator
|
39
|
+
@log.info "Successfully validated using #{validator::NAME}"
|
40
|
+
true
|
112
41
|
else
|
113
|
-
@log.info
|
114
|
-
|
42
|
+
@log.info 'unable to validate using any validator'
|
43
|
+
false
|
115
44
|
end
|
116
45
|
end
|
117
46
|
|
118
47
|
private
|
119
48
|
|
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
|
125
|
-
end
|
126
|
-
|
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
|
131
|
-
def setup
|
132
|
-
true
|
133
|
-
end
|
134
|
-
|
135
49
|
# Find other classes that inherit from this class.
|
136
50
|
# Used to discover autosign validators. There is probably no reason to use
|
137
51
|
# this directly.
|
138
52
|
# @return [Array] of classes inheriting from Autosign::Validator
|
139
|
-
def self.
|
140
|
-
|
53
|
+
def self.validator_classes
|
54
|
+
validators = Dir.glob(File.join(__dir__, 'validator', '*')).sort.each {|k| require k }
|
55
|
+
ObjectSpace.each_object(Class).select { |klass| klass < Autosign::Validator::ValidatorBase }
|
141
56
|
end
|
142
|
-
|
143
|
-
# provide a merged settings hash of default settings for a validator,
|
144
|
-
# config file settings for the validator, and override settings defined in
|
145
|
-
# the validator.
|
146
|
-
#
|
147
|
-
# Do not override this in child classes. If you need to set
|
148
|
-
# custom config settings, override the get_override_settings method.
|
149
|
-
# The section of the config file this reads from is the same as the name
|
150
|
-
# method returns.
|
151
|
-
#
|
152
|
-
# @return [Hash] of config settings
|
153
|
-
def settings
|
154
|
-
@log.debug "merging settings"
|
155
|
-
setting_sources = [get_override_settings, load_config, default_settings]
|
156
|
-
merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
|
157
|
-
@log.debug "using merged settings: " + merged_settings.to_s
|
158
|
-
@log.debug "validating merged settings"
|
159
|
-
if validate_settings(merged_settings)
|
160
|
-
@log.debug "successfully validated merged settings"
|
161
|
-
return merged_settings
|
162
|
-
else
|
163
|
-
@log.warn "validation of merged settings failed"
|
164
|
-
@log.warn "unable to validate settings in #{self.name} validator"
|
165
|
-
raise "settings validation error"
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
# (optionally) override this from a child class to set config defaults.
|
170
|
-
# These will be overridden by config file settings.
|
171
|
-
#
|
172
|
-
# Override this when inheriting if you need to set config defaults.
|
173
|
-
# For example, if you want to pull settings from zookeeper, this would
|
174
|
-
# be a good place to do that.
|
175
|
-
#
|
176
|
-
# @return [Hash] of config settings
|
177
|
-
def default_settings
|
178
|
-
{}
|
179
|
-
end
|
180
|
-
|
181
|
-
|
182
|
-
# (optionally) override this to perform validation checks on the merged
|
183
|
-
# config hash of default settings, config file settings, and override
|
184
|
-
# settings.
|
185
|
-
# @return [True, False]
|
186
|
-
def validate_settings(settings)
|
187
|
-
settings.is_a?(Hash)
|
188
|
-
end
|
189
|
-
|
190
|
-
# load any required configuration from the config file.
|
191
|
-
# Do not override this in child classes.
|
192
|
-
# @return [Hash] configuration settings from the validator's section of the config file
|
193
|
-
def load_config
|
194
|
-
@log.debug "loading validator-specific configuration"
|
195
|
-
config = Autosign::Config.new
|
196
|
-
|
197
|
-
if config.settings.to_hash[self.name].nil?
|
198
|
-
@log.warn "Unable to load validator-specific configuration"
|
199
|
-
@log.warn "Cannot load configuration section named '#{self.name}'"
|
200
|
-
return {}
|
201
|
-
else
|
202
|
-
@log.debug "Set validator-specific settings from config file: " + config.settings.to_hash[self.name].to_s
|
203
|
-
return config.settings.to_hash[self.name]
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
# (optionally) override this from child classes to get custom configuration
|
208
|
-
# from a validator.
|
209
|
-
#
|
210
|
-
# This is how you override defaults and config file settings.
|
211
|
-
# @return [Hash] configuration settings
|
212
|
-
def get_override_settings
|
213
|
-
{}
|
214
|
-
end
|
215
|
-
|
216
57
|
end
|
217
58
|
end
|
218
|
-
|
219
|
-
# must run at the end because the validators inherit this class
|
220
|
-
# this loads all validators
|
221
|
-
require_rel 'validators'
|
@@ -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
|