Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ gem 'sequel', '~> 5.97'
gem 'sequel_pg', require: 'sequel'
gem 'sinatra', '~> 3.2'
gem 'sinatra-contrib'
gem 'sshkey'
gem 'statsd-ruby', '~> 1.5.0'
gem 'steno'
gem 'talentbox-delayed_job_sequel', '~> 4.3.0'
Expand Down
2 changes: 0 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,6 @@ GEM
spring (4.4.0)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sshkey (3.0.0)
statsd-ruby (1.5.0)
steno (1.3.5)
fluent-logger
Expand Down Expand Up @@ -710,7 +709,6 @@ DEPENDENCIES
spork!
spring
spring-commands-rspec
sshkey
statsd-ruby (~> 1.5.0)
steno
talentbox-delayed_job_sequel (~> 4.3.0)
Expand Down
35 changes: 29 additions & 6 deletions lib/cloud_controller/diego/ssh_key.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'net/ssh'
require 'sshkey'
require 'openssl'
require 'digest'
require 'base64'

module VCAP
module CloudController
Expand All @@ -15,26 +17,47 @@ def private_key

def authorized_key
@authorized_key ||= begin
type = key.ssh_type
data = [key.to_blob].pack('m0')

type = ssh_type_for(key)
data = [public_key_blob].pack('m0') # Base64 without newlines
"#{type} #{data}"
end
end

def fingerprint
@fingerprint ||= ::SSHKey.new(key.to_der).sha1_fingerprint
@fingerprint ||= colon_hex(OpenSSL::Digest::SHA1.digest(public_key_blob)) # 3)
end

def sha256_fingerprint
@sha256_fingerprint ||= ::SSHKey.new(key.to_der).sha256_fingerprint
@sha256_fingerprint ||= Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(public_key_blob))
end

private

def key
@key ||= OpenSSL::PKey::RSA.new(@bits)
end

def public_key_blob
@public_key_blob ||= begin
b = Net::SSH::Buffer.new
b.write_string(ssh_type_for(key)) # key type
b.write_bignum(key.e) # public exponent (e)
b.write_bignum(key.n) # modulus (n)
b.to_s
end
end

def ssh_type_for(key)
case key
when OpenSSL::PKey::RSA then 'ssh-rsa' # net-ssh doesn’t publish a constant for this
else
raise NotImplementedError.new("Unsupported key type: #{key.class}")
end
end

def colon_hex(bytes)
bytes.unpack('C*').map { |b| sprintf('%02x', b) }.join(':') # byte-wise hex with colons
end
end
end
end
Expand Down
53 changes: 47 additions & 6 deletions spec/unit/lib/cloud_controller/diego/ssh_key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,48 @@
module VCAP::CloudController
module Diego
RSpec.describe SSHKey do
let(:ssh_key) { SSHKey.new(1024) }

describe '#authorized_key' do
it 'returns an open ssh format authorized key' do
ssh_key = SSHKey.new(1024)
expect(ssh_key.authorized_key).to match(/\Assh-rsa .{200,}\Z/)
end

it 'does not change' do
ssh_key = SSHKey.new(1024)
key1 = ssh_key.authorized_key
key2 = ssh_key.authorized_key
expect(key1).to eq(key2)
end

it 'has no newlines and decodes to a valid blob matching the key' do
ssh_key = VCAP::CloudController::Diego::SSHKey.new(1024)

ak = ssh_key.authorized_key
expect(ak).not_to include("\n")

type, b64 = ak.split(' ', 2)
expect(type).to eq('ssh-rsa')

blob = Base64.strict_decode64(b64)
blob_buffer = Net::SSH::Buffer.new(blob)
blob_type = blob_buffer.read_string
e = blob_buffer.read_bignum # public exponent
n = blob_buffer.read_bignum # modulus
expect(blob_type).to eq('ssh-rsa')

pk = OpenSSL::PKey::RSA.new(ssh_key.private_key)
expect(e).to eq(pk.e)
expect(n).to eq(pk.n)
end
end

describe '#private_key' do
it 'returns an open ssh format private key' do
ssh_key = SSHKey.new(1024)
expect(ssh_key.private_key).to start_with('-----BEGIN RSA PRIVATE KEY-----')
expect(ssh_key.private_key).to end_with("-----END RSA PRIVATE KEY-----\n")
end

it 'does not change' do
ssh_key = SSHKey.new(1024)
key1 = ssh_key.private_key
key2 = ssh_key.private_key
expect(key1).to eq(key2)
Expand All @@ -35,17 +54,39 @@ module Diego

describe '#fingerprint' do
it 'returns an sha1 fingerprint' do
ssh_key = SSHKey.new(1024)
expect(ssh_key.fingerprint).to match(/([0-9a-f]{2}:){19}[0-9a-f]{2}/)
end

it 'match digests over the authorized_key blob exactly' do
ssh_key = VCAP::CloudController::Diego::SSHKey.new(1024)

b64 = ssh_key.authorized_key.split(' ', 2).last
blob = Base64.strict_decode64(b64)

sha1 = OpenSSL::Digest::SHA1.digest(blob)
sha1_colon = sha1.unpack('C*').map { |b| sprintf('%02x', b) }.join(':')
expect(ssh_key.fingerprint).to eq(sha1_colon)

sha256 = OpenSSL::Digest::SHA256.digest(blob)
expect(ssh_key.sha256_fingerprint).to eq(Base64.strict_encode64(sha256))
end
end

describe '#fingerprint 256' do
it 'returns an sha256 fingerprint' do
ssh_key = SSHKey.new(1024)
expect(ssh_key.sha256_fingerprint).to match(%r{[a-zA-Z0-9+/=]{44}})
end
end

describe 'key generation' do
it 'produces different keys for different instances' do
a = VCAP::CloudController::Diego::SSHKey.new(1024)
b = VCAP::CloudController::Diego::SSHKey.new(1024)

expect(a.private_key).not_to eq(b.private_key)
expect(a.authorized_key).not_to eq(b.authorized_key)
end
end
end
end
end