autosign 0.1.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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['Autosign::Decoder']
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 = csr.attributes.find { |a| a.oid == 'challengePassword' }.value.value.first.value.to_s
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]
@@ -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['Autosign::Journal']
19
- @log.debug "initializing Autosign::Journal"
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
@@ -1,6 +1,6 @@
1
1
  module Autosign
2
2
  require 'jwt'
3
- require 'json'
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['Autosign::Token']
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['Autosign::Token.validate']
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 = JSON.parse(JWT.decode(token, hmac_secret)[0]["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 = JSON.parse(decoded["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
- JSON.generate to_hash
189
+ MultiJson.dump to_hash
190
190
  end
191
191
 
192
192
  end
@@ -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
- # 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
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 [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)
95
- @log = Logging.logger[self.name]
96
- # iterate over all known validators and attempt to validate using them
97
- results_by_validator = {}
98
- results = self.descendants.map {|c|
99
- validator = c.new()
100
- @log.debug "attempting to validate using #{validator.name}"
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 "unable to validate using any validator"
114
- return false
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.descendants
140
- ObjectSpace.each_object(Class).select { |klass| klass < self }
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 Validators
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, raw_csr)
30
- @log.info "attempting to validate JWT token"
31
- return false unless Autosign::Token.validate(certname, token, settings['secret'])
32
- @log.info "validated JWT token"
33
- @log.debug "validated JWT token, checking reusability"
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
- return false
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({'journalfile' => settings['journalfile']})
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
- return true
58
+ true
59
59
  else
60
- @log.warn "journal cannot validate one-time token; may already have been used"
61
- return false
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["AUTOSIGN_TESTMODE"] == "true" and
79
- !ENV["AUTOSIGN_TEST_SECRET"].nil? and
80
- !ENV["AUTOSIGN_TEST_JOURNALFILE"].nil? )
81
- {
82
- 'secret' => ENV["AUTOSIGN_TEST_SECRET"].to_s,
83
- 'journalfile' => ENV["AUTOSIGN_TEST_JOURNALFILE"].to_s
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
- # 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"
99
- return true
100
- else
101
- @log.error "no secret setting found"
102
- return false
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