autosign 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|