autosign 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d43769ea221d92c48e517138ec7a3c1ba16ed3a
4
- data.tar.gz: 0a8454928712134b07dfaa5aa16c4451042086ac
3
+ metadata.gz: df93f6a505859c956b527410ffd01c67f3aad762
4
+ data.tar.gz: f9a1c7f7e857e6f596e6a50b4f534b772a1e62af
5
5
  SHA512:
6
- metadata.gz: fd1b62abf9b19f6806a356a693c42eb91593ca9cad9bcc79240b7df3d01d1b72bc92c6b8cb14647f92cb92a59179831aa7a90e6d379274b536c977b65a844fd1
7
- data.tar.gz: 4d5660957738acf9edd0413efab0c1fcc029ecc23ba049d75c26807a7e68f2310d5f5e22054a3c1469ab6e8609ec796ce1956968f6445bfb6e28ff3d990eda74
6
+ metadata.gz: 86adf30615877ec93e1038b51bf59b73026d01e842c93cdcc7b834965f7de546b8cded69e23d10917a393b0633650899c0460d3b3b60fcd1bb9cf9f5e76e39ff
7
+ data.tar.gz: 0d2b232814a18fef940e3605d83148a45d724c22ca3f7e490dbf14550ef4d67fbf0ef6eb3281ef35e9ad9fce8ff429ca88bb9d546071208d54fef405bd72229b
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ language: ruby
3
+ before_install: rm Gemfile.lock || true
4
+ cache: bundler
5
+ sudo: false
6
+ rvm:
7
+ - 1.9.3
8
+ - jruby-19mode
9
+ - 2.0.0
10
+ - 2.1.5
11
+ - 2.2.2
12
+ script:
13
+ - bundle exec cucumber
data/Gemfile.lock CHANGED
@@ -5,14 +5,15 @@ PATH
5
5
  deep_merge
6
6
  gli (~> 2)
7
7
  iniparse (~> 1)
8
+ json
8
9
  jwt (~> 1)
9
10
  logging
10
11
  require_all
12
+ yard
11
13
 
12
14
  GEM
13
15
  remote: https://rubygems.org/
14
16
  specs:
15
- CFPropertyList (2.2.8)
16
17
  aruba (0.6.2)
17
18
  childprocess (>= 0.3.6)
18
19
  cucumber (>= 1.1.1)
@@ -31,17 +32,12 @@ GEM
31
32
  gherkin (~> 2.12.0)
32
33
  deep_merge (1.0.1)
33
34
  diff-lcs (1.2.5)
34
- facter (2.2.0)
35
- CFPropertyList (~> 2.2.6)
36
35
  ffi (1.9.9)
37
36
  gherkin (2.12.2)
38
37
  multi_json (~> 1.3)
39
38
  gli (2.13.1)
40
- hiera (1.3.4)
41
- json_pure
42
39
  iniparse (1.4.0)
43
40
  json (1.8.3)
44
- json_pure (1.8.1)
45
41
  jwt (1.5.1)
46
42
  little-plugger (1.1.3)
47
43
  logging (2.0.0)
@@ -49,10 +45,6 @@ GEM
49
45
  multi_json (~> 1.10)
50
46
  multi_json (1.11.1)
51
47
  multi_test (0.1.2)
52
- puppet (3.7.0)
53
- facter (> 1.6, < 3)
54
- hiera (~> 1.0)
55
- json_pure
56
48
  rake (10.4.2)
57
49
  rdoc (4.2.0)
58
50
  json (~> 1.4)
@@ -61,6 +53,7 @@ GEM
61
53
  diff-lcs (>= 1.2.0, < 2.0)
62
54
  rspec-support (~> 3.3.0)
63
55
  rspec-support (3.3.0)
56
+ yard (0.8.7.6)
64
57
 
65
58
  PLATFORMS
66
59
  ruby
@@ -68,6 +61,6 @@ PLATFORMS
68
61
  DEPENDENCIES
69
62
  aruba
70
63
  autosign!
71
- puppet
64
+ cucumber
72
65
  rake
73
66
  rdoc
data/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # autosign
2
+ [![Build Status](https://travis-ci.org/danieldreier/autosign.svg?branch=master)](https://travis-ci.org/danieldreier/autosign) [![Code Climate](https://codeclimate.com/github/danieldreier/autosign/badges/gpa.svg)](https://codeclimate.com/github/danieldreier/autosign) [![Dependency Status](https://gemnasium.com/danieldreier/autosign.svg)](https://gemnasium.com/danieldreier/autosign) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/danieldreier/autosign) [![Inline docs](http://inch-ci.org/github/danieldreier/autosign.png)](http://inch-ci.org/github/danieldreier/autosign)
3
+
4
+ Tooling to make puppet autosigning easy, secure, and extensible
5
+
6
+ ### Introduction
7
+
8
+ This tool provides a CLI for performing puppet policy-based autosigning using JWT tokens. Read more at https://danieldreier.github.io/autosign.
data/autosign.gemspec CHANGED
@@ -19,11 +19,14 @@ spec = Gem::Specification.new do |s|
19
19
  s.add_development_dependency('rake')
20
20
  s.add_development_dependency('rdoc')
21
21
  s.add_development_dependency('aruba')
22
+ s.add_development_dependency('cucumber')
22
23
  s.add_development_dependency('puppet')
23
24
  s.add_runtime_dependency('gli','~> 2')
24
25
  s.add_runtime_dependency('jwt','~> 1')
25
26
  s.add_runtime_dependency('iniparse','~> 1')
26
27
  s.add_runtime_dependency('logging')
28
+ s.add_runtime_dependency('json')
27
29
  s.add_runtime_dependency('deep_merge')
28
30
  s.add_runtime_dependency('require_all')
31
+ s.add_runtime_dependency('yard')
29
32
  end
@@ -16,14 +16,15 @@ certname = ARGV[0]
16
16
  @logger.debug "certname is " + certname
17
17
 
18
18
  @logger.debug "reading CSR from stdin"
19
- csr = Autosign::Decoder.decode_csr($stdin.read)
19
+ raw_csr = $stdin.read
20
+ csr = Autosign::Decoder.decode_csr(raw_csr)
20
21
  exit 1 unless csr.is_a?(Hash)
21
22
 
22
23
  @logger.debug "CSR: " + csr.to_s
23
24
  ### End Inputs
24
25
 
25
26
  ### validate token
26
- token_validation = Autosign::Validator.any_validator(csr[:challenge_password].to_s, certname.to_s)
27
+ token_validation = Autosign::Validator.any_validator(csr[:challenge_password].to_s, certname.to_s, raw_csr)
27
28
  ### end validation
28
29
 
29
30
  ### Exit with correct exit status
@@ -9,7 +9,9 @@ Feature: Validate autosign key
9
9
  | AUTOSIGN_TESTMODE | true |
10
10
  | AUTOSIGN_TEST_SECRET | secret |
11
11
  | AUTOSIGN_TEST_LOGLEVEL | info |
12
- When I run `autosign-validator i-7672fe81` interactively
12
+ | AUTOSIGN_TEST_JOURNALFILE | /tmp/autosign_journal |
13
+ When I run `rm -f /tmp/autosign_journal`
14
+ And I run `autosign-validator i-7672fe81` interactively
13
15
  And I pipe in the file "../../fixtures/i-7672fe81.pem"
14
16
  Then the output should contain "token validated successfully"
15
17
  Then the exit status should be 0
data/lib/autosign.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'rubygems'
2
+ require 'json'
1
3
  require 'autosign/version.rb'
2
4
  require 'autosign/token.rb'
3
5
  require 'autosign/config.rb'
@@ -1,45 +1,73 @@
1
+ require 'iniparse'
2
+ require 'rbconfig'
3
+ require 'securerandom'
4
+ require 'deep_merge'
5
+
1
6
  module Autosign
7
+ # Exceptions namespace for Autosign class
2
8
  module Exceptions
9
+ # Exception representing a general failure during validation
3
10
  class Validation < Exception
4
11
  end
12
+ # Exception representing a missing file during config validation
5
13
  class NotFound < Exception
6
14
  end
15
+ # Exception representing a permissions error during config validation
7
16
  class Permissions < Exception
8
17
  end
18
+ # Exception representing errors that Autosign does not know how to handle
9
19
  class Error < Exception
10
20
  end
11
21
  end
12
- require 'iniparse'
13
- require 'rbconfig'
14
- require 'securerandom'
15
- require 'deep_merge'
22
+
16
23
  class Config
17
- attr_accessor :location
18
- attr_accessor :config_file_paths
19
- def initialize(settings = {})
24
+ # Create a config instance to interact with configuration settings
25
+ # To specify a configuration file, settings_param should include something like:
26
+ # {'config_file' => '/usr/local/etc/autosign.conf'}
27
+ #
28
+ # If no defaults are provided, the class checks several common locations for config file path.
29
+ #
30
+ # @param settings_param [Hash] config settings that should override defaults and config file settings
31
+ # @return [Autosign::Config] instance of the Autosign::Config class
32
+ def initialize(settings_param = {})
20
33
  # set up logging
21
34
  @log = Logging.logger['Autosign::Config']
22
35
  @log.debug "initializing Autosign::Config"
23
36
 
24
37
  # validate parameter
25
- raise 'settings is not a hash' unless settings.is_a?(Hash)
38
+ raise 'settings is not a hash' unless settings_param.is_a?(Hash)
26
39
 
27
40
  # look in the following places for a config file
28
41
  @config_file_paths = ['/etc/autosign.conf', '/usr/local/etc/autosign.conf', File.join(Dir.home, '.autosign.conf')]
29
- @config_file_paths = [ settings['config_file'] ] unless settings['config_file'].nil?
42
+ @config_file_paths = [ settings_param['config_file'] ] unless settings_param['config_file'].nil?
30
43
 
31
- @settings = settings
44
+ @settings = settings_param
32
45
  @log.debug "Using merged settings hash: " + @settings.to_s
33
46
  end
34
47
 
48
+ # Return a merged settings hash of defaults, config file settings
49
+ # and passed in settings (such as from the CLI)
50
+ #
51
+ # @return [Hash] deep merged settings hash
35
52
  def settings
36
53
  @log.debug "merging settings"
37
54
  setting_sources = [default_settings, configfile, @settings]
38
- setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
55
+ merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
56
+ @log.debug "using merged settings: " + merged_settings.to_s
57
+ return merged_settings
39
58
  end
40
59
 
41
60
  private
42
61
 
62
+ # default settings hash for the whole autosign gem
63
+ # the format must map to something an ini file can represent, so the
64
+ # structure should have a top-level key named "general" and key-value pairs
65
+ # below it, with strings as keys and strings or integers as values.
66
+ #
67
+ # validator settings should not be here; put those in the validator's
68
+ # default settings method.
69
+ #
70
+ # @return [Hash] default configuration settings
43
71
  def default_settings
44
72
  { 'general' =>
45
73
  {
@@ -54,6 +82,10 @@ module Autosign
54
82
  }
55
83
  end
56
84
 
85
+ # Locate the configuration file, parse it from INI-format, and return
86
+ # the results as a hash. Returns an empty hash if no config file is found.
87
+ #
88
+ # @return [Hash] configuration settings loaded from INI file
57
89
  def configfile
58
90
  @log.debug "Finding config file"
59
91
  @config_file_paths.each { |file|
@@ -71,6 +103,11 @@ module Autosign
71
103
  return {}
72
104
  end
73
105
 
106
+ # Validate configuration file
107
+ # Raises an exception if the config file cannot be validated
108
+ #
109
+ # @param configfile [String] the absolute path of the config file to validate
110
+ # @return [String] the absolute path of the config file
74
111
  def validate_config_file(configfile = location)
75
112
  @log.debug "validating config file"
76
113
  unless File.file?(configfile)
@@ -87,6 +124,9 @@ module Autosign
87
124
  configfile
88
125
  end
89
126
 
127
+ # Generate a default configuration file
128
+ # As a convenience for the user, we can generate a default config file
129
+ # This class is currently too tightly coupled with the JWT token validator
90
130
  def self.generate_default()
91
131
  os_defaults = (
92
132
  case RbConfig::CONFIG['host_os']
@@ -1,7 +1,9 @@
1
- require 'logging'
2
-
3
1
  module Autosign
4
2
  class Decoder
3
+ # Extract common name and challenge_password OID from X509 SSL Certificate signing requests
4
+ #
5
+ # @param csr[String] X509 format CSR
6
+ # @return [Hash] hash containing :challenge_password and :common_name keys
5
7
  def self.decode_csr(csr)
6
8
  @log = Logging.logger['Autosign::Decoder']
7
9
  @log.debug "decoding CSR"
@@ -1,73 +1,45 @@
1
1
  require 'logging'
2
2
  require 'yaml/store'
3
3
 
4
- # Autosign::Journal checks for one-time keys that have been used before
5
4
 
6
5
  module Autosign
6
+ # Autosign::Journal tracks one-time keys to prevent key re-use.
7
+ # Keys are stored in the journal file by UUID.
8
+ # The journal uses ruby's yaml/store, which is a YAML version of the PStore
9
+ # data store. It is multi-process safe, and blocks until transactions in other
10
+ # processes are complete.
7
11
  class Journal
12
+ #@return [Hash] settings of the autosign journal instance, such as the location of the journal file
8
13
  attr_accessor :settings
14
+
15
+ # @param settings [Hash] config settings for the new journal instance
16
+ # @return [Autosign::Journal] instance of the Autosign::Journal class
9
17
  def initialize(settings = {})
10
18
  @log = Logging.logger['Autosign::Journal']
11
19
  @log.debug "initializing Autosign::Journal"
12
20
  @settings = settings
13
- fail unless self.setup
14
- end
15
-
16
- def setup
17
- journalfile = self.settings['journalfile']
18
- store = YAML::Store.new(journalfile, true)
19
- store.ultra_safe = true
20
- return store
21
- end
22
-
23
- # Check whether a UUID already exists in the journal
24
- #
25
- # ==== Attributes
26
- #
27
- # * +uuid+ - Unique journal entry identifier
28
- #
29
- # ==== Examples
30
- #
31
- # To exit if a token has already been used:
32
- #
33
- # journal = Autosign::Journal.new({journalfile = '/etc/autosign/journal')
34
- # exit 1 if journal.check('d2e601c8-93df-4459-be18-1877eaf00920')
35
- def check(uuid)
36
- fail unless validate_uuid(uuid)
37
- true
21
+ fail unless setup
38
22
  end
39
23
 
40
- def delete(uuid)
41
- fail unless validate_uuid(uuid)
42
- true
43
- end
44
24
 
45
25
 
46
- # Add a new token to the journal
47
- #
48
- # ==== Attributes
49
- #
50
- # * +uuid+ - Unique journal entry identifier
51
- # * +validto+ - Integer seconds unix timestamp that the token will be valid until
52
- # * +data+ - Arbitrary hash that will be serialized and stored in the journal for auditing purposes
26
+ # Add a new token to the journal. Only succeeds if the token is not in the journal already.
53
27
  #
54
- # ==== Examples
28
+ # @param uuid [String] RFC4122 v4 UUID functioning as unique journal entry identifier
29
+ # @param validto [Integer] POSIX timestamp in seconds since epoch that the token will be valid until
30
+ # @param data [Hash] Arbitrary hash that will be serialized and stored in the journal for auditing purposes
55
31
  #
56
- # To attempt adding a token to the journal:
57
- #
58
- # journal = Autosign::Journal.new({journalfile = '/etc/autosign/journal')
59
- # exit 1 if journal.add('d2e601c8-93df-4459-be18-1877eaf00920')
32
+ # @example attempt adding a token to the journal
33
+ # journal = Autosign::Journal.new({journalfile = '/etc/autosign/journal')
34
+ # fail unless journal.add('d2e601c8-93df-4459-be18-1877eaf00920')
60
35
  #
61
36
  # This will only succeed if the token has not previously been added
62
37
  # This is the primary way this class is expected to be used
63
38
  def add(uuid, validto, data = {})
64
39
  @log.debug "attempting to add UUID: '#{uuid.to_s}' which is valid to '#{Time.at(validto.to_i)}' with data #{data.to_s}"
65
40
  puts validate_uuid(uuid).to_s
66
- #fail unless validate_uuid(uuid)
67
- #fail unless validate_timestamp(validto)
68
- #fail unless validate_data(data)
69
41
 
70
- store = self.setup
42
+ store = setup
71
43
  # wrap the change in a transaction because multiple autosign instances
72
44
  # may try to run simultaneously. This will block until another process
73
45
  # releases the transaction lock.
@@ -86,6 +58,22 @@ module Autosign
86
58
  return !!result
87
59
  end
88
60
 
61
+ private
62
+
63
+ # Create a new journal file, or load an existing one if it already exists.
64
+ # @return [YAML::Store] instance of YAML::Store using the configured journal file.
65
+ def setup
66
+ @log.debug "using journalfile: " + self.settings['journalfile']
67
+ journalfile = self.settings['journalfile']
68
+ store = YAML::Store.new(journalfile, true)
69
+ store.ultra_safe = true
70
+ return store
71
+ end
72
+
73
+ # Verify that a string is a V4 UUID
74
+ #
75
+ # @param uuid [String] RFC4122 v4 UUID
76
+ # @return [Boolean] true if the uuid string is a valid UUID, false if not a valid UUID
89
77
  def validate_uuid(uuid)
90
78
  unless uuid.is_a?(String)
91
79
  @log.error "UUID is not a string"
@@ -96,6 +84,7 @@ module Autosign
96
84
  @log.error "UUID is not a valid V4 UUID"
97
85
  return false
98
86
  end
87
+ return true
99
88
  end
100
89
 
101
90
  def validate_data(data)
@@ -1,17 +1,36 @@
1
1
  module Autosign
2
2
  require 'jwt'
3
- require 'JSON'
3
+ require 'json'
4
4
  require 'securerandom'
5
5
 
6
+ # Class modeling JSON Web Tokens as credentials for certificate auto signing.
7
+ # See http://jwt.io for more information about JSON web tokens in general.
8
+ #
9
+ # @return [Autosign::Token] instance of the Autosign::Token class
6
10
  class Token
11
+ # @return [Integer, String] seconds that the token will be valid for after being issued
7
12
  attr_reader :validfor
13
+ # @return [String] common name or regex of common names for which this token is valid
8
14
  attr_reader :certname
15
+ # @return [True, False] true if the token can be used multiple times, false if the token is intended as a one-time credential
9
16
  attr_reader :reusable
17
+ # @return [String] arbitrary string identifying the person or machine that generated the token initially
10
18
  attr_reader :requester
19
+ # @return [String] shared HMAC secret used to sign or validate tokens
11
20
  attr_reader :secret
21
+ # @return [Integer, String] POSIX seconds since epoch that the token is valid until
12
22
  attr_accessor :validto
23
+ # @return [String] RFC4122 v4 UUID functioning as unique identifier for the token
13
24
  attr_accessor :uuid
14
25
 
26
+ # Create a token instance to model an individual JWT token
27
+ #
28
+ # @param certname [String] common name or regex of common names for which this token is valid
29
+ # @param reusable [True, False] true if the token can be used multiple times, false if the token is intended as a one-time credential
30
+ # @param validfor [Integer, String] seconds that the token will be valid for, starting at the current time
31
+ # @param requester [String] arbitrary string identifying the person or machine that generated the token initially
32
+ # @param secret [String] shared HMAC secret used to sign or validate tokens
33
+ # @return [Autosign::Config] instance of the Autosign::Config class
15
34
  def initialize(certname, reusable=false, validfor=7200, requester, secret)
16
35
  # set up logging
17
36
  @log = Logging.logger['Autosign::Token']
@@ -26,6 +45,16 @@ module Autosign
26
45
  @validto = Time.now.to_i + self.validfor
27
46
  end
28
47
 
48
+ # Validate an existing JSON Web Token.
49
+ #
50
+ # 1. Use the HMAC secret to validate the data in the token
51
+ # 2. Compare the expiration time in the token to the current time to determine if it's valid
52
+ # 3. compare the certname or regex of certnames in the token to the requested common name from the certificate signing request
53
+ #
54
+ # @param requested_certname [String] common name coming from the certificate signing request. This is the common name the requester wants.
55
+ # @param token [String] JSON Web Token coming from the certificate signing request
56
+ # @param hmac_secret [String] Password that the token was (hopefully) originally signed with.
57
+ # @return [True, False] returns true if the token can be validated, or false if the token cannot be validated.
29
58
  def self.validate(requested_certname, token, hmac_secret)
30
59
  @log = Logging.logger['Autosign::Token.validate']
31
60
  @log.debug "attempting to validate token"
@@ -69,15 +98,29 @@ module Autosign
69
98
  raise Autosign::Token::ValidationError
70
99
  end
71
100
 
72
- def self.token_validto(token, hmac_secret)
73
- begin
74
- decoded = JWT.decode(token, hmac_secret)[0]
75
- rescue JWT::ExpiredSignature
76
- raise Autosign::Token::ExpiredToken
77
- rescue
78
- raise Autosign::Token::Invalid
79
- end
80
- return decoded['exp'].to_i
101
+ # check if the token is reusable or a one-time use token
102
+ # @return [True, False] return true if the token can be used multiple times, false if the token can only be used once
103
+ def reusable
104
+ !!@reusable
105
+ end
106
+
107
+ # convert the token to a hash
108
+ # @return [Hash{String => String}]
109
+ def to_hash
110
+ {
111
+ "certname" => certname,
112
+ "requester" => requester,
113
+ "reusable" => reusable,
114
+ "validfor" => validfor,
115
+ "uuid" => uuid
116
+ }
117
+ end
118
+
119
+ # Sign the token with HMAC using a SHA-512 hash
120
+ # @return [String] signed, serialized JSON web token
121
+ def sign()
122
+ exp_payload = { :data => to_json, :exp => validto.to_s}
123
+ JWT.encode exp_payload, secret, 'HS512'
81
124
  end
82
125
 
83
126
  def self.from_token(token, hmac_secret)
@@ -100,6 +143,23 @@ module Autosign
100
143
  return new_token
101
144
  end
102
145
 
146
+ # Extract the expiration time, in seconds since epoch, from a signed token. Uses HMAC secret to validate the expiration time.
147
+ # @param token [String] Serialized JSON web token
148
+ # @param hmac_secret [String] Password that the token was (hopefully) originally signed with.
149
+ # @return [Integer] POSIX time (seconds since epoch) that the token is valid until
150
+ def self.token_validto(token, hmac_secret)
151
+ begin
152
+ decoded = JWT.decode(token, hmac_secret)[0]
153
+ rescue JWT::ExpiredSignature
154
+ raise Autosign::Token::ExpiredToken
155
+ rescue
156
+ raise Autosign::Token::Invalid
157
+ end
158
+ return decoded['exp'].to_i
159
+ end
160
+
161
+ private
162
+
103
163
  def certname=(str)
104
164
  @name = str
105
165
  end
@@ -108,10 +168,6 @@ module Autosign
108
168
  @reusable = !!bool
109
169
  end
110
170
 
111
- def reusable
112
- !!@reusable
113
- end
114
-
115
171
  def requester=(str)
116
172
  @requester = str
117
173
  end
@@ -120,23 +176,8 @@ module Autosign
120
176
  @secret = str
121
177
  end
122
178
 
123
- def to_hash
124
- {
125
- "certname" => self.certname,
126
- "requester" => self.requester,
127
- "reusable" => self.reusable,
128
- "validfor" => self.validfor,
129
- "uuid" => self.uuid
130
- }
131
- end
132
-
133
179
  def to_json
134
- JSON.generate self.to_hash
135
- end
136
-
137
- def sign()
138
- exp_payload = { :data => self.to_json, :exp => self.validto.to_s}
139
- JWT.encode exp_payload, self.secret, 'HS512'
180
+ JSON.generate to_hash
140
181
  end
141
182
 
142
183
  end
@@ -2,13 +2,44 @@ require 'logging'
2
2
  require 'require_all'
3
3
 
4
4
  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
5
17
  class Validator
6
18
  def initialize()
7
- @log = Logging.logger[self.name]
19
+ start_logging()
8
20
  settings() # just run to validate settings
9
21
  setup()
22
+ # call name to ensure that the class fails immediately if child classes
23
+ # do not implement it.
24
+ name()
10
25
  end
11
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.
12
43
  def name
13
44
  # override this after inheriting
14
45
  # should return a string with no spaces
@@ -16,12 +47,30 @@ module Autosign
16
47
  raise NotImplementedError
17
48
  end
18
49
 
19
- def validate(challenge_password, certname)
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)
20
69
  @log.debug "running validate"
21
70
  fail unless challenge_password.is_a?(String)
22
71
  fail unless certname.is_a?(String)
23
72
 
24
- case perform_validation(challenge_password, certname)
73
+ case perform_validation(challenge_password, certname, raw_csr)
25
74
  when true
26
75
  @log.debug "validated successfully"
27
76
  @log.info "Validated '#{certname}' using '#{name}' validator"
@@ -36,18 +85,27 @@ module Autosign
36
85
  end
37
86
  end
38
87
 
39
- def self.any_validator(challenge_password, certname)
88
+ # Class method to attempt validation of a request against all validators which inherit from this class.
89
+ # The request is considered to be validated if any one validator succeeds.
90
+ # @param challenge_password [String] the challenge_password OID from the certificate signing request
91
+ # @param certname [String] the common name being requested in the certificate signing request
92
+ # @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)
40
95
  @log = Logging.logger[self.name]
41
96
  # iterate over all known validators and attempt to validate using them
97
+ results_by_validator = {}
42
98
  results = self.descendants.map {|c|
43
99
  validator = c.new()
44
100
  @log.debug "attempting to validate using #{validator.name}"
45
- result = validator.validate(challenge_password, certname)
101
+ result = validator.validate(challenge_password, certname, raw_csr)
102
+ results_by_validator[validator.name] = result
46
103
  @log.debug "result: #{result.to_s}"
47
104
  result
48
105
  }
49
106
  @log.debug "validator results: " + results.to_s
50
- success = results.inject(true) { |sum, n| n and sum }
107
+ @log.info "results by validator: " + results_by_validator.to_s
108
+ success = results.any?{|result| result == true}
51
109
  if success
52
110
  @log.info "successfully validated using one or more validators"
53
111
  return true
@@ -58,14 +116,18 @@ module Autosign
58
116
  end
59
117
 
60
118
  private
61
- def perform_validation(challenge_password, certname)
62
- # override this after inheriting
63
- # should return true to indicate success validating
64
- # or false to indicate that the validator was unable to validate
65
- raise NotImplementedError
119
+
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
66
125
  end
67
126
 
68
- # perform any steps you want to take during initialization
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
69
131
  def setup
70
132
  true
71
133
  end
@@ -74,9 +136,19 @@ module Autosign
74
136
  ObjectSpace.each_object(Class).select { |klass| klass < self }
75
137
  end
76
138
 
139
+ # provide a merged settings hash of default settings for a validator,
140
+ # config file settings for the validator, and override settings defined in
141
+ # the validator.
142
+ #
143
+ # Do not override this in child classes. If you need to set
144
+ # custom config settings, override the get_override_settings method.
145
+ # The section of the config file this reads from is the same as the name
146
+ # method returns.
147
+ #
148
+ # @return [Hash] of config settings
77
149
  def settings
78
150
  @log.debug "merging settings"
79
- setting_sources = [default_settings, load_config, get_override_settings]
151
+ setting_sources = [get_override_settings, load_config, default_settings]
80
152
  merged_settings = setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
81
153
  @log.debug "using merged settings: " + merged_settings.to_s
82
154
  @log.debug "validating merged settings"
@@ -90,28 +162,37 @@ module Autosign
90
162
  end
91
163
  end
92
164
 
93
- # this hash will be merged with the configuration section's hash
94
- # override this when inheriting if you need to set config defaults
165
+ # (optionally) override this from a child class to set config defaults.
166
+ # These will be overridden by config file settings.
167
+ #
168
+ # Override this when inheriting if you need to set config defaults.
169
+ # For example, if you want to pull settings from zookeeper, this would
170
+ # be a good place to do that.
171
+ #
172
+ # @return [Hash] of config settings
95
173
  def default_settings
96
174
  {}
97
175
  end
98
176
 
99
177
 
100
- # override this to perform validation checks on the merged config hash
101
- # of default settings, config file settings, and override settings.
102
- # return either true or false
178
+ # (optionally) override this to perform validation checks on the merged
179
+ # config hash of default settings, config file settings, and override
180
+ # settings.
181
+ # @return [True, False]
103
182
  def validate_settings(settings)
104
183
  settings.is_a?(Hash)
105
184
  end
106
185
 
107
- # load any required configuration
186
+ # load any required configuration from the config file.
187
+ # Do not override this in child classes.
188
+ # @return [Hash] configuration settings from the validator's section of the config file
108
189
  def load_config
109
190
  @log.debug "loading validator-specific configuration"
110
191
  config = Autosign::Config.new
111
192
 
112
193
  if config.settings.to_hash[self.name].nil?
113
- @log.warning "Unable to load validator-specific configuration"
114
- @log.warning "Cannot load configuration section named '#{self.name}'"
194
+ @log.warn "Unable to load validator-specific configuration"
195
+ @log.warn "Cannot load configuration section named '#{self.name}'"
115
196
  return {}
116
197
  else
117
198
  @log.debug "Set validator-specific settings from config file: " + config.settings.to_hash[self.name].to_s
@@ -119,8 +200,11 @@ module Autosign
119
200
  end
120
201
  end
121
202
 
122
- # this hook gets run to get settings from the CLI etc
123
- # this is how you override defaults and config file settings
203
+ # (optionally) override this from child classes to get custom configuration
204
+ # from a validator.
205
+ #
206
+ # This is how you override defaults and config file settings.
207
+ # @return [Hash] configuration settings
124
208
  def get_override_settings
125
209
  {}
126
210
  end
@@ -7,7 +7,7 @@ module Autosign
7
7
 
8
8
  private
9
9
 
10
- def perform_validation(token, certname)
10
+ def perform_validation(token, certname, raw_csr)
11
11
  puts "attempting to validate JWT token"
12
12
  return false unless Autosign::Token.validate(certname, token, settings['secret'])
13
13
  puts "validated JWT token"
@@ -24,6 +24,7 @@ module Autosign
24
24
 
25
25
  def add_to_journal(token)
26
26
  validated_token = Autosign::Token.from_token(token, settings['secret'])
27
+ @log.debug 'add_to_journal settings: ' + settings.to_s
27
28
  journal = Autosign::Journal.new({'journalfile' => settings['journalfile']})
28
29
  token_expiration = Autosign::Token.token_validto(token, settings['secret'])
29
30
 
@@ -44,7 +45,17 @@ module Autosign
44
45
  end
45
46
 
46
47
  def get_override_settings
47
- {}
48
+ # this is a hack to make testing easier
49
+ if (ENV["AUTOSIGN_TESTMODE"] == "true" and
50
+ !ENV["AUTOSIGN_TEST_SECRET"].nil? and
51
+ !ENV["AUTOSIGN_TEST_JOURNALFILE"].nil? )
52
+ {
53
+ 'secret' => ENV["AUTOSIGN_TEST_SECRET"].to_s,
54
+ 'journalfile' => ENV["AUTOSIGN_TEST_JOURNALFILE"].to_s
55
+ }
56
+ else
57
+ {}
58
+ end
48
59
  end
49
60
 
50
61
  def validate_settings(settings)
@@ -0,0 +1,65 @@
1
+ module Autosign
2
+ module Validators
3
+ class Multiplexer < Autosign::Validator
4
+ def name
5
+ "multiplexer"
6
+ end
7
+
8
+ private
9
+
10
+ def perform_validation(token, certname, raw_csr)
11
+ results = []
12
+ @log.debug "validating using multiplexed external executables"
13
+ policy_executables.each {|executable|
14
+ @log.debug "attempting to validate using #{executable.to_s}"
15
+ results << IO.popen(executable, 'r+') {|obj| obj.puts raw_csr; obj.close_write; obj.read; obj.close; $?.to_i }
16
+ @log.debug "exit code from #{executable.to_s}: #{results.last}"
17
+ }
18
+ bool_results = results.map {|val| val == 0}
19
+ return validate_using_strategy(bool_results)
20
+ end
21
+
22
+
23
+ def default_settings
24
+ {
25
+ 'strategy' => 'any',
26
+ }
27
+ end
28
+
29
+ def validate_using_strategy(array)
30
+ case settings['strategy']
31
+ when 'any'
32
+ @log.debug "validating using 'any' strategy"
33
+ return array.any?
34
+ when 'all'
35
+ @log.debug "validating using 'all' strategy"
36
+ return array.all?
37
+ else
38
+ @log.error "unable to validate; unknown strategy"
39
+ return false
40
+ end
41
+ end
42
+
43
+ def policy_executables
44
+ return [] if settings['external_policy_executable'].nil?
45
+ exec_list = settings['external_policy_executable']
46
+ return [exec_list] if exec_list.is_a?(String)
47
+ return exec_list if exec_list.is_a?(Array)
48
+ return []
49
+ end
50
+
51
+
52
+ def validate_settings(settings)
53
+ @log.debug "validating settings: " + settings.to_s
54
+ unless ['any', 'all'].include? settings['strategy']
55
+ @log.error "strategy setting must be set to 'any' or 'all'"
56
+ return false
57
+ end
58
+
59
+ @log.debug "done validating settings"
60
+ true
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -1,3 +1,3 @@
1
1
  module Autosign
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: autosign
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Your Name Here
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-13 00:00:00.000000000 Z
11
+ date: 2015-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: cucumber
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: puppet
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: json
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: deep_merge
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +178,20 @@ dependencies:
150
178
  - - ">="
151
179
  - !ruby/object:Gem::Version
152
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: yard
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
153
195
  description:
154
196
  email: your@email.address.com
155
197
  executables:
@@ -159,8 +201,10 @@ extra_rdoc_files:
159
201
  - README.rdoc
160
202
  - autosign.rdoc
161
203
  files:
204
+ - ".travis.yml"
162
205
  - Gemfile
163
206
  - Gemfile.lock
207
+ - README.md
164
208
  - README.rdoc
165
209
  - Rakefile
166
210
  - autosign.gemspec
@@ -180,6 +224,7 @@ files:
180
224
  - lib/autosign/token.rb
181
225
  - lib/autosign/validator.rb
182
226
  - lib/autosign/validators/jwt.rb
227
+ - lib/autosign/validators/multiplexer.rb
183
228
  - lib/autosign/version.rb
184
229
  homepage: http://your.website.com
185
230
  licenses: []