Skip to content

Commit 003f49f

Browse files
authored
Re-implement SSHKey to remove sshkey gem dependency (#4643)
Refactor the `VCAP::CloudController::Diego::SSHKey` class to remove the dependency on the `sshkey` gem. This change reduces external dependencies and avoids potential compilation issues related to native extensions. The class is re-implemented using the standard `openssl` and `digest` libraries. The public interface and output formats of the `private_key`, `authorized_key`, `fingerprint` (SHA1), and `sha256_fingerprint` methods are preserved, ensuring it acts as a transparent, drop-in replacement. The performance impact is negligible as the most expensive operation, RSA key generation, still relies on OpenSSL, and all derived values are memoized.
1 parent 028e945 commit 003f49f

File tree

4 files changed

+76
-15
lines changed

4 files changed

+76
-15
lines changed

Gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ gem 'sequel', '~> 5.98'
3535
gem 'sequel_pg', require: 'sequel'
3636
gem 'sinatra', '~> 3.2'
3737
gem 'sinatra-contrib'
38-
gem 'sshkey'
3938
gem 'statsd-ruby', '~> 1.5.0'
4039
gem 'steno'
4140
gem 'talentbox-delayed_job_sequel', '~> 4.3.0'

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,6 @@ GEM
559559
spring (4.4.0)
560560
spring-commands-rspec (1.0.4)
561561
spring (>= 0.9.1)
562-
sshkey (3.0.0)
563562
statsd-ruby (1.5.0)
564563
steno (1.3.5)
565564
fluent-logger
@@ -690,7 +689,6 @@ DEPENDENCIES
690689
spork!
691690
spring
692691
spring-commands-rspec
693-
sshkey
694692
statsd-ruby (~> 1.5.0)
695693
steno
696694
talentbox-delayed_job_sequel (~> 4.3.0)

lib/cloud_controller/diego/ssh_key.rb

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
require 'net/ssh'
2-
require 'sshkey'
2+
require 'openssl'
3+
require 'digest'
4+
require 'base64'
35

46
module VCAP
57
module CloudController
@@ -15,26 +17,47 @@ def private_key
1517

1618
def authorized_key
1719
@authorized_key ||= begin
18-
type = key.ssh_type
19-
data = [key.to_blob].pack('m0')
20-
20+
type = ssh_type_for(key)
21+
data = [public_key_blob].pack('m0') # Base64 without newlines
2122
"#{type} #{data}"
2223
end
2324
end
2425

2526
def fingerprint
26-
@fingerprint ||= ::SSHKey.new(key.to_der).sha1_fingerprint
27+
@fingerprint ||= colon_hex(OpenSSL::Digest::SHA1.digest(public_key_blob)) # 3)
2728
end
2829

2930
def sha256_fingerprint
30-
@sha256_fingerprint ||= ::SSHKey.new(key.to_der).sha256_fingerprint
31+
@sha256_fingerprint ||= Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(public_key_blob))
3132
end
3233

3334
private
3435

3536
def key
3637
@key ||= OpenSSL::PKey::RSA.new(@bits)
3738
end
39+
40+
def public_key_blob
41+
@public_key_blob ||= begin
42+
b = Net::SSH::Buffer.new
43+
b.write_string(ssh_type_for(key)) # key type
44+
b.write_bignum(key.e) # public exponent (e)
45+
b.write_bignum(key.n) # modulus (n)
46+
b.to_s
47+
end
48+
end
49+
50+
def ssh_type_for(key)
51+
case key
52+
when OpenSSL::PKey::RSA then 'ssh-rsa' # net-ssh doesn’t publish a constant for this
53+
else
54+
raise NotImplementedError.new("Unsupported key type: #{key.class}")
55+
end
56+
end
57+
58+
def colon_hex(bytes)
59+
bytes.unpack('C*').map { |b| sprintf('%02x', b) }.join(':') # byte-wise hex with colons
60+
end
3861
end
3962
end
4063
end

spec/unit/lib/cloud_controller/diego/ssh_key_spec.rb

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,48 @@
44
module VCAP::CloudController
55
module Diego
66
RSpec.describe SSHKey do
7+
let(:ssh_key) { SSHKey.new(1024) }
8+
79
describe '#authorized_key' do
810
it 'returns an open ssh format authorized key' do
9-
ssh_key = SSHKey.new(1024)
1011
expect(ssh_key.authorized_key).to match(/\Assh-rsa .{200,}\Z/)
1112
end
1213

1314
it 'does not change' do
14-
ssh_key = SSHKey.new(1024)
1515
key1 = ssh_key.authorized_key
1616
key2 = ssh_key.authorized_key
1717
expect(key1).to eq(key2)
1818
end
19+
20+
it 'has no newlines and decodes to a valid blob matching the key' do
21+
ssh_key = VCAP::CloudController::Diego::SSHKey.new(1024)
22+
23+
ak = ssh_key.authorized_key
24+
expect(ak).not_to include("\n")
25+
26+
type, b64 = ak.split(' ', 2)
27+
expect(type).to eq('ssh-rsa')
28+
29+
blob = Base64.strict_decode64(b64)
30+
blob_buffer = Net::SSH::Buffer.new(blob)
31+
blob_type = blob_buffer.read_string
32+
e = blob_buffer.read_bignum # public exponent
33+
n = blob_buffer.read_bignum # modulus
34+
expect(blob_type).to eq('ssh-rsa')
35+
36+
pk = OpenSSL::PKey::RSA.new(ssh_key.private_key)
37+
expect(e).to eq(pk.e)
38+
expect(n).to eq(pk.n)
39+
end
1940
end
2041

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

2848
it 'does not change' do
29-
ssh_key = SSHKey.new(1024)
3049
key1 = ssh_key.private_key
3150
key2 = ssh_key.private_key
3251
expect(key1).to eq(key2)
@@ -35,17 +54,39 @@ module Diego
3554

3655
describe '#fingerprint' do
3756
it 'returns an sha1 fingerprint' do
38-
ssh_key = SSHKey.new(1024)
3957
expect(ssh_key.fingerprint).to match(/([0-9a-f]{2}:){19}[0-9a-f]{2}/)
4058
end
59+
60+
it 'match digests over the authorized_key blob exactly' do
61+
ssh_key = VCAP::CloudController::Diego::SSHKey.new(1024)
62+
63+
b64 = ssh_key.authorized_key.split(' ', 2).last
64+
blob = Base64.strict_decode64(b64)
65+
66+
sha1 = OpenSSL::Digest::SHA1.digest(blob)
67+
sha1_colon = sha1.unpack('C*').map { |b| sprintf('%02x', b) }.join(':')
68+
expect(ssh_key.fingerprint).to eq(sha1_colon)
69+
70+
sha256 = OpenSSL::Digest::SHA256.digest(blob)
71+
expect(ssh_key.sha256_fingerprint).to eq(Base64.strict_encode64(sha256))
72+
end
4173
end
4274

4375
describe '#fingerprint 256' do
4476
it 'returns an sha256 fingerprint' do
45-
ssh_key = SSHKey.new(1024)
4677
expect(ssh_key.sha256_fingerprint).to match(%r{[a-zA-Z0-9+/=]{44}})
4778
end
4879
end
80+
81+
describe 'key generation' do
82+
it 'produces different keys for different instances' do
83+
a = VCAP::CloudController::Diego::SSHKey.new(1024)
84+
b = VCAP::CloudController::Diego::SSHKey.new(1024)
85+
86+
expect(a.private_key).not_to eq(b.private_key)
87+
expect(a.authorized_key).not_to eq(b.authorized_key)
88+
end
89+
end
4990
end
5091
end
5192
end

0 commit comments

Comments
 (0)