sshkeyauth 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.
- 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
|
+
|