sshkeyauth 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ssh/key/signature.rb +31 -0
- data/lib/ssh/key/signer.rb +88 -0
- data/lib/ssh/key/verifier.rb +232 -0
- data/samples/client.rb +24 -0
- data/samples/server.rb +25 -0
- data/samples/test.rb +13 -0
- metadata +79 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# TODO(sissel): Cache keys read from disk?
|
2
|
+
#
|
3
|
+
module SSH; module Key; class Signature
|
4
|
+
attr_accessor :type
|
5
|
+
attr_accessor :signature
|
6
|
+
attr_accessor :identity
|
7
|
+
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@use_agent = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_string(string)
|
14
|
+
keysig = self.new
|
15
|
+
keysig.parse(string)
|
16
|
+
return keysig
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parse an ssh key signature. Expects a signed string that came from the ssh
|
20
|
+
# agent, such as from SSHKeyAuth#sign
|
21
|
+
def parse(string)
|
22
|
+
offset = 0
|
23
|
+
typelen = string[offset..(offset + 3)].reverse.unpack("L")[0]
|
24
|
+
offset += 4
|
25
|
+
@type = string[offset .. (offset + typelen)]
|
26
|
+
offset += typelen
|
27
|
+
siglen = string[offset ..(offset + 3)].reverse.unpack("L")[0]
|
28
|
+
offset += 4
|
29
|
+
@signature = string[offset ..(offset + siglen)]
|
30
|
+
end # def parse
|
31
|
+
end; end; end # class SSH::Key::Signature
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "net/ssh"
|
5
|
+
require "ssh/key/signature"
|
6
|
+
require "etc"
|
7
|
+
|
8
|
+
module SSH; module Key; class Signer
|
9
|
+
attr_accessor :account
|
10
|
+
attr_accessor :sshd_config_file
|
11
|
+
attr_accessor :logger
|
12
|
+
attr_accessor :use_agent
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@agent = Net::SSH::Authentication::Agent.new
|
16
|
+
@use_agent = true
|
17
|
+
#@logger = Logger.new(STDERR)
|
18
|
+
#@logger.level = Logger::WARN
|
19
|
+
@logger = Logger.new("/tmp/verifier.log")
|
20
|
+
@logger.level = Logger::INFO
|
21
|
+
@keys = []
|
22
|
+
end # def initialize
|
23
|
+
|
24
|
+
def ensure_connected
|
25
|
+
begin
|
26
|
+
@agent.connect! if !@agent.socket
|
27
|
+
rescue Net::SSH::Authentication::AgentNotAvailable => e
|
28
|
+
@use_agent = false
|
29
|
+
end
|
30
|
+
end # def ensure_connected
|
31
|
+
|
32
|
+
# Add a private key to this signer.
|
33
|
+
def add_key_file(path, passphrase=nil)
|
34
|
+
@logger.info "Adding key from file #{path} (with#{passphrase ? "" : "out"} passphrase)"
|
35
|
+
@keys << Net::SSH::KeyFactory.load_private_key(path, passphrase)
|
36
|
+
end # def add_key_file
|
37
|
+
|
38
|
+
# Signs a string with all available ssh keys
|
39
|
+
#
|
40
|
+
# * string - the value to sign
|
41
|
+
#
|
42
|
+
# Returns an array of SSH::Key::Signature objects
|
43
|
+
#
|
44
|
+
# 'identity' on each object is an openssl key instance of one of these typs:
|
45
|
+
# * OpenSSL::PKey::RSA
|
46
|
+
# * OpenSSL::PKey::DSA
|
47
|
+
# * OpenSSL::PKey::DH
|
48
|
+
#
|
49
|
+
# Net::SSH monkeypatches the above classes to add additional methods, so just
|
50
|
+
# be aware.
|
51
|
+
def sign(string)
|
52
|
+
identities = signing_identities
|
53
|
+
signatures = []
|
54
|
+
identities.each do |identity|
|
55
|
+
if identity.private?
|
56
|
+
# FYI: OpenSSL::PKey::RSA#ssh_type and #ssh_do_sign are monkeypatched
|
57
|
+
# by Net::SSH
|
58
|
+
signature = SSH::Key::Signature.new
|
59
|
+
signature.type = identity.ssh_type
|
60
|
+
signature.signature = identity.ssh_do_sign(string)
|
61
|
+
else
|
62
|
+
# Only public signing identities come from our agent.
|
63
|
+
signature = SSH::Key::Signature.from_string(@agent.sign(identity, string))
|
64
|
+
end
|
65
|
+
signature.identity = identity
|
66
|
+
signatures << signature
|
67
|
+
end
|
68
|
+
return signatures
|
69
|
+
end
|
70
|
+
|
71
|
+
def signing_identities
|
72
|
+
identities = []
|
73
|
+
if @use_agent
|
74
|
+
ensure_connected
|
75
|
+
begin
|
76
|
+
@agent.identities.each { |id| identities << id }
|
77
|
+
rescue => e
|
78
|
+
@logger.warn("Error talking to agent while asking for message signing. Disabling agent (Error: #{e})")
|
79
|
+
@use_agent = false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if @keys
|
84
|
+
@keys.each { |id| identities << id }
|
85
|
+
end
|
86
|
+
return identities
|
87
|
+
end # def signing_identities
|
88
|
+
end; end; end # class SSH::Key::Signer
|
@@ -0,0 +1,232 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "net/ssh"
|
5
|
+
require "ssh/key/signature"
|
6
|
+
require "etc"
|
7
|
+
|
8
|
+
if $DEBUG
|
9
|
+
require "awesome_print"
|
10
|
+
end
|
11
|
+
|
12
|
+
module SSH; module Key; class Verifier
|
13
|
+
attr_accessor :account
|
14
|
+
attr_accessor :sshd_config_file
|
15
|
+
attr_accessor :authorized_keys_file
|
16
|
+
attr_accessor :logger
|
17
|
+
attr_accessor :use_agent
|
18
|
+
attr_accessor :use_authorized_keys
|
19
|
+
|
20
|
+
# We only support protocol 2 public keys.
|
21
|
+
# protocol2 is: options keytype b64key comment
|
22
|
+
AUTHORIZED_KEYS_REGEX =
|
23
|
+
/^((?:[A-Za-z0-9-]+(?:="[^"]+")?,?)+ *)?(ssh-(?:dss|rsa)) *([^ ]*) *(.*)/
|
24
|
+
|
25
|
+
# A new SSH Key Verifier.
|
26
|
+
#
|
27
|
+
# * account - optional string username. Should be a valid user on the system.
|
28
|
+
#
|
29
|
+
# If account is nil or omitted, then it defaults to the user running
|
30
|
+
# this process (current user)
|
31
|
+
def initialize(account=nil)
|
32
|
+
if account == nil
|
33
|
+
account = Etc.getlogin
|
34
|
+
end
|
35
|
+
|
36
|
+
@account = account
|
37
|
+
@agent = Net::SSH::Authentication::Agent.new
|
38
|
+
@use_agent = true
|
39
|
+
@use_authorized_keys = true
|
40
|
+
@sshd_config_file = "/etc/ssh/sshd_config"
|
41
|
+
@authorized_keys_file = nil
|
42
|
+
#@logger = Logger.new("/tmp/verifier.log")
|
43
|
+
@logger = Logger.new(STDERR)
|
44
|
+
@logger.level = Logger::WARN
|
45
|
+
@keys = []
|
46
|
+
end # def initialize
|
47
|
+
|
48
|
+
def ensure_connected
|
49
|
+
begin
|
50
|
+
@agent.connect! if !@agent.socket
|
51
|
+
rescue Net::SSH::Authentication::AgentNotAvailable => e
|
52
|
+
@use_agent = false
|
53
|
+
@logger.warn "SSH Agent not available"
|
54
|
+
rescue => e
|
55
|
+
@use_agent = false
|
56
|
+
@logger.warn "Unexpected error ocurred. Disabling agent usage."
|
57
|
+
end
|
58
|
+
end # def ensure_connected
|
59
|
+
|
60
|
+
# Can we validate 'original' against the signature(s)?
|
61
|
+
#
|
62
|
+
# * signature - a single SSH::Key::Signature or
|
63
|
+
# hash of { identity => signature } values.
|
64
|
+
# * original - the original string to verify against
|
65
|
+
#
|
66
|
+
# See also: SSH::Key::Signer#sign
|
67
|
+
def verify?(signature, original)
|
68
|
+
results = verify(signature, original)
|
69
|
+
results.each do |identity, verified|
|
70
|
+
@logger.warn "Trying key #{identity.to_s[0..30]}... #{verified}"
|
71
|
+
return true if verified
|
72
|
+
end
|
73
|
+
return false
|
74
|
+
end # def verify?
|
75
|
+
|
76
|
+
# Verify an original with the signatures.
|
77
|
+
# * signatures - a hash of { identity => signature } values
|
78
|
+
# or, it can be an array of signature strings
|
79
|
+
# or, it can simply be a signature string.
|
80
|
+
# * original - the original string value to verify
|
81
|
+
def verify(signatures, original)
|
82
|
+
@logger.info "Getting identities"
|
83
|
+
identities = verifying_identities
|
84
|
+
@logger.info "Have #{identities.length} identities"
|
85
|
+
results = {}
|
86
|
+
|
87
|
+
if signatures.is_a? Hash
|
88
|
+
inputs = signatures.values
|
89
|
+
elsif signatures.is_a? Array
|
90
|
+
inputs = signatures
|
91
|
+
elsif signatures.is_a? String
|
92
|
+
inputs = [signatures]
|
93
|
+
end
|
94
|
+
|
95
|
+
if inputs[0].is_a? SSH::Key::Signature
|
96
|
+
inputs = inputs.collect { |i| i.signature }
|
97
|
+
end
|
98
|
+
|
99
|
+
inputs.each do |signature|
|
100
|
+
identities.each do |identity|
|
101
|
+
results[identity] = identity.ssh_do_verify(signature, original)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
return results
|
105
|
+
end # def verify
|
106
|
+
|
107
|
+
def verifying_identities
|
108
|
+
identities = []
|
109
|
+
if @use_agent
|
110
|
+
ensure_connected
|
111
|
+
begin
|
112
|
+
@agent.identities.each { |id| identities << id }
|
113
|
+
rescue ArgumentError => e
|
114
|
+
@logger.warn("Error from agent query: #{e}")
|
115
|
+
@use_agent = false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if @use_authorized_keys
|
120
|
+
# Verifying should include your authorized_keys file, too, if we can
|
121
|
+
# find it.
|
122
|
+
authorized_keys.each { |id| identities << id }
|
123
|
+
end
|
124
|
+
|
125
|
+
@keys.each { |id| identities << id }
|
126
|
+
return identities
|
127
|
+
end # def verifying_identities
|
128
|
+
|
129
|
+
def add_public_key_data(data)
|
130
|
+
@logger.info "Adding key from data #{data}"
|
131
|
+
@keys << Net::SSH::KeyFactory.load_data_public_key(data)
|
132
|
+
end # def add_key_file
|
133
|
+
|
134
|
+
def find_authorized_keys_file
|
135
|
+
# Look up the @account's home directory.
|
136
|
+
begin
|
137
|
+
account_info = Etc.getpwnam(@account)
|
138
|
+
rescue ArgumentError => e
|
139
|
+
@logger.warn("User '#{@account}' does not exist.")
|
140
|
+
end
|
141
|
+
|
142
|
+
# TODO(sissel): It's not clear how we should handle empty homedirs, if
|
143
|
+
# that happens?
|
144
|
+
|
145
|
+
# Default authorized_keys location
|
146
|
+
authorized_keys_file = ".ssh/authorized_keys"
|
147
|
+
|
148
|
+
# Try to find the AuthorizedKeysFile definition in the config.
|
149
|
+
if File.exists?(@sshd_config_file)
|
150
|
+
begin
|
151
|
+
authorized_keys_file = File.new(@sshd_config_file).grep(/^\s*AuthorizedKeysFile/)[-1].split(" ")[-1]
|
152
|
+
rescue
|
153
|
+
@logger.info("No AuthorizedKeysFile setting found in #{@sshd_config_file}, assuming '#{authorized_keys_file}'")
|
154
|
+
end
|
155
|
+
else
|
156
|
+
@logger.warn("No sshd_config file found '#{@sshd_config_file}'. Won't check for authorized keys files. Assuming '#{authorized_keys_file}'")
|
157
|
+
end
|
158
|
+
|
159
|
+
# Support things sshd_config does.
|
160
|
+
authorized_keys_file.gsub!(/%%/, "%")
|
161
|
+
authorized_keys_file.gsub!(/%u/, @account)
|
162
|
+
if authorized_keys_file =~ /%h/
|
163
|
+
if account_info == nil
|
164
|
+
@logger.warn("No homedirectory for #{@account}, skipping authorized_keys")
|
165
|
+
return nil
|
166
|
+
end
|
167
|
+
|
168
|
+
authorized_keys_file.gsubs!(/%h/, account_info.dir)
|
169
|
+
end
|
170
|
+
|
171
|
+
# If relative path, use the homedir.
|
172
|
+
if authorized_keys_file[0] != "/"
|
173
|
+
if account_info == nil
|
174
|
+
@logger.warn("No homedirectory for #{@account} and authorized_keys path is relative, skipping authorized_keys")
|
175
|
+
return nil
|
176
|
+
end
|
177
|
+
|
178
|
+
authorized_keys_file = "#{account_info.dir}/#{authorized_keys_file}"
|
179
|
+
end
|
180
|
+
|
181
|
+
return authorized_keys_file
|
182
|
+
end # find_authorized_keys_file
|
183
|
+
|
184
|
+
def authorized_keys
|
185
|
+
if @authorized_keys_file
|
186
|
+
authorized_keys_file = @authorized_keys_file
|
187
|
+
else
|
188
|
+
authorized_keys_file = find_authorized_keys_file
|
189
|
+
end
|
190
|
+
|
191
|
+
if authorized_keys_file == nil
|
192
|
+
@logger.info("No authorized keys file found.")
|
193
|
+
return []
|
194
|
+
end
|
195
|
+
|
196
|
+
if !File.exists?(authorized_keys_file)
|
197
|
+
@logger.info("User '#{@account}' has no authorized keys file '#{authorized_keys_file}'")
|
198
|
+
return []
|
199
|
+
end
|
200
|
+
|
201
|
+
keys = []
|
202
|
+
@logger.info("AuthorizedKeysFile ==> #{authorized_keys_file}")
|
203
|
+
File.new(authorized_keys_file).each do |line|
|
204
|
+
next if line =~ /^\s*$/ # Skip blanks
|
205
|
+
next if line =~ /^\s*\#$/ # Skip comments
|
206
|
+
@logger.info line
|
207
|
+
|
208
|
+
comment = nil
|
209
|
+
|
210
|
+
# TODO(sissel): support more known_hosts formats
|
211
|
+
if line =~ /^\|1\|/ # hashed known_hosts format
|
212
|
+
comment, line = line.split(" ",2)
|
213
|
+
end
|
214
|
+
|
215
|
+
identity = Net::SSH::KeyFactory.load_data_public_key(line)
|
216
|
+
# Add the '.comment' attribute to our key
|
217
|
+
identity.extend(Net::SSH::Authentication::Agent::Comment)
|
218
|
+
|
219
|
+
match = AUTHORIZED_KEYS_REGEX.match(line)
|
220
|
+
if match
|
221
|
+
comment = match[-1]
|
222
|
+
else
|
223
|
+
puts "No comment or could not parse #{line}"
|
224
|
+
end
|
225
|
+
identity.comment = comment if comment
|
226
|
+
|
227
|
+
keys << identity
|
228
|
+
end
|
229
|
+
#@logger.info keys.awesome_inspect
|
230
|
+
return keys
|
231
|
+
end
|
232
|
+
end; end; end # class SSH::Key::Verifier
|
data/samples/client.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
require "base64"
|
5
|
+
require "json"
|
6
|
+
$:.unshift "../lib"
|
7
|
+
$:.unshift "lib"
|
8
|
+
require "ssh/key/signer"
|
9
|
+
|
10
|
+
def main(argv)
|
11
|
+
if argv.length == 0
|
12
|
+
data = $stdin.read
|
13
|
+
else
|
14
|
+
data = argv[0]
|
15
|
+
end
|
16
|
+
signer = SSH::Key::Signer.new
|
17
|
+
sigs = signer.sign(data)
|
18
|
+
sigs.each do |signature|
|
19
|
+
sig64 = Base64.encode64(signature.signature)
|
20
|
+
puts({ "original" => data, "signature" => sig64 }.to_json)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
main(ARGV)
|
data/samples/server.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
|
4
|
+
require "base64"
|
5
|
+
require "json"
|
6
|
+
$:.unshift "../lib"
|
7
|
+
require "ssh/key/verifier"
|
8
|
+
|
9
|
+
def main(argv)
|
10
|
+
if argv.length == 0
|
11
|
+
input = $stdin
|
12
|
+
else
|
13
|
+
input = argv
|
14
|
+
end
|
15
|
+
verifier = SSH::Key::Verifier.new
|
16
|
+
|
17
|
+
input.each do |line|
|
18
|
+
data = JSON.parse(line)
|
19
|
+
signature = Base64.decode64(data["signature"])
|
20
|
+
original = data["original"]
|
21
|
+
puts verifier.verify?(signature, original)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
main(ARGV)
|
data/samples/test.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift "../lib"
|
2
|
+
require "ssh/key/signer"
|
3
|
+
require "ssh/key/verifier"
|
4
|
+
|
5
|
+
|
6
|
+
signer = SSH::Key::Signer.new
|
7
|
+
verifier = SSH::Key::Verifier.new
|
8
|
+
|
9
|
+
original = "Hello world"
|
10
|
+
result = signer.sign original
|
11
|
+
verified = verifier.verify?(result, original)
|
12
|
+
puts "Verified: #{verified}"
|
13
|
+
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sshkeyauth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jordan Sissel
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-10 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: net-ssh
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
description: Add file 'tail' implemented with EventMachine. Also includes a 'glob watch' class for watching a directory pattern for new matches, like /var/log/*.log
|
33
|
+
email: jls@semicomplete.com
|
34
|
+
executables: []
|
35
|
+
|
36
|
+
extensions: []
|
37
|
+
|
38
|
+
extra_rdoc_files: []
|
39
|
+
|
40
|
+
files:
|
41
|
+
- lib/ssh/key/verifier.rb
|
42
|
+
- lib/ssh/key/signature.rb
|
43
|
+
- lib/ssh/key/signer.rb
|
44
|
+
- samples/server.rb
|
45
|
+
- samples/test.rb
|
46
|
+
- samples/client.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://github.com/jordansissel/ruby-sshkeyauth
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.3.6
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: eventmachine tail - a file tail implementation with glob support
|
78
|
+
test_files: []
|
79
|
+
|