├── 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 | --------------------------------------------------------------------------------