Skip to content

Instantly share code, notes, and snippets.

@UmbrielSecurity
Last active March 8, 2017 22:05
Show Gist options
  • Save UmbrielSecurity/8b37c30c7e1ff89f82e6c8b93ef3881e to your computer and use it in GitHub Desktop.
Save UmbrielSecurity/8b37c30c7e1ff89f82e6c8b93ef3881e to your computer and use it in GitHub Desktop.
Ruby client to request user/host SSH key signing (see: ssh_ca_api.rb)
#!/usr/bin/ruby
require 'rest-client'
require 'socket'
require 'base64'
require 'json'
require 'digest'
def usage()
puts
puts "Usage:"
puts " ruby keymgr.rb ( -u [userid2][,userid3]...| -d | -h fqdn )"
puts
puts " -u [userid2][,userid3]..."
puts " Request user public key signing for the current user (with other user principles too)"
puts " -d"
puts " Request that SSH daemon files (known_hosts and sshd_config) be updated (requires root)"
puts " -h fqdn"
puts " Requests host public key signing (requires root)"
puts
end
# Base part of our API's URI
base_api_uri = "http://vm-ca.fabrikam.com:4567/"
# The list of routes supported by this API client
host_sign_route = "sign_host_key"
user_sign_route = "sign_user_key"
daemon_update_route = "daemon_update"
# Full path to the system-wide SSH known_hosts file
known_hosts_file = "/etc/ssh/ssh_known_hosts"
# Location of the host SSH public key
host_public_key = "/etc/ssh/ssh_host_rsa_key.pub"
matchdata = host_public_key.match(/(^.*)(\.pub)$/)
# Location of the signed host SSH public key (will be created by this script)
signed_host_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
# Location of the CA's user public key (will be created by this script)
trusted_user_ca_key_file = "/etc/ssh/user_ca.pub"
auth_token = "foo"
# SSHD configurations files
sshd_config = "/etc/ssh/sshd_config"
sshd_config_backup = "/etc/ssh/sshd_config.keymgr.backup"
# Take our mode of operation from the first command line argument
mode = ARGV[0]
if ( mode == "-h" )
# This mode requires root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
# Obtain the hostname
if (ARGV[1])
fqdn=ARGV[1].chomp
else
puts "No fqdn supplied"
usage()
exit
end
matchdata = fqdn.match(/^([^\.]+)\.(.*)$/)
hostname = matchdata[1].chomp
dns_domain = matchdata[2].chomp
raw_public_key = File.read(host_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
# Submit REST call
response = RestClient.post "#{base_api_uri}#{host_sign_route}", \
:hostname => hostname, \
:dns_domain => dns_domain, \
:public_key => "#{encoded_public_key}", \
:auth_token => auth_token
# Parse the response (it should be in JSON format)
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{signed_host_key_file}."
File.open(signed_host_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_host_key']))}
if File.readlines(sshd_config).grep(/HostCertificate\s+#{signed_host_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("HostCertificate #{signed_host_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
# Request a user key be signed
elsif ( mode == "-u" )
# Set the default validity
validity="+24h"
username=ENV['USER'].chomp
# Obtain the username
if (ARGV[1])
principles=ARGV[1].chomp
end
# Obtain the location of the user's home directory from /etc/passwd
etc_passwd = (File.foreach('/etc/passwd').grep /^#{username}:/).to_s
home_dir_matchdata = etc_passwd.match(/^.*:([^:]+):[^:]+$/)
home_dir = home_dir_matchdata[1]
user_public_key = "#{home_dir}/.ssh/id_rsa.pub"
matchdata = user_public_key.match(/(^.*)(\.pub)$/)
signed_user_key_file = "#{matchdata[1]}-cert#{matchdata[2]}"
raw_public_key = File.read(user_public_key)
encoded_public_key = Base64.strict_encode64(raw_public_key).chomp
if (principles)
principles_list = "#{username},#{principles}"
else
principles_list = username
end
# Submit REST call
response = RestClient.post "#{base_api_uri}#{user_sign_route}", \
:principles => principles_list, \
:public_key => encoded_public_key, \
:validity => validity, \
:auth_token => auth_token
# Parse the response, it should be in JSON format
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
signed_user_key = Base64.decode64(response_hash['encoded_signed_user_key'])
hash_signed_user_key = Digest::SHA256.hexdigest signed_user_key
hash_signed_user_key_file = 0
if (File.file?(signed_user_key_file))
hash_signed_user_key_file = (Digest::SHA256.file signed_user_key_file).hexdigest
end
if (hash_signed_user_key != hash_signed_user_key_file)
if (hash_signed_user_key_file == 0)
puts "Creating #{signed_user_key_file}"
else
puts "Overwriting #{signed_user_key_file}"
end
File.open(signed_user_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['encoded_signed_user_key']))}
else
puts "No changes required for #{signed_user_key_file}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
# Update SSH daemon files
elsif (mode == "-d")
# Requres root privilege
if (Process.uid != 0)
puts "Root privilege required for this functionality"
usage()
else
response = RestClient.post "#{base_api_uri}#{daemon_update_route}", :auth_token => auth_token
response_hash = JSON.parse response
if ( response_hash['status'] == "Success")
puts "Creating #{known_hosts_file}."
File.open(known_hosts_file, 'w') { |file| file.write(Base64.decode64(response_hash['known_hosts']))}
trusted_user_ca_key = Base64.decode64(response_hash['trusted_user_ca_public_key'])
hash_trusted_user_ca_key = Digest::SHA256.hexdigest trusted_user_ca_key
# hash the user_ca file if it exists
hash_trusted_user_ca_key_file = 0
if (File.file?(trusted_user_ca_key_file))
hash_trusted_user_ca_key_file = (Digest::SHA256.file trusted_user_ca_key_file).hexdigest
end
# is the new user_ca key different from the one we already have
if (hash_trusted_user_ca_key != hash_trusted_user_ca_key_file)
if (hash_trusted_user_ca_key_file == 0)
puts "Creating #{trusted_user_ca_key_file}"
else
puts "Overwriting #{trusted_user_ca_key_file}"
end
File.open(trusted_user_ca_key_file, 'w') { |file| file.write(Base64.decode64(response_hash['trusted_user_ca_public_key']))}
else
puts "No changes required for #{trusted_user_ca_key_file}"
end
if File.readlines(sshd_config).grep(/TrustedUserCAKeys\s+#{trusted_user_ca_key_file}/).size <= 0
FileUtils.cp sshd_config, sshd_config_backup
File.open(sshd_config,'a') { |f| f.write("TrustedUserCAKeys #{trusted_user_ca_key_file}") }
puts "Please restart sshd."
else
puts "No changes required for #{sshd_config}"
end
else
puts response_hash['status']
puts response_hash['status_message']
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment