autosign 0.1.1 → 1.0.1
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 +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
|