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.
@@ -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
@@ -1,6 +1,8 @@
1
- module Autosign
2
- module Validators
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(token, certname, raw_csr)
40
+ def perform_validation(_token, certname, raw_csr)
46
41
  results = []
47
- @log.debug "validating using multiplexed external executables"
48
- policy_executables.each {|executable|
49
- @log.debug "attempting to validate using #{executable.to_s}"
50
- results << IO.popen(executable + ' ' + certname.to_s, 'r+') {|obj| obj.puts raw_csr; obj.close_write; obj.read; obj.close; $?.to_i }
51
- @log.debug "exit code from #{executable.to_s}: #{results.last}"
52
- }
53
- bool_results = results.map {|val| val == 0}
54
- return validate_using_strategy(bool_results)
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
- return array.any?
69
+ array.any?
76
70
  when 'all'
77
71
  @log.debug "validating using 'all' strategy"
78
- return array.all?
72
+ array.all?
79
73
  else
80
- @log.error "unable to validate; unknown strategy"
81
- return false
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
- Array(settings['external_policy_executable'])
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 "validating settings: " + settings.to_s
99
- unless ['any', 'all'].include? settings['strategy']
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 "done validating settings"
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 Validators
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
- def name
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, certname, raw_csr)
22
- @log.debug "validating against simple password list"
23
- @log.debug "passwords: " + settings.to_s
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 "validation result: " + result.to_s
26
- return result
25
+ @log.debug 'validation result: ' + result.to_s
26
+ result
27
27
  end
28
28
 
29
29
  def validate_password(password)
30
- @log.debug "Checking if password list includes password"
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
- Array(settings['password'])
35
+ Array(settings['password'])
36
36
  end
37
37
 
38
- def validate_settings(settings)
39
- @log.debug "validating settings: " + settings.to_s
40
- true
41
- end
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