autosign 0.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 +7 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +73 -0
- data/README.rdoc +6 -0
- data/Rakefile +44 -0
- data/autosign.gemspec +29 -0
- data/autosign.rdoc +5 -0
- data/bin/autosign +169 -0
- data/bin/autosign-validator +41 -0
- data/features/autosign.feature +78 -0
- data/features/step_definitions/autosign_steps.rb +44 -0
- data/features/support/env.rb +17 -0
- data/features/validate.feature +20 -0
- data/fixtures/i-7672fe81.pem +34 -0
- data/lib/autosign.rb +9 -0
- data/lib/autosign/config.rb +131 -0
- data/lib/autosign/decoder.rb +32 -0
- data/lib/autosign/journal.rb +123 -0
- data/lib/autosign/logger.rb +7 -0
- data/lib/autosign/token.rb +143 -0
- data/lib/autosign/validator.rb +133 -0
- data/lib/autosign/validators/jwt.rb +63 -0
- data/lib/autosign/version.rb +3 -0
- metadata +213 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
When(/^I get help for "([^"]*)"$/) do |app_name|
|
2
|
+
@app_name = app_name
|
3
|
+
step %(I run `#{app_name} help`)
|
4
|
+
end
|
5
|
+
|
6
|
+
Given(/^a pre\-shared key of "([^"]*)"$/) do |presharedkey|
|
7
|
+
@psk = presharedkey
|
8
|
+
end
|
9
|
+
|
10
|
+
Given(/^a hostname of "([^"]*)"$/) do |host|
|
11
|
+
@hostname = host
|
12
|
+
end
|
13
|
+
|
14
|
+
Given(/^the current time is (\d+)$/) do |time|
|
15
|
+
@current_time = time
|
16
|
+
end
|
17
|
+
|
18
|
+
Given(/^a static token file containing:$/) do |multiline|
|
19
|
+
@static_token_file = multiline
|
20
|
+
end
|
21
|
+
|
22
|
+
Given(/^a mocked "\/(\S*)" directory$/)do |directory|
|
23
|
+
dir_name = File.join(File.expand_path(current_dir), "etc")
|
24
|
+
FileUtils.mkdir_p dir_name
|
25
|
+
set_env 'ETCROOT', dir_name
|
26
|
+
# create_dir("etc")
|
27
|
+
end
|
28
|
+
|
29
|
+
Then(/^a "\/(\S*)" (?:file|directory) should exist$/) do |file|
|
30
|
+
#expect(File.exist?(File.join(File.expand_path(current_dir), file))).to be true
|
31
|
+
fullpath = File.join(File.expand_path(current_dir), file)
|
32
|
+
FileUtils.mkdir_p fullpath
|
33
|
+
$world.puts "path: " + fullpath
|
34
|
+
expect(File.exist?(file)).to be true
|
35
|
+
end
|
36
|
+
|
37
|
+
#When(/^I pipe in the file "(.*?)"$/) do |file|
|
38
|
+
# in_current_dir do
|
39
|
+
# File.open(file, 'r').each_line do |line|
|
40
|
+
# _write_interactive(line)
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# @interactive.stdin.close()
|
44
|
+
#end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
|
3
|
+
|
4
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
5
|
+
LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
|
6
|
+
|
7
|
+
Before do
|
8
|
+
# Using "announce" causes massive warnings on 1.9.2
|
9
|
+
@puts = true
|
10
|
+
@original_rubylib = ENV['RUBYLIB']
|
11
|
+
ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
|
12
|
+
$world = self
|
13
|
+
end
|
14
|
+
|
15
|
+
After do
|
16
|
+
ENV['RUBYLIB'] = @original_rubylib
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Validate autosign key
|
2
|
+
In order to sign puppet certificates automatically
|
3
|
+
I want to validate autosign keys programatically
|
4
|
+
So that I only grant access to allowed systems without needing manual authorization
|
5
|
+
|
6
|
+
Scenario: Validate a certificate signing request
|
7
|
+
Given I set the environment variables to:
|
8
|
+
| variable | value |
|
9
|
+
| AUTOSIGN_TESTMODE | true |
|
10
|
+
| AUTOSIGN_TEST_SECRET | secret |
|
11
|
+
| AUTOSIGN_TEST_LOGLEVEL | info |
|
12
|
+
When I run `autosign-validator i-7672fe81` interactively
|
13
|
+
And I pipe in the file "../../fixtures/i-7672fe81.pem"
|
14
|
+
Then the output should contain "token validated successfully"
|
15
|
+
Then the exit status should be 0
|
16
|
+
|
17
|
+
Scenario: Do not validate a certificate signing request whose certname does not match the certificate
|
18
|
+
When I run `autosign-validator wrong-certname.example.com` interactively
|
19
|
+
And I pipe in the file "../../fixtures/i-7672fe81.pem"
|
20
|
+
Then the exit status should be 1
|
@@ -0,0 +1,34 @@
|
|
1
|
+
-----BEGIN CERTIFICATE REQUEST-----
|
2
|
+
MIIF8jCCA9oCAQAwFTETMBEGA1UEAwwKaS03NjcyZmU4MTCCAiIwDQYJKoZIhvcN
|
3
|
+
AQEBBQADggIPADCCAgoCggIBAKy9i/q94RycncCxUgoGexiHY/El/q5mV+fB6DlB
|
4
|
+
60tPpAxUUDpdEFmSS63/uKc5bGIE7R+YD64etHHY6WeX/JK1OIE17YCIMT+J82tC
|
5
|
+
MCTiXHgD52uViuYC0a3Yr7PrrAYALrVzSds3hbfsQNmC/9BfwLD1FB5g8PVPb82J
|
6
|
+
paahRAh4dBzjgbNVrgI0s54dlDca9LvO+RDORqJVBM27jy1rScdg4rxcy30Fw9DI
|
7
|
+
1NhrhfXrNTnUb6T7GOpKZjJO0mbhItZRQr28S1Rx3Lyqk43HkIJQ66/7tUhrBXmG
|
8
|
+
787hHhqYbh0dvxR8t61Xu7ITIX3U164kEE70nVNxelTFuedYUA7os85WDriCoD9n
|
9
|
+
O2eNatMdWPpw7kcvoPZzIc2kfu4UQxP9UTYLKz2sNnjan2ZSoTdQ6K4tc0ee5hVv
|
10
|
+
AF3klg+M9LpoLEnBu51hhxhfEiaY/cx2EdQvsJOJH9U5Q11jvnn6McxDfoLHeGeW
|
11
|
+
7Cp6qjmVysirE/tQU9Ds2v+NEkmOF1JLMe95lxISvwz4tuoRlgGRLD6vJhyDyUtg
|
12
|
+
Zg505eiWxjWJ6h4591Ll0yjVgYuOf4I5aePXsOsh9zS4cchzbktWdAoBgv/p6sVH
|
13
|
+
67Gfd1EzdkByT8RGBOCV9T4jX4NwBqFL7HczHO0GnUqvZ0034THnqCyPSxTqOy6n
|
14
|
+
QogDAgMBAAGgggGWMIIBkgYJKoZIhvcNAQkHMYIBgxOCAX9leUowZVhBaU9pSktW
|
15
|
+
MVFpTENKaGJHY2lPaUpJVXpVeE1pSjkuZXlKa1lYUmhJam9pZTF3aVkyVnlkRzVo
|
16
|
+
YldWY0lqcGNJbWt0TnpZM01tWmxPREZjSWl4Y0luSmxjWFZsYzNSbGNsd2lPbHdp
|
17
|
+
UkdGdWFXVnNjeTFOWVdOQ2IyOXJMVkJ5YnkweUxteHZZMkZzWENJc1hDSnlaWFZ6
|
18
|
+
WVdKc1pWd2lPbVpoYkhObExGd2lkbUZzYVdSbWIzSmNJam81T1RrNU9Ua3NYQ0ox
|
19
|
+
ZFdsa1hDSTZYQ0kwWVRNMlpqQTBOUzFqTm1ObExUUmlaall0WW1Fell5MDJaak5s
|
20
|
+
TnpobE5tSTNNV05jSW4waUxDSmxlSEFpT2lJeE5ETTNORGN3TVRrMkluMC5PWlFk
|
21
|
+
ZW5Wekl4eS1JczI3MVRLMHFxaEttUmZxa0IyTGhzY3N6LWtJSzRIUWFlbTNBd3g3
|
22
|
+
elZraUNwajBfZUZja2dhS1lOQk1BZGhVZklNcVMzSU1tdzANBgkqhkiG9w0BAQsF
|
23
|
+
AAOCAgEADY7/yRoEeXziUo8bwIp4E+0FPfpP9+uVbhHJE+5TIgBBR1B4qzN0/vRG
|
24
|
+
Rv6Vo9dwjMadxxWmXQbKjzkC+t2DuIY2u9bgoVr54IMzllwBvfhvDzj9qO6gRlko
|
25
|
+
cNyWzoz/Bc9/KmO7jCttI7QjkJWPE8JvlId5TEzgNPmo7L1QXQXNSlp1tc3EDwdp
|
26
|
+
xWpfx7NXzZJUpgCueywYwjWB/ckK4Mu5wvXuWyHw2J/4hMbxhJzl++DKkHxxtDIa
|
27
|
+
UKfTlEp/9InQUUo6HluN4GqMguu9wcvcRQIKf8SiWiQmH3LnMaCl+yWQ8rYS7w7O
|
28
|
+
oIIlw9IrQMrKwt14hEDDORdSBiFcRHkmdegW0Z+OWTNVkeSA3IS0YRTNnItnDg24
|
29
|
+
GJcx8ooaj9upbfixnnhBWZwfQwf1ngWN192hXlORkTSeav9/1nSTJujy6X5wp6b/
|
30
|
+
3WtYGy6c9Gknx26dQB6JLTxJyGUQbgoNMG3toCaISLJUamDcT03eTx6yvpm2eC4l
|
31
|
+
uwRPdegUj0sMNQivJ34elXeJtCT70RcQGU3dhspXI1AsYVmNNGqj20Kkzd971WAq
|
32
|
+
lxkG9yhW9TapxTe1G1BPETYXtLK8a0Y4evdnFVE1w9MRWfmWO2UMMC2bOVlV83pN
|
33
|
+
x8ykrmy6WP7cubIIH7/PUD8jI2ekBEjvIDdft2dZeYEWZIs5DcI=
|
34
|
+
-----END CERTIFICATE REQUEST-----
|
data/lib/autosign.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'autosign/version.rb'
|
2
|
+
require 'autosign/token.rb'
|
3
|
+
require 'autosign/config.rb'
|
4
|
+
require 'autosign/decoder.rb'
|
5
|
+
require 'autosign/journal.rb'
|
6
|
+
require 'autosign/validator.rb'
|
7
|
+
|
8
|
+
# Add requires for other files you add to your project here, so
|
9
|
+
# you just need to require this one file in your bin file
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Autosign
|
2
|
+
module Exceptions
|
3
|
+
class Validation < Exception
|
4
|
+
end
|
5
|
+
class NotFound < Exception
|
6
|
+
end
|
7
|
+
class Permissions < Exception
|
8
|
+
end
|
9
|
+
class Error < Exception
|
10
|
+
end
|
11
|
+
end
|
12
|
+
require 'iniparse'
|
13
|
+
require 'rbconfig'
|
14
|
+
require 'securerandom'
|
15
|
+
require 'deep_merge'
|
16
|
+
class Config
|
17
|
+
attr_accessor :location
|
18
|
+
attr_accessor :config_file_paths
|
19
|
+
def initialize(settings = {})
|
20
|
+
# set up logging
|
21
|
+
@log = Logging.logger['Autosign::Config']
|
22
|
+
@log.debug "initializing Autosign::Config"
|
23
|
+
|
24
|
+
# validate parameter
|
25
|
+
raise 'settings is not a hash' unless settings.is_a?(Hash)
|
26
|
+
|
27
|
+
# look in the following places for a config file
|
28
|
+
@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?
|
30
|
+
|
31
|
+
@settings = settings
|
32
|
+
@log.debug "Using merged settings hash: " + @settings.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def settings
|
36
|
+
@log.debug "merging settings"
|
37
|
+
setting_sources = [default_settings, configfile, @settings]
|
38
|
+
setting_sources.inject({}) { |merged, hash| merged.deep_merge(hash) }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def default_settings
|
44
|
+
{ 'general' =>
|
45
|
+
{
|
46
|
+
'loglevel' => 'INFO',
|
47
|
+
'token_validity' => 7200,
|
48
|
+
'logfile' => '/var/log/autosign.log',
|
49
|
+
'journalfile' => '/var/log/autosign.journal',
|
50
|
+
},
|
51
|
+
'jwt_token' => {
|
52
|
+
'validity' => 7200
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def configfile
|
58
|
+
@log.debug "Finding config file"
|
59
|
+
@config_file_paths.each { |file|
|
60
|
+
@log.debug "Checking if file '#{file}' exists"
|
61
|
+
if File.file?(file)
|
62
|
+
@log.debug "Reading config file from: " + file
|
63
|
+
config_file = File.read(file)
|
64
|
+
parsed_config_file = IniParse.parse(config_file).to_hash
|
65
|
+
@log.debug "configuration read from config file: " + parsed_config_file.to_s
|
66
|
+
return parsed_config_file if parsed_config_file.is_a?(Hash)
|
67
|
+
else
|
68
|
+
@log.debug "Configuration file '#{file}' not found"
|
69
|
+
end
|
70
|
+
}
|
71
|
+
return {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_config_file(configfile = location)
|
75
|
+
@log.debug "validating config file"
|
76
|
+
unless File.file?(configfile)
|
77
|
+
@log.error "configuration file not found at: #{configfile}"
|
78
|
+
raise Autosign::Exceptions::NotFound
|
79
|
+
end
|
80
|
+
|
81
|
+
# check if file is world-readable
|
82
|
+
if File.world_readable?(configfile) or File.world_writable?(configfile)
|
83
|
+
@log.error "configuration file #{configfile} is world-readable or world-writable, which is a security risk"
|
84
|
+
raise Autosign::Exceptions::Permissions
|
85
|
+
end
|
86
|
+
|
87
|
+
configfile
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.generate_default()
|
91
|
+
os_defaults = (
|
92
|
+
case RbConfig::CONFIG['host_os']
|
93
|
+
when /darwin|mac os/
|
94
|
+
{
|
95
|
+
'logpath' => File.join(Dir.home, 'autosign.log'),
|
96
|
+
'confpath' => File.join(Dir.home, '.autosign.conf'),
|
97
|
+
'journalfile' => File.join(Dir.home, '.autosign.journal')
|
98
|
+
}
|
99
|
+
when /linux/
|
100
|
+
{
|
101
|
+
'logpath' => '/var/log/autosign.log',
|
102
|
+
'confpath' => '/etc/autosign.conf',
|
103
|
+
'journalfile' => File.join(Dir.home, '/var/log/autosign.journal')
|
104
|
+
}
|
105
|
+
when /bsd/
|
106
|
+
{
|
107
|
+
'logpath' => '/var/log/autosign.log',
|
108
|
+
'confpath' => '/usr/local/etc/autosign.conf',
|
109
|
+
'journalfile' => File.join(Dir.home, '/var/log/autosign.journal')
|
110
|
+
}
|
111
|
+
else
|
112
|
+
raise Autosign::Exceptions::Error, "unsupported os: #{host_os.inspect}"
|
113
|
+
end
|
114
|
+
)
|
115
|
+
|
116
|
+
config = IniParse.gen do |doc|
|
117
|
+
doc.section("general") do |general|
|
118
|
+
general.option("loglevel", "warn")
|
119
|
+
general.option("logfile", os_defaults['logpath'])
|
120
|
+
general.option("journalfile", os_defaults['journalfile'])
|
121
|
+
end
|
122
|
+
doc.section("jwt_token") do |jwt_token|
|
123
|
+
jwt_token.option("secret", SecureRandom.base64(15))
|
124
|
+
jwt_token.option("validity", 7200)
|
125
|
+
end
|
126
|
+
end.to_ini
|
127
|
+
raise Autosign::Exceptions::Error, "file #{os_defaults['confpath']} already exists, aborting" if File.file?(os_defaults['confpath'])
|
128
|
+
File.write(os_defaults['confpath'], config)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
module Autosign
|
4
|
+
class Decoder
|
5
|
+
def self.decode_csr(csr)
|
6
|
+
@log = Logging.logger['Autosign::Decoder']
|
7
|
+
@log.debug "decoding CSR"
|
8
|
+
|
9
|
+
begin
|
10
|
+
csr = OpenSSL::X509::Request.new(csr)
|
11
|
+
rescue OpenSSL::X509::RequestError
|
12
|
+
@log.error "Rescued OpenSSL::X509::RequestError; unable to decode CSR"
|
13
|
+
return nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# extract challenge password
|
17
|
+
challenge_password = csr.attributes.select { |a| a.oid == 'challengePassword' }.first.value.value.first.value
|
18
|
+
|
19
|
+
# extract common name
|
20
|
+
common_name = /^\/CN=(\S*)$/.match(csr.subject.to_s)[1]
|
21
|
+
|
22
|
+
output = {
|
23
|
+
:challenge_password => challenge_password,
|
24
|
+
:common_name => common_name
|
25
|
+
}
|
26
|
+
|
27
|
+
@log.info "Decoded CSR for CN: " + output[:common_name].to_s
|
28
|
+
@log.debug "Decoded CSR as: " + output.to_s
|
29
|
+
return output
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'logging'
|
2
|
+
require 'yaml/store'
|
3
|
+
|
4
|
+
# Autosign::Journal checks for one-time keys that have been used before
|
5
|
+
|
6
|
+
module Autosign
|
7
|
+
class Journal
|
8
|
+
attr_accessor :settings
|
9
|
+
def initialize(settings = {})
|
10
|
+
@log = Logging.logger['Autosign::Journal']
|
11
|
+
@log.debug "initializing Autosign::Journal"
|
12
|
+
@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
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(uuid)
|
41
|
+
fail unless validate_uuid(uuid)
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
|
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
|
53
|
+
#
|
54
|
+
# ==== Examples
|
55
|
+
#
|
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')
|
60
|
+
#
|
61
|
+
# This will only succeed if the token has not previously been added
|
62
|
+
# This is the primary way this class is expected to be used
|
63
|
+
def add(uuid, validto, data = {})
|
64
|
+
@log.debug "attempting to add UUID: '#{uuid.to_s}' which is valid to '#{Time.at(validto.to_i)}' with data #{data.to_s}"
|
65
|
+
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
|
+
|
70
|
+
store = self.setup
|
71
|
+
# wrap the change in a transaction because multiple autosign instances
|
72
|
+
# may try to run simultaneously. This will block until another process
|
73
|
+
# releases the transaction lock.
|
74
|
+
result = store.transaction do
|
75
|
+
# check whether the UUID is already in the store
|
76
|
+
if store.root?(uuid)
|
77
|
+
@log.warn "Token with UUID '#{uuid}' is already saved in the journal, will not add'"
|
78
|
+
store.abort
|
79
|
+
else
|
80
|
+
# save the token identified by UUID
|
81
|
+
store[uuid.to_s] = {:validto => validto.to_s, :data => data}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# return true if the transaction went through
|
86
|
+
return !!result
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate_uuid(uuid)
|
90
|
+
unless uuid.is_a?(String)
|
91
|
+
@log.error "UUID is not a string"
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
|
95
|
+
unless !!/^\S{8}-\S{4}-4\S{3}-[89abAB]\S{3}-\S{12}$/.match(uuid.to_s)
|
96
|
+
@log.error "UUID is not a valid V4 UUID"
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_data(data)
|
102
|
+
unless data.is_a?(Hash)
|
103
|
+
@log.error "data is not a hash"
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def validate_timestamp(time)
|
109
|
+
unless time.is_a?(Integer)
|
110
|
+
@log.error "timestamp is not an integer"
|
111
|
+
return false
|
112
|
+
end
|
113
|
+
|
114
|
+
if Time.at(time) > Time.now
|
115
|
+
@log.debug "validated timestamp: " + time
|
116
|
+
return true
|
117
|
+
else
|
118
|
+
@log.error "invalid timestamp: " + time
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Autosign
|
2
|
+
require 'jwt'
|
3
|
+
require 'JSON'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
class Token
|
7
|
+
attr_reader :validfor
|
8
|
+
attr_reader :certname
|
9
|
+
attr_reader :reusable
|
10
|
+
attr_reader :requester
|
11
|
+
attr_reader :secret
|
12
|
+
attr_accessor :validto
|
13
|
+
attr_accessor :uuid
|
14
|
+
|
15
|
+
def initialize(certname, reusable=false, validfor=7200, requester, secret)
|
16
|
+
# set up logging
|
17
|
+
@log = Logging.logger['Autosign::Token']
|
18
|
+
@log.debug "initializing"
|
19
|
+
|
20
|
+
@validfor = validfor
|
21
|
+
@certname = certname
|
22
|
+
@reusable = reusable
|
23
|
+
@requester = requester
|
24
|
+
@secret = secret
|
25
|
+
@uuid = SecureRandom.uuid # UUID is needed to allow token regeneration with the same settings
|
26
|
+
@validto = Time.now.to_i + self.validfor
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.validate(requested_certname, token, hmac_secret)
|
30
|
+
@log = Logging.logger['Autosign::Token.validate']
|
31
|
+
@log.debug "attempting to validate token"
|
32
|
+
@log.info "attempting to validate token for: #{requested_certname.to_s}"
|
33
|
+
errors = []
|
34
|
+
begin
|
35
|
+
@log.debug "Decoding and parsing token"
|
36
|
+
data = JSON.parse(JWT.decode(token, hmac_secret)[0]["data"])
|
37
|
+
rescue JWT::ExpiredSignature
|
38
|
+
@log.warn "Token has an expired signature"
|
39
|
+
errors << "Expired Signature"
|
40
|
+
rescue
|
41
|
+
@log.warn "Unable to validate token successfully"
|
42
|
+
errors << "Invalid Token"
|
43
|
+
end
|
44
|
+
@log.warn "validation failed with: #{errors.join(', ')}" unless errors.count == 0
|
45
|
+
certname_is_regex = (data["certname"] =~ /\/[^\/].*\//) ? true : false
|
46
|
+
|
47
|
+
if certname_is_regex
|
48
|
+
@log.debug "validating certname as regular expression"
|
49
|
+
regexp = Regexp.new(/\/([^\/].*)\//.match(data["certname"])[1])
|
50
|
+
unless regexp.match(requested_certname)
|
51
|
+
errors << "certname: '#{requested_certname}' does not match validation regex: '#{regexp.to_s}'"
|
52
|
+
end
|
53
|
+
else
|
54
|
+
unless data["certname"] == requested_certname
|
55
|
+
errors << "certname: '#{requested_certname}' does not match certname '#{data["certname"]}' in token"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
unless errors.count == 0
|
60
|
+
@log.warn "validation failed with: #{errors.join(', ')}"
|
61
|
+
return false
|
62
|
+
else
|
63
|
+
@log.info "validated token successfully"
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
|
67
|
+
# we should never get here, but if we do we should break instead of returning anything
|
68
|
+
@log.error "unexpectedly reached end of validation method"
|
69
|
+
raise Autosign::Token::ValidationError
|
70
|
+
end
|
71
|
+
|
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
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.from_token(token, hmac_secret)
|
84
|
+
begin
|
85
|
+
decoded = JWT.decode(token, hmac_secret)[0]
|
86
|
+
rescue JWT::ExpiredSignature
|
87
|
+
raise Autosign::Token::ExpiredToken
|
88
|
+
rescue
|
89
|
+
raise Autosign::Token::Invalid
|
90
|
+
end
|
91
|
+
certname = JSON.parse(decoded["data"])["certname"]
|
92
|
+
requester = JSON.parse(decoded["data"])["requester"]
|
93
|
+
reusable = JSON.parse(decoded["data"])["reusable"]
|
94
|
+
validfor = JSON.parse(decoded["data"])["validfor"]
|
95
|
+
|
96
|
+
new_token = self.new(certname, reusable, validfor, requester, hmac_secret)
|
97
|
+
new_token.validto = self.token_validto(token, hmac_secret)
|
98
|
+
new_token.uuid = JSON.parse(decoded["data"])["uuid"]
|
99
|
+
|
100
|
+
return new_token
|
101
|
+
end
|
102
|
+
|
103
|
+
def certname=(str)
|
104
|
+
@name = str
|
105
|
+
end
|
106
|
+
|
107
|
+
def reusable=(bool)
|
108
|
+
@reusable = !!bool
|
109
|
+
end
|
110
|
+
|
111
|
+
def reusable
|
112
|
+
!!@reusable
|
113
|
+
end
|
114
|
+
|
115
|
+
def requester=(str)
|
116
|
+
@requester = str
|
117
|
+
end
|
118
|
+
|
119
|
+
def secret=(str)
|
120
|
+
@secret = str
|
121
|
+
end
|
122
|
+
|
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
|
+
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'
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|