sshkeyauth 0.0.1

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