├── LICENSE
├── README.textile
├── Rakefile
├── TODO
├── lib
└── ssh
│ └── key
│ ├── helper.rb
│ ├── signature.rb
│ ├── signer.rb
│ └── verifier.rb
├── samples
├── client.rb
├── server.rb
├── speed.rb
└── test.rb
├── sshkeyauth.gemspec
└── test
├── alltests.rb
├── keys
├── README
├── tester_nopassphrase_rsa
├── tester_nopassphrase_rsa.pub
├── tester_withpassphrase_rsa
└── tester_withpassphrase_rsa.pub
├── test_agent.rb
└── test_with_files.rb
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2014 Jordan Sissel and contributors.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | h1. SSH Key Authentication for Ruby
2 |
3 | This projects aims to provide authentication (signing and verifying) by using
4 | ssh keys.
5 |
6 | h2. Why?
7 |
8 | Most infrastructures I've managed have had bits of single-sign-on mixed
9 | not-so-single-sign-on. Further, the first thing I disable in sshd is always
10 | password authentication and make sure everyone gets an ssh key and knows
11 | how to use it.
12 |
13 | I like ssh keys. They're simple. On my infrastructure, they're everywhere. It's
14 | how you log in. Further, like kerberos tickets, you can forward your ssh agent
15 | along with your ssh sessions letting you forward your credentials more
16 | naturally than, say, with ssl or pgp.
17 |
18 | With tools like mcollective, openvpn, etc, you often need a way to authorize
19 | and authenticate users. With OpenVPN you can use passwords or SSL certs.
20 | Managing my own SSL CA is not my idea of a good time, but is often required
21 | for some things. Further, teaching engineers how to use SSH with keys is hard
22 | enough without saying "oh, here's another authentication scheme with your SSL
23 | cert."
24 |
25 | Add PGP to the mix, and you've got big frownyface - from me, anyway. So many
26 | systems require one unique authentication mechanism that you end up with N
27 | mechanisms. Hopefully this tool lets you avoid that if possible.
28 |
29 | The original point of writing this module was to allow authentication of
30 | messages from specific users over mcollective. The goal is to be able to use
31 | keys in your ssh-agent and sign messages and have them verified on the remote
32 | end by using your own user's authorized_keys file.
33 |
34 | h2. Get with the downloading
35 |
36 | To install sshkeyauth, you can use gem:
37 |
38 | bc. gem install sshkeyauth
39 |
40 | Otherwise, you are welcome to clone this repository.
41 |
42 | h2. Example
43 |
44 | h3. First Requirements
45 |
46 | Signing requires you either have an ssh agent running or explicitly tell the
47 | signer about an ssh private key file.
48 |
49 | h3. Sign a string
50 |
51 | Signing a string will sign with all known ssh keys. This is useful for
52 | broadcast media like mcollective where you don't have the luxury of
53 | challenge-response (two-way, multi-message communication). For systems with
54 | challenge-response, you can simply iterate over the results and try each
55 | signature.
56 |
57 |
58 | require "ssh/key/signer"
59 |
60 | signer = SSH::Key::Signer.new
61 | # By default we'll automatically use your ssh agent for signing keys.
62 | # But if you want to also (or instead) specify a private key file, use this:
63 | signer.add_key_file("/path/to/key", "mypassphrase")
64 |
65 | # Of course, if your key doesn't have a passphrase, you can omit the argument:
66 | # This works well with host keys, too, if you want to sign messages as a host
67 | # instead of a user.
68 | signer.add_key_file("/path/to/key")
69 |
70 | # Now sign a string (will sign with all known private or agent keys)
71 | signatures = signer.sign("Hello world")
72 |
73 | # 'signatures' is an array of SSH::Key::Signature which has properties:
74 | # type - the ssh key type (ssh-rsa, ssh-dsa)
75 | # signature - the signature string (bytes)
76 | # identity - the identity that signed the original string
77 |
78 |
79 | h3. Verifying a signature
80 |
81 | Verifying requires you have a signature and the original string.
82 | The verifier will use your ssh agent if possible. Disable this by setting
83 | Verifier#use_agent=false. It will also try to find your user's authorized_keys
84 | file and the public keys in it. Additionally, you can add public key strings
85 | with Verifier#add_public_key_data.
86 |
87 |
88 | require "ssh/key/verifier"
89 | verifier = SSH::Key::Verifier.new
90 |
91 | # Again, by default we'll try to use your ssh agent.
92 |
93 | # from above, 'signatures'
94 | verified = verifier.verify?(signatures, original)
95 | puts "Verified: #{verified}"
96 | # the above should print 'true' if verification was successful.
97 |
98 |
99 | You could also use SSH::Key::Verifier#verify(...) to get a detailed result of
100 | the verification.
101 |
102 | h3. Using the stuff in samples/
103 |
104 | The samples/ directory has a 'client' and 'server'. The client does signing;
105 | server does verification. Client writes to stdout; server reads from stdin.
106 | They both use json for and base64 for simplicity in message passing.
107 |
108 | Check your ssh-agent has keys loaded:
109 |
110 |
111 | % ssh-add -l
112 | 2048 4b:cc:f1:b7:60:99:ac:77:b3:51:38:3e:f4:b6:d6:74 id_rsa_tester (RSA)
113 |
114 |
115 | Client-only:
116 |
117 |
118 | % ruby client.rb "hello"
119 | {"signature":"INBpScDBdmmRjrEbLjyarGwoZh1tGEtVVHX8syq94Z/hN36B0r88FhCXjcPj\ngafhVDhZXAaoVSSE0L2o8i045F7Fbn8Uh0jjmCWKJX8jY0ZqNVWfetmfjbEL\nuovuTR5+pBhnf5QMVgAXirNBqvT0vOPcrOuHFr9kcAH7RYdLydPyQmVDyjGa\nOTOffumaUFLX/KbCM/4jR0zpVA8i4E9MlEwd7gGNy1RmE4chZvexP6rgMMyk\n52BUT12QIiXiXbY7SzR5AwrfSJbw+CAudQEHm4rjPTgATZvqhyeNuEuYjpwA\n3RTRl9gA7Qrc/Gcwt9jMlkKgmOr8OMQRPZr2l2YMHg==\n","original":"hello"}
120 |
121 |
122 | Both:
123 |
124 | % ruby client.rb "hello" | SSH_AUTH_SOCK= ruby server.rb
125 | W, [2010-10-10T03:07:02.866074 #26915] WARN -- : SSH Agent not available
126 | true
127 |
128 |
129 | We empty 'SSH_AUTH_SOCK' to prevent the 'server' from using the ssh agent for
130 | public key knowledge (forces authorized_keys usage). Server will try all ssh
131 | keys in your user's authorized_keys file.
132 |
133 | h2. Scaling/Speed
134 |
135 | You can use 'openssl speed rsa' to benchmark how many signatures and
136 | verifications per second you can do. A sample output looks like this:
137 |
138 |
139 | sign verify sign/s verify/s
140 | rsa 512 bits 0.000143s 0.000013s 6985.0 79548.6
141 | rsa 1024 bits 0.000645s 0.000035s 1550.7 28378.6
142 | rsa 2048 bits 0.004012s 0.000113s 249.3 8858.3
143 | rsa 4096 bits 0.026481s 0.000406s 37.8 2460.7
144 |
145 |
146 | Verification is much faster than signing, which is good becuase in many cases
147 | you probably have many more public keys to verify against than you would to
148 | sign against.
149 |
150 | h2. TODO
151 |
152 | * Currently we iterate all known public keys (for a user, etc), this may be
153 | undesirable in some cases.
154 | * Send the public key along with any signature so the reciever can easily look
155 | up which ssh key should be used to verify. That is, we should look for matching
156 | public keys that are valid for the user and then verify with that. the full
157 | public key to identify which public key we are using to avoid unnecessary extra key
158 | signature checking, which can be costly at scale (especially if you are verifying
159 | against a known_hosts file of thousands and can't find the host entry).
160 | * Currently restricted to signing with private keys and verifying with public keys.
161 | Obviously, signing with public keys and verifying with private keys would be useful,
162 | too. However, I don't think we can verify using private keys using ssh agents.
163 | * Add helper methods for verifying a signature against a known_hosts key
164 | (lookup using ssh-keygen -F )
165 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | task :default => [:package]
2 |
3 | task :test do
4 | system("cd test; ruby alltests.rb")
5 | end
6 |
7 | task :package => [:test, :package_real] do
8 | end
9 |
10 | task :package_real do
11 | system("gem build sshkeyauth.gemspec")
12 | end
13 |
14 | task :publish do
15 | latest_gem = %x{ls -t sshkeyauth*.gem}.split("\n").first
16 | system("gem push #{latest_gem}")
17 | end
18 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | - Inject 'comment' field for keys loaded from authorized_keys file
2 |
--------------------------------------------------------------------------------
/lib/ssh/key/helper.rb:
--------------------------------------------------------------------------------
1 | require "net/ssh"
2 |
3 | module SSH; module Key; class KeyNotFound < StandardError
4 | end; end; end
5 |
6 | module SSH; module Key; module Helper
7 | # Add a private key to this signer.
8 | def add_key_file(path, passphrase=nil)
9 | @logger.info "Adding key from file #{path} (with#{passphrase ? "" : "out"} passphrase)"
10 | @keys << Net::SSH::KeyFactory.load_private_key(path, passphrase)
11 | end # def add_key_file
12 |
13 | # Add a public key from your known_hosts file
14 | def add_key_from_host(hostname)
15 | hostkey = %x{ssh-keygen -F "#{hostname}"}.split("\n")[1].chomp.split(" ",2)[-1] rescue nil
16 | if hostkey == nil
17 | raise SSH::Key::KeyNotFound.new("Could not find host key '#{hostname}' " \
18 | "in known_hosts (using ssh-keygen -F)")
19 | end
20 | @keys << Net::SSH::KeyFactory.load_data_public_key(hostkey)
21 | end
22 |
23 | # Add a public key from a ublic key string
24 | def add_public_key_data(data)
25 | @logger.info "Adding key from data #{data}"
26 | @keys << Net::SSH::KeyFactory.load_data_public_key(data)
27 | end # def add_key_file
28 | end; end; end # module SSH::Key::Helper
29 |
--------------------------------------------------------------------------------
/lib/ssh/key/signature.rb:
--------------------------------------------------------------------------------
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
32 |
--------------------------------------------------------------------------------
/lib/ssh/key/signer.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "rubygems"
4 | require "net/ssh"
5 | require "ssh/key/signature"
6 | require "ssh/key/helper"
7 | require "etc"
8 |
9 | module SSH; module Key; class Signer
10 | include SSH::Key::Helper
11 |
12 | attr_accessor :account
13 | attr_accessor :sshd_config_file
14 | attr_accessor :logger
15 | attr_accessor :use_agent
16 |
17 | def initialize
18 | @agent = Net::SSH::Authentication::Agent.new
19 | @use_agent = true
20 | @logger = Logger.new(STDERR)
21 | @logger.level = Logger::WARN
22 | @keys = []
23 | end # def initialize
24 |
25 | def ensure_connected
26 | begin
27 | @agent.connect! if !@agent.socket
28 | rescue Net::SSH::Authentication::AgentNotAvailable => e
29 | @use_agent = false
30 | end
31 | end # def ensure_connected
32 |
33 | # Signs a string with all available ssh keys
34 | #
35 | # * string - the value to sign
36 | #
37 | # Returns an array of SSH::Key::Signature objects
38 | #
39 | # 'identity' on each object is an openssl key instance of one of these typs:
40 | # * OpenSSL::PKey::RSA
41 | # * OpenSSL::PKey::DSA
42 | # * OpenSSL::PKey::DH
43 | #
44 | # Net::SSH monkeypatches the above classes to add additional methods, so just
45 | # be aware.
46 | def sign(string)
47 | identities = signing_identities
48 | signatures = []
49 | identities.each do |identity|
50 | if identity.private?
51 | # FYI: OpenSSL::PKey::RSA#ssh_type and #ssh_do_sign are monkeypatched
52 | # by Net::SSH
53 | signature = SSH::Key::Signature.new
54 | signature.type = identity.ssh_type
55 | signature.signature = identity.ssh_do_sign(string)
56 | else
57 | # Only public signing identities come from our agent.
58 | signature = SSH::Key::Signature.from_string(@agent.sign(identity, string))
59 | end
60 | signature.identity = identity
61 | signatures << signature
62 | end
63 | return signatures
64 | end
65 |
66 | # Get a list of all identities we can sign with. This will pull from your
67 | # ssh-agent if enabled.
68 | def signing_identities
69 | identities = []
70 | if @use_agent
71 | ensure_connected
72 | begin
73 | @agent.identities.each { |id| identities << id }
74 | rescue => e
75 | @logger.warn("Error talking to agent while asking for message signing. Disabling agent (Error: #{e})")
76 | @use_agent = false
77 | end
78 | end
79 |
80 | if @keys
81 | @keys.each { |id| identities << id }
82 | end
83 | return identities
84 | end # def signing_identities
85 |
86 | # Add a private key to this Signer from a file (like ".ssh/id_rsa")
87 | # * path - the string path to the key
88 | # * passphrase - the passphrase for this key, omit if no passphrase.
89 | def add_private_key_file(path, passphrase=nil)
90 | @keys << Net::SSH::KeyFactory.load_private_key(path, passphrase)
91 | end # def add_private_key_file(path)
92 | end; end; end # class SSH::Key::Signer
93 |
--------------------------------------------------------------------------------
/lib/ssh/key/verifier.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "rubygems"
4 | require "net/ssh"
5 | require "ssh/key/signature"
6 | require "ssh/key/helper"
7 | require "etc"
8 |
9 | module SSH; module Key; class Verifier
10 | include SSH::Key::Helper
11 |
12 | attr_accessor :account
13 | attr_accessor :sshd_config_file
14 | attr_accessor :authorized_keys_file
15 | attr_accessor :logger
16 | attr_accessor :use_agent
17 | attr_accessor :use_authorized_keys
18 |
19 | # We only support protocol 2 public keys.
20 | # protocol2 is: options keytype b64key comment
21 | AUTHORIZED_KEYS_REGEX =
22 | /^((?:[A-Za-z0-9-]+(?:="[^"]+")?,?)+ *)?(ssh-(?:dss|rsa)) *([^ ]*) *(.*)/
23 |
24 | # A new SSH Key Verifier.
25 | #
26 | # * account - optional string username. Should be a valid user on the system.
27 | #
28 | # If account is nil or omitted, then it defaults to the user running
29 | # this process (current user)
30 | def initialize(account=nil)
31 | if account == nil
32 | account = Etc.getlogin
33 | end
34 |
35 | @account = account
36 | @agent = Net::SSH::Authentication::Agent.new
37 | @use_agent = true
38 | @use_authorized_keys = true
39 | @sshd_config_file = "/etc/ssh/sshd_config"
40 | @authorized_keys_file = nil
41 | @logger = Logger.new(STDERR)
42 | @logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
43 | @keys = []
44 | end # def initialize
45 |
46 | def ensure_connected
47 | begin
48 | @agent.connect! if !@agent.socket
49 | rescue Net::SSH::Authentication::AgentNotAvailable => e
50 | @use_agent = false
51 | @logger.info "SSH Agent not available"
52 | rescue => e
53 | @use_agent = false
54 | @logger.warn "Unexpected error ocurred. Disabling agent usage."
55 | end
56 | end # def ensure_connected
57 |
58 | # Can we validate 'original' against the signature(s)?
59 | #
60 | # * signature - a single SSH::Key::Signature or
61 | # hash of { identity => signature } values.
62 | # * original - the original string to verify against
63 | #
64 | # See also: SSH::Key::Signer#sign
65 | def verify?(signature, original)
66 | results = verify(signature, original)
67 | results.each do |identity, verified|
68 | return true if verified
69 | end
70 | return false
71 | end # def verify?
72 |
73 | # Verify an original with the signatures.
74 | # * signatures - a hash of { identity => signature } values
75 | # or, it can be an array of signature strings
76 | # or, it can simply be a signature string.
77 | # * original - the original string value to verify
78 | def verify(signatures, original)
79 | @logger.info "Getting identities"
80 | identities = verifying_identities
81 | @logger.info "Have #{identities.length} identities"
82 | results = {}
83 |
84 | if signatures.is_a? Hash
85 | @logger.debug("verify 'signatures' is a Hash")
86 | inputs = signatures.values
87 | elsif signatures.is_a? Array
88 | @logger.debug("verify 'signatures' is an Array")
89 | inputs = signatures
90 | elsif signatures.is_a? String
91 | @logger.debug("verify 'signatures' is an String")
92 | inputs = [signatures]
93 | end
94 |
95 | if inputs[0].is_a? SSH::Key::Signature
96 | @logger.debug("verify 'signatures' is an array of Signatures")
97 | inputs = inputs.collect { |i| i.signature }
98 | end
99 |
100 | inputs.each do |signature|
101 | identities.each do |identity|
102 | key = [signature, identity]
103 | results[key] = identity.ssh_do_verify(signature, original)
104 | @logger.info "Trying key #{identity.to_s.split("\n")[1]}... #{results[key]}"
105 | end
106 | end
107 | return results
108 | end # def verify
109 |
110 | def verifying_identities
111 | identities = []
112 | ensure_connected
113 | if @use_agent
114 | begin
115 | @agent.identities.each { |id| identities << id }
116 | rescue ArgumentError => e
117 | @logger.warn("Error from agent query: #{e}")
118 | @use_agent = false
119 | end
120 | end
121 |
122 | if @use_authorized_keys
123 | # Verifying should include your authorized_keys file, too, if we can
124 | # find it.
125 | authorized_keys.each { |id| identities << id }
126 | end
127 |
128 | @keys.each { |id| identities << id }
129 | return identities
130 | end # def verifying_identities
131 |
132 | def find_authorized_keys_file
133 | # Look up the @account's home directory.
134 | begin
135 | account_info = Etc.getpwnam(@account)
136 | rescue ArgumentError => e
137 | @logger.warn("User '#{@account}' does not exist.")
138 | end
139 |
140 | # TODO(sissel): It's not clear how we should handle empty homedirs, if
141 | # that happens?
142 |
143 | # Default authorized_keys location
144 | authorized_keys_file = ".ssh/authorized_keys"
145 |
146 | # Try to find the AuthorizedKeysFile definition in the config.
147 | if File.exists?(@sshd_config_file)
148 | begin
149 | authorized_keys_file = File.new(@sshd_config_file).grep(/^\s*AuthorizedKeysFile/)[-1].split(" ")[-1]
150 | rescue
151 | @logger.info("No AuthorizedKeysFile setting found in #{@sshd_config_file}, assuming '#{authorized_keys_file}'")
152 | end
153 | else
154 | @logger.warn("No sshd_config file found '#{@sshd_config_file}'. Won't check for authorized keys files. Assuming '#{authorized_keys_file}'")
155 | end
156 |
157 | # Support things sshd_config does.
158 | authorized_keys_file.gsub!(/%%/, "%")
159 | authorized_keys_file.gsub!(/%u/, @account)
160 | if authorized_keys_file =~ /%h/
161 | if account_info == nil
162 | @logger.warn("No homedirectory for #{@account}, skipping authorized_keys")
163 | return nil
164 | end
165 |
166 | authorized_keys_file.gsub!(/%h/, account_info.dir)
167 | end
168 |
169 | # If relative path, use the homedir.
170 | if authorized_keys_file[0,1] != "/"
171 | if account_info == nil
172 | @logger.warn("No homedirectory for #{@account} and authorized_keys path is relative, skipping authorized_keys")
173 | return nil
174 | end
175 |
176 | authorized_keys_file = "#{account_info.dir}/#{authorized_keys_file}"
177 | end
178 |
179 | return authorized_keys_file
180 | end # find_authorized_keys_file
181 |
182 | def authorized_keys
183 | if @authorized_keys_file
184 | authorized_keys_file = @authorized_keys_file
185 | else
186 | authorized_keys_file = find_authorized_keys_file
187 | end
188 |
189 | if authorized_keys_file == nil
190 | @logger.info("No authorized keys file found.")
191 | return []
192 | end
193 |
194 | if !File.exists?(authorized_keys_file)
195 | @logger.info("User '#{@account}' has no authorized keys file '#{authorized_keys_file}'")
196 | return []
197 | end
198 |
199 | keys = []
200 | @logger.info("AuthorizedKeysFile ==> #{authorized_keys_file}")
201 | File.new(authorized_keys_file).each do |line|
202 | next if line =~ /^\s*$/ # Skip blanks
203 | next if line =~ /^\s*\#/ # Skip comments
204 | @logger.info line
205 |
206 | comment = nil
207 |
208 | # TODO(sissel): support more known_hosts formats
209 | if line =~ /^\|1\|/ # hashed known_hosts format
210 | comment, line = line.split(" ",2)
211 | end
212 |
213 | identity = Net::SSH::KeyFactory.load_data_public_key(line)
214 |
215 | # Add the '.comment' attribute to our key
216 | identity.extend(Net::SSH::Authentication::Agent::Comment)
217 |
218 | match = AUTHORIZED_KEYS_REGEX.match(line)
219 | if match
220 | comment = match[-1]
221 | else
222 | puts "No comment or could not parse #{line}"
223 | end
224 | identity.comment = comment if comment
225 |
226 | keys << identity
227 | end
228 | return keys
229 | end
230 |
231 | # Add a private key to this Verifier from a file (like ".ssh/id_rsa")
232 | # * path - the string path to the key
233 | # * passphrase - the passphrase for this key, omit if no passphrase.
234 | def add_private_key_file(path, passphrase=nil)
235 | @keys << Net::SSH::KeyFactory.load_private_key(path, passphrase)
236 | end # def add_private_key_file(path)
237 |
238 | # Add a public key to this Verifier from a file (like ".ssh/id_rsa.pub")
239 | #
240 | # This is for individual key files. If you want to specify an alternate
241 | # location for your authorized_keys file, set:
242 | # Verifier#authorized_keys_file = "/path/to/authorized_keys"
243 | #
244 | # * path - the string path to the public key
245 | def add_public_key_file(path)
246 | @keys << Net::SSH::KeyFactory.load_public_key(path)
247 | end # def add_private_key_file(path)
248 | end; end; end # class SSH::Key::Verifier
249 |
--------------------------------------------------------------------------------
/samples/client.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | #
3 |
4 | require "base64"
5 | require "json"
6 |
7 | $:.unshift "#{File.dirname(__FILE__)}/../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)
25 |
--------------------------------------------------------------------------------
/samples/server.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | #
3 |
4 | require "base64"
5 | require "json"
6 |
7 | $:.unshift "#{File.dirname(__FILE__)}/../lib"
8 | require "ssh/key/verifier"
9 |
10 | def main(argv)
11 | if argv.length == 0
12 | input = $stdin
13 | else
14 | input = argv
15 | end
16 | verifier = SSH::Key::Verifier.new
17 | verifier.use_agent = false
18 |
19 | input.each do |line|
20 | data = JSON.parse(line)
21 | signature = Base64.decode64(data["signature"])
22 | original = data["original"]
23 | puts verifier.verify?(signature, original)
24 | end
25 | end
26 |
27 | main(ARGV)
28 |
--------------------------------------------------------------------------------
/samples/speed.rb:
--------------------------------------------------------------------------------
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 | data = (argv[0] or "Hello world")
11 | signer = SSH::Key::Signer.new
12 |
13 | start = Time.now
14 | 0.upto(1000) do
15 | sigs = signer.sign(data)
16 | end
17 | duration = Time.now - start
18 | puts "Duration: #{duration}"
19 |
--------------------------------------------------------------------------------
/samples/test.rb:
--------------------------------------------------------------------------------
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 |
14 |
--------------------------------------------------------------------------------
/sshkeyauth.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |spec|
2 | files = []
3 | #dirs = %w{lib samples test bin}
4 | dirs = %w{lib samples}
5 | dirs.each do |dir|
6 | files += Dir["#{dir}/**/*"]
7 | end
8 |
9 | #svnrev = %x{svn info}.split("\n").grep(/Revision:/).first.split(" ").last.to_i
10 | rev = Time.now.strftime("%Y%m%d%H%M%S")
11 | spec.name = "sshkeyauth"
12 | spec.version = "0.0.9"
13 | spec.summary = "ssh key authentication (signing and verification)"
14 | spec.description = "Use your ssh keys (and your ssh agent) to sign and verify messages"
15 | spec.add_dependency("net-ssh")
16 | spec.files = files
17 | spec.require_paths << "lib"
18 | spec.license = "Apache 2.0"
19 |
20 | spec.author = "Jordan Sissel"
21 | spec.email = "jls@semicomplete.com"
22 | spec.homepage = "http://github.com/jordansissel/ruby-sshkeyauth"
23 | end
24 |
--------------------------------------------------------------------------------
/test/alltests.rb:
--------------------------------------------------------------------------------
1 | require 'test_agent'
2 | require 'test_with_files'
3 |
--------------------------------------------------------------------------------
/test/keys/README:
--------------------------------------------------------------------------------
1 | The passphrase for the key here is 'testing'
2 |
--------------------------------------------------------------------------------
/test/keys/tester_nopassphrase_rsa:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEoQIBAAKCAQEA8tpxavVG0N2T7dPKjSUZH12zwfyQkeH7HD8i7rv7/tXt0C+i
3 | k4Vo8FubxBy1+dd5+/M/AcChlAfPhRhkt+5Ge0XGhjJ/nsWJAahEYJH1N5E6JQnU
4 | VHSxYdDux76bUSEodfuskc/opmUaJ0Qbpw40OYe8e7VquL9pwAMHK6vUmfPfG0Jb
5 | 6AhwfDQ5nj/sZa3zs8GAueI4LD694oanm308vnDsrJuhfER4Hyiv89cega0k+gFk
6 | CR5/2cn33XFzl8x+70Cvd0jxKwVhTEKF3Dg5W/nz+ilfEUvfSF7Z5Vp9mxvtECe9
7 | DPJiBAWtvZA4LcSct/KcQ/k1W6CZbYVnMe6llwIBIwKCAQB85V7mjMVVeUTDdDxI
8 | lrx2h/YL/ju4vVySLxlHk+Cu8a2BAo4f+3fMEdsUZol5LPzn+XDcVHBawjA10gfp
9 | kHwE4g5TpPEtFSHjmF2uAevTb0J5csT4PAN0Ik7qYga8AmyUcs3HVPtOQp+8bCt6
10 | fFVfamDvKhmg13g23PxfmjLS1DBv4Ibgpw/4Z9baYRNDEwNnB1McxAfLhsPoUirB
11 | ByyAV333ZEMgpwv8NylqP5TT1jt2J4H9E4g9t8+YDC0QZZvTpnJzHS5MNTcdNOZU
12 | pt0KDQPCQyXaXUhY2H/zxYOAEbx8MS0OrgDdfnm/9FUTNaNpcbmdYJyqVpe/LrYl
13 | pOh7AoGBAPulbx9JTJnXt5kxODr9WlndyC9WOu6p1kzA+bGzoX1dnu1phPRJrG9W
14 | NWZYbL6p/ZBf2p+Jf0Q68Qr+cfpKn1/n9WRcgASWx4Awb5tgw7uptGSmKmKtPs+J
15 | MBT8AyS6IenyibmlSXktbJY2AjYQ8n66yM0AppNU/We6Z0enDoRFAoGBAPcOEG8/
16 | PhslCAzg2Shl54T/axNWYwF2qYJsCaLF3AVK0opT07jk4XF6+c5pRBPKAMumL0e+
17 | MiR8LGdwPSGtSxqy3PrJLKDHK70Nsyqd9yU7LEDofGutOzv1+5M6AfWg14ZlBYEg
18 | cwOZsWOuwSPMzWA+WuZYbBwQwe4xJtnP5pYrAoGBAO1ENZKG8HPLY/bWortGpaUw
19 | McY7XCLapXuRYHRY6LgH0FwwSigoNN4A00M9bdhXIkZLv4B6U2w3ks/Z9m+lcbIy
20 | eafE7bsvE99DnG3thVkq+orIjl0JvuDvEBPJCkczJ0pLFCQQ3t/3oOVmH1eMUlo7
21 | FRkPPfiglydJWhBbrpn3AoGAP4dGDfpRzHc8lZjnYijVIjMM57cgxdyvPstS71dz
22 | F03BHEFw9Qenr3dkzq1apgC+YECtEnK8b8gorOJY3MYpQWExglD82OLCGqvAW2py
23 | wG5NNUMYrflYX+Aqv3VQ9gTJtNgl/KHxsHf6ahb+duuFRKJR43XSqCGQ9BtEgSbU
24 | 5MkCgYBKNuZaqkOMQ6lgdqOXRdKb54fQsgOAUUBi4dkscRjUtO9ukv4Cb1au27UC
25 | lV3S5zXpBhxdgqzBzl/vMCF+rBbeamNxe9CdAd0eNzJtQvY2EjUUYsbkfSHy3QXU
26 | 0B2m7/UJ6RIhfmhOhnAG0POo7sy6oN1AmwwASAxFjZikkKq+vw==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/test/keys/tester_nopassphrase_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA8tpxavVG0N2T7dPKjSUZH12zwfyQkeH7HD8i7rv7/tXt0C+ik4Vo8FubxBy1+dd5+/M/AcChlAfPhRhkt+5Ge0XGhjJ/nsWJAahEYJH1N5E6JQnUVHSxYdDux76bUSEodfuskc/opmUaJ0Qbpw40OYe8e7VquL9pwAMHK6vUmfPfG0Jb6AhwfDQ5nj/sZa3zs8GAueI4LD694oanm308vnDsrJuhfER4Hyiv89cega0k+gFkCR5/2cn33XFzl8x+70Cvd0jxKwVhTEKF3Dg5W/nz+ilfEUvfSF7Z5Vp9mxvtECe9DPJiBAWtvZA4LcSct/KcQ/k1W6CZbYVnMe6llw== jls@snack.home
2 |
--------------------------------------------------------------------------------
/test/keys/tester_withpassphrase_rsa:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | Proc-Type: 4,ENCRYPTED
3 | DEK-Info: DES-EDE3-CBC,0CB5569EED9F08E0
4 |
5 | /COfpGU+bITpu74mcmyuQfS1xTStIubLSpHIszrUbbGVMz0bxXINZTtr6AO6zy87
6 | N2TdqsnLeJzy9ZOFpqJIx2lYlSzZ9vdtvW6+0hqgvB1zIYGr9kkE+jCAj9yCjToG
7 | zXiIIphUjeKyh/njJ4SfQdiptHT4px4y2X8qMp40ON3ra7xludMZLNysZd5GOlpH
8 | mBFtEuoa1A6PgDbbgdi3NOO3rFNgQ135ZJLSJ87QpU3t0SnQ/e1lL0zklFAhcArh
9 | UdOw8Z+JDCTq9UsO1tLsqDiwq5fNgWLiT3wD80HoUHvYi+4R2LtLU48BOKaJxIHm
10 | +yd4hYgTMgO0HPkYNhUs3mdaQ7+FOZkGpuv9c2gHPXaqW5BxTF+kQWonHet4/w+9
11 | bjOUO/fsKl293OlGxGsDTEuWLgaD3ybPx9a74z+pU+Lp5VTPaLzVGS5UHkZAupxP
12 | svd+EK1HbsPiPluawg4bcg6jtfkRgsAc/k8BMHRbM9NasaCwTrnqmDJ68DDtLHuK
13 | NhgLx+fJ9z28w4AHRBLAwEHhnS1ZLV2PCZzbiCNssylA4LxnjEZG9F5BMNGYDUWp
14 | lT/BDOEjb9DNtYUWTJXNKxCGOaSxRiVzcY/ZLx5Rnld2/mHMoIpmji/Mu0UIOFnV
15 | TkdGGMhEeGfC3JKyIf7j23ydJWu83L0FbUVsJ5dyTnNT0by1eZyeMWa8jwJL3lh9
16 | aV0IdvP/f4y5BKsuDxRn7U76hJ48SfqywaIb0iC7rb5yWEY54BuDdzbl94gm1wWL
17 | doo56QBX+UY2J38AomVXT0dbB2/H5ex2RXNbHoRVcnuupcjBgbSsakTEsBCj3fZc
18 | Mn1NySQa7ZzHWaQtj/rEWe0Bq0/kBEthhlc+tjz9fYI6GrmG24RA6S3/hHXGTxCz
19 | X4gM/goajURDIw7UuqU3gUX6z8DIpts5u86fEYAZHt1kB9NYPXLQ6YDxOObg7q+f
20 | qNFgJFGbi80YdrigZsi4b5XettbMhkfz+jTwucPBVxcxi8/AtbURI2m5+J2X6f46
21 | 8xti8IKIrAetoZpaLhM3WwRJUbYGO3dRw2YpJnj0946SHYX1OotvgGgS7bpkprfB
22 | NpKLFbE3ZoPxOprSOh+RtbwRhVHVnDIu3HntaClpoSWMY9+fLaFJ8GO1mqPBFGab
23 | R0MQyQ6Q3SpeEv+94XxTMkr6wo794uCr1yxGyG4GkEMDCBrxHCbK62+cVVbjkI/m
24 | Qjnin7q+Mq0tdD89QqL3jwDesv5Z/Ad7pYu1i8FgC7LAY2+Z9fZc3x22N6s9FUue
25 | d+dwXQZYa4P4x4z6KFWTPg+BASeC84fgNYLMuBhU8jA9hKSTo86lEXOS2APhbwe1
26 | 1GBJE0ya9UfFaOvcdhdDRAMWNkWg2DXmDQGLd44U05713sQy0d9fi6ewdsWXp7JT
27 | 48vE5SVpK/jtxW8zULoMDjymtx+U1DKWul4cLqLgRbMlkTXe+iIS1Ptgh6MdAF5l
28 | jyLNzWJJq0K2K1vQqws5RoZG7SQgWgljLlmMF1VL+v42OFmnDDH78ZcdsbWkiXcQ
29 | nZq3qEj1dRhdBxHonARBZSKJ3Mm7Sx0q9eAnUewXycsYE7fGt25xSA==
30 | -----END RSA PRIVATE KEY-----
31 |
--------------------------------------------------------------------------------
/test/keys/tester_withpassphrase_rsa.pub:
--------------------------------------------------------------------------------
1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4cqwTnDIJrQvDLNFeFHNXEUpniveoaDMRJ05XFg3b55i8h0oQV/8OpU2HMuTRTrL3/24hjL6f9rzumeYbI6/eIkGw1Zb1HQ4KCJorDVcMgbpQwlenqcCYNgzL4IujrmtFnd4s4hR2jxnlScHwGeuOgdqufBKWwQoTXOWi6mOGyuWF1dH4DHLUX74GaspG+3Pgg/J3/iZAuGZyWq4dHPIYMPeonlbeJovI9JBrJ2/KDpqIzrAgVkvRCbU824wIOsoAMybZMX3iN6N13yl6J4DOe2U28qxAdkK7LJuIw/RtiDhOCyie9/ILFdDzeTHVvzJSesUNrQh8k7c2I6GdBN3jQ== jls@snack.home
2 |
--------------------------------------------------------------------------------
/test/test_agent.rb:
--------------------------------------------------------------------------------
1 |
2 | require "test/unit"
3 |
4 | $:.unshift "#{File.dirname(__FILE__)}/../lib"
5 | require "ssh/key/signer"
6 | require "ssh/key/verifier"
7 |
8 | class TestAgent < Test::Unit::TestCase
9 | def setup
10 | # Ensure we don't accidentally use the caller's ssh agent
11 | ENV.delete("SSH_AUTH_SOCK")
12 | ENV.delete("SSH_AGENT_PID")
13 |
14 | # Run our own ssh-agent; it will output values and then fork/detach
15 | values = %x{ssh-agent}.split("\n").grep(/^SSH[^=]+=/)
16 | values.collect { |line| line.split("=", 2) }.each do |key, value|
17 | value.gsub!(/; export.*/, "")
18 | ENV[key] = value
19 | #puts "ENV[#{key}] = #{value}"
20 | end
21 | end # def setup
22 |
23 | def teardown
24 | # Should we use ssh-agent -k, instead?
25 | Process.kill("KILL", ENV["SSH_AGENT_PID"].to_i) rescue nil
26 | end # def teardown
27 |
28 | def test_no_keys
29 | signer = SSH::Key::Signer.new
30 | idcount = signer.signing_identities.length
31 | assert_equal(0, idcount,
32 | "A new signer with an empty ssh-agent should have no " \
33 | "identities, found #{idcount}")
34 | end
35 |
36 | def test_with_rsa_key
37 | system("ssh-add keys/tester_nopassphrase_rsa > /dev/null 2>&1")
38 |
39 | signer = SSH::Key::Signer.new
40 | idcount = signer.signing_identities.length
41 | assert_equal(1, idcount, "Expected 1 identity, found #{idcount}.")
42 | end
43 |
44 | def test_sign_and_verify_with_rsa_key
45 | system("ssh-add keys/tester_nopassphrase_rsa > /dev/null 2>&1")
46 | signer = SSH::Key::Signer.new
47 | verifier = SSH::Key::Verifier.new
48 |
49 | inputs = [ "hello", "foo bar 1 2 3 4", Marshal.dump({:test => :fizz}),
50 | "", "1", " " ]
51 | inputs.each do |data|
52 | signatures = signer.sign(data)
53 | assert(verifier.verify?(signatures, data),
54 | "Signature verify failed against data '#{data.inspect}'")
55 | end
56 | end
57 |
58 | def test_sign_and_verify_with_rsa_key_fails_on_bad_data
59 | system("ssh-add keys/tester_nopassphrase_rsa > /dev/null 2>&1")
60 | signer = SSH::Key::Signer.new
61 | verifier = SSH::Key::Verifier.new
62 |
63 | inputs = [ "hello", "foo bar 1 2 3 4", Marshal.dump({:test => :fizz}),
64 | "", "1", " " ]
65 | inputs.each do |data|
66 | signatures = signer.sign(data)
67 | assert(!verifier.verify?(signatures, data + "bad"),
68 | "Signature verify expected to fail when verifying against altered data")
69 | end
70 | end
71 | end # class TestAgent
72 |
73 |
74 |
--------------------------------------------------------------------------------
/test/test_with_files.rb:
--------------------------------------------------------------------------------
1 |
2 | require "test/unit"
3 |
4 | $:.unshift "#{File.dirname(__FILE__)}/../lib"
5 | require "ssh/key/signer"
6 | require "ssh/key/verifier"
7 |
8 | class TestWithFiles < Test::Unit::TestCase
9 | def setup
10 | @signer = SSH::Key::Signer.new
11 | @signer.use_agent = false
12 |
13 | @verifier = SSH::Key::Verifier.new
14 | @verifier.use_agent = false
15 | @verifier.use_authorized_keys = false
16 | end # def setup
17 |
18 | def test_with_rsa_key_without_passphrase
19 | @signer.add_private_key_file("keys/tester_nopassphrase_rsa")
20 | idcount = @signer.signing_identities.length
21 | assert_equal(1, idcount, "Expected 1 identity, found #{idcount}.")
22 | end # def test_with_rsa_key_without_passphrase
23 |
24 | def test_with_rsa_key_with_passphrase
25 | @signer.add_private_key_file("keys/tester_withpassphrase_rsa", "testing")
26 | idcount = @signer.signing_identities.length
27 | assert_equal(1, idcount, "Expected 1 identity, found #{idcount}.")
28 | end
29 |
30 | def test_sign_and_verify_with_rsa_key_file
31 | @signer.add_private_key_file("keys/tester_nopassphrase_rsa")
32 | @verifier.add_public_key_file("keys/tester_nopassphrase_rsa.pub")
33 |
34 | inputs = [ "hello", "foo bar 1 2 3 4", Marshal.dump({:test => :fizz}),
35 | "", "1", " " ]
36 | inputs.each do |data|
37 | signatures = @signer.sign(data)
38 | assert(@verifier.verify?(signatures, data),
39 | "Signature verify failed against data '#{data.inspect}'")
40 | end
41 | end
42 |
43 | def test_sign_and_verify_with_rsa_key_fails_on_bad_data
44 | @signer.add_private_key_file("keys/tester_nopassphrase_rsa")
45 | @verifier.add_public_key_file("keys/tester_nopassphrase_rsa.pub")
46 |
47 | inputs = [ "hello", "foo bar 1 2 3 4", Marshal.dump({:test => :fizz}),
48 | "", "1", " " ]
49 | inputs.each do |data|
50 | signatures = @signer.sign(data)
51 | assert(!@verifier.verify?(signatures, data + "bad"),
52 | "Signature verify expected to fail when verifying against altered data")
53 | end
54 | end
55 | end # class TestWithFiles
56 |
--------------------------------------------------------------------------------