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