Skip to content

Commit 60b9caf

Browse files
committed
Move the Ed25519 implementation to OpenSSL and stop depending on the Ed25519 gem
1 parent beb47bb commit 60b9caf

File tree

10 files changed

+76
-140
lines changed

10 files changed

+76
-140
lines changed

Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ GEM
1414
coderay (1.1.3)
1515
debug_inspector (1.2.0)
1616
diff-lcs (1.5.0)
17-
ed25519 (1.3.0)
1817
method_source (1.0.0)
1918
parser (3.2.2.4)
2019
ast (~> 2.4.1)
@@ -59,7 +58,6 @@ PLATFORMS
5958
ruby
6059

6160
DEPENDENCIES
62-
ed25519 (~> 1.2)
6361
pry (~> 0.14)
6462
rspec (~> 3.10)
6563
rspec-mocks (~> 3.10)

README.md

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This is a Ruby library for processing SSH keys and certificates.
44

5-
The scope of this project is limited to processing and directly using keys and certificates. It can be used to generate SSH private keys, verify signatures using public keys, sign data using private keys, issue certificates using private keys, and parse certificates and public and private keys. This library supports RSA, DSA, ECDSA, and ED25519<sup>[*](#ed25519-support)</sup> keys. This library does not offer or intend to offer functionality for SSH connectivity, processing of SSH wire protocol data, or processing of other key formats or types.
5+
The scope of this project is limited to processing and directly using keys and certificates. It can be used to generate SSH private keys, verify signatures using public keys, sign data using private keys, issue certificates using private keys, and parse certificates and public and private keys. This library supports RSA, DSA, ECDSA, and ED25519 keys. This library does not offer or intend to offer functionality for SSH connectivity, processing of SSH wire protocol data, or processing of other key formats or types.
66

77
**Project Status:** Used by @github in production
88

@@ -32,26 +32,6 @@ cert.public_key
3232
#=> <SSHData::PublicKey::RSA>
3333
```
3434

35-
## ED25519 support
36-
37-
Ruby's standard library does not include support for ED25519, though the algorithm is implemented by the [`ed25519` Gem](https://rubygems.org/gems/ed25519). This library can parse ED25519 public and private keys itself, but in order to generate keys or sign or verify messages, the calling application must load the `ed25519` Gem itself. This avoids the necessity of installing or loading this third party dependency when the calling application is only interested in parsing keys.
38-
39-
```ruby
40-
require "ssh_data"
41-
42-
key_data = File.read("~/.ssh/id_ed25519")
43-
key = SSHData::PrivateKey.parse_openssh(key_data)
44-
#=> <SSHData::PrivateKey::ED25519>
45-
46-
SSHData::PrivateKey::ED25519.generate
47-
#=> raises SSHData::AlgorithmError
48-
49-
require "ed25519"
50-
51-
SSHData::PrivateKey::ED25519.generate
52-
#=> <SSHData::PrivateKey::ED25519>
53-
```
54-
5535
## Contributions
5636

5737
This project is not currently seeking contributions for new features or functionality, though bug fixes are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for more information.

lib/ssh_data/private_key/ed25519.rb

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
module SSHData
22
module PrivateKey
33
class ED25519 < Base
4-
attr_reader :pk, :sk, :ed25519_key
4+
attr_reader :pk, :sk, :openssl
55

66
# Generate a new private key.
77
#
88
# Returns a PublicKey::Base subclass instance.
99
def self.generate
10-
PublicKey::ED25519.ed25519_gem_required!
11-
from_ed25519(Ed25519::SigningKey.generate)
10+
from_openssl(OpenSSL::PKey.generate_key("ED25519"))
1211
end
1312

14-
# Create from a ::Ed25519::SigningKey instance.
13+
# Create from a ::OpenSSL::PKey::PKey instance.
1514
#
16-
# key - A ::Ed25519::SigningKey instance.
15+
# key - A ::OpenSSL::PKey::PKey instance.
1716
#
1817
# Returns a ED25519 instance.
19-
def self.from_ed25519(key)
18+
def self.from_openssl(key)
2019
new(
2120
algo: PublicKey::ALGO_ED25519,
22-
pk: key.verify_key.to_bytes,
23-
sk: key.to_bytes + key.verify_key.to_bytes,
21+
pk: key.raw_public_key,
22+
sk: key.raw_private_key + key.raw_public_key,
2423
comment: "",
2524
)
2625
end
@@ -40,12 +39,10 @@ def initialize(algo:, pk:, sk:, comment:)
4039

4140
super(algo: algo, comment: comment)
4241

43-
if PublicKey::ED25519.enabled?
44-
@ed25519_key = Ed25519::SigningKey.new(sk.byteslice(0, 32))
42+
@openssl = OpenSSL::PKey.read(raw_to_private_key_info_der(sk.byteslice(0, 32)))
4543

46-
if @ed25519_key.verify_key.to_bytes != pk
47-
raise DecodeError, "bad pk"
48-
end
44+
if @openssl.raw_public_key != pk
45+
raise DecodeError, "bad pk"
4946
end
5047

5148
@public_key = PublicKey::ED25519.new(algo: algo, pk: pk)
@@ -57,12 +54,24 @@ def initialize(algo:, pk:, sk:, comment:)
5754
#
5855
# Returns a binary String signature.
5956
def sign(signed_data, algo: nil)
60-
PublicKey::ED25519.ed25519_gem_required!
6157
algo ||= self.algo
6258
raise AlgorithmError unless algo == self.algo
63-
raw_sig = ed25519_key.sign(signed_data)
59+
raw_sig = openssl.sign(nil, signed_data)
6460
Encoding.encode_signature(algo, raw_sig)
6561
end
62+
63+
private
64+
65+
def raw_to_private_key_info_der(key)
66+
inner_octet_string = OpenSSL::ASN1::OctetString.new(key)
67+
private_key_field = OpenSSL::ASN1::OctetString.new(inner_octet_string.to_der)
68+
version = OpenSSL::ASN1::Integer.new(0)
69+
OpenSSL::ASN1::Sequence.new([
70+
version,
71+
PublicKey::ED25519.asn_algorithm_identifier,
72+
private_key_field
73+
]).to_der
74+
end
6675
end
6776
end
6877
end

lib/ssh_data/public_key/ed25519.rb

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
module SSHData
22
module PublicKey
33
class ED25519 < Base
4-
attr_reader :pk, :ed25519_key
4+
attr_reader :pk, :openssl
55

6-
# ed25519 isn't a hard requirement for using this Gem. We only do actual
7-
# validation with the key if the ed25519 Gem has been loaded.
8-
def self.enabled?
9-
Object.const_defined?(:Ed25519)
10-
end
6+
@@alg_id = OpenSSL::ASN1::Sequence([
7+
OpenSSL::ASN1::ObjectId("1.3.101.112") # id-Ed25519
8+
])
119

12-
# Assert that the ed25519 gem has been loaded.
13-
#
14-
# Returns nothing, raises AlgorithmError.
15-
def self.ed25519_gem_required!
16-
raise AlgorithmError, "the ed25519 gem is not loaded" unless enabled?
10+
def self.asn_algorithm_identifier
11+
@@alg_id
1712
end
1813

1914
def self.algorithm_identifier
@@ -26,10 +21,7 @@ def initialize(algo:, pk:)
2621
end
2722

2823
@pk = pk
29-
30-
if self.class.enabled?
31-
@ed25519_key = Ed25519::VerifyKey.new(pk)
32-
end
24+
@openssl = OpenSSL::PKey.read(raw_to_subject_public_key_info_der(pk))
3325

3426
super(algo: algo)
3527
end
@@ -41,18 +33,12 @@ def initialize(algo:, pk:)
4133
#
4234
# Returns boolean.
4335
def verify(signed_data, signature)
44-
self.class.ed25519_gem_required!
45-
4636
sig_algo, raw_sig, _ = Encoding.decode_signature(signature)
4737
if sig_algo != self.class.algorithm_identifier
4838
raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
4939
end
5040

51-
begin
52-
ed25519_key.verify(raw_sig, signed_data)
53-
rescue Ed25519::VerifyError
54-
false
55-
end
41+
return openssl.verify(nil, raw_sig, signed_data)
5642
end
5743

5844
# RFC4253 binary encoding of the public key.
@@ -73,6 +59,15 @@ def rfc4253
7359
def ==(other)
7460
super && other.pk == pk
7561
end
62+
63+
private
64+
65+
def raw_to_subject_public_key_info_der(key)
66+
OpenSSL::ASN1::Sequence([
67+
@@alg_id,
68+
OpenSSL::ASN1::BitString.new(key)
69+
]).to_der
70+
end
7671
end
7772
end
7873
end

lib/ssh_data/public_key/sked25519.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ def rfc4253
2525
end
2626

2727
def verify(signed_data, signature, **opts)
28-
self.class.ed25519_gem_required!
2928
opts = DEFAULT_SK_VERIFY_OPTS.merge(opts)
3029
unknown_opts = opts.keys - DEFAULT_SK_VERIFY_OPTS.keys
3130
raise UnsupportedError, "Verification options #{unknown_opts.inspect} are not supported." unless unknown_opts.empty?
@@ -36,12 +35,7 @@ def verify(signed_data, signature, **opts)
3635
raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}"
3736
end
3837

39-
result = begin
40-
ed25519_key.verify(raw_sig, blob)
41-
rescue Ed25519::VerifyError
42-
false
43-
end
44-
38+
result = openssl.verify(nil, raw_sig, blob)
4539
# We don't know that the flags are correct until after we've validated the signature
4640
# which embeds the flags, so always verify the signature first.
4741
return false if opts[:user_presence_required] && (sk_flags & SK_FLAG_USER_PRESENCE != SK_FLAG_USER_PRESENCE)

spec/private_key/ed25519_spec.rb

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
require_relative "../spec_helper"
22

33
describe SSHData::PrivateKey::ED25519 do
4-
let(:signing_key) { Ed25519::SigningKey.generate }
5-
let(:verify_key) { signing_key.verify_key }
4+
let(:signing_key) { OpenSSL::PKey.generate_key("ED25519") }
5+
let(:verify_key) { OpenSSL::PKey.read(signing_key.public_to_pem) }
6+
67
let(:comment) { "asdf" }
78
let(:message) { "hello, world!" }
89
let(:cert_key) { SSHData::PrivateKey::DSA.generate.public_key }
@@ -12,8 +13,8 @@
1213
subject do
1314
described_class.new(
1415
algo: SSHData::PublicKey::ALGO_ED25519,
15-
pk: verify_key.to_bytes,
16-
sk: signing_key.to_bytes + verify_key.to_bytes,
16+
pk: verify_key.raw_public_key,
17+
sk: signing_key.raw_private_key + verify_key.raw_public_key,
1718
comment: comment,
1819
)
1920
end
@@ -54,22 +55,22 @@
5455
end
5556

5657
it "has params" do
57-
expect(subject.pk).to eq(verify_key.to_bytes)
58-
expect(subject.sk).to eq(signing_key.to_bytes + verify_key.to_bytes)
58+
expect(subject.pk).to eq(verify_key.raw_public_key)
59+
expect(subject.sk).to eq(signing_key.raw_private_key + verify_key.raw_public_key)
5960
end
6061

6162
it "has a comment" do
6263
expect(subject.comment).to eq(comment)
6364
end
6465

65-
it "has an Ed25519 representation" do
66-
expect(subject.ed25519_key).to be_a(Ed25519::SigningKey)
67-
expect(subject.ed25519_key.to_bytes).to eq(signing_key.to_bytes)
66+
it "has an PKey representation" do
67+
expect(subject.openssl).to be_a(OpenSSL::PKey::PKey)
68+
expect(subject.openssl.raw_private_key).to eq(signing_key.raw_private_key)
6869
end
6970

7071
it "has a public key" do
71-
expect(subject.public_key).to be_a(SSHData::PublicKey::ED25519)
72-
expect(subject.public_key.ed25519_key.to_bytes).to eq(verify_key.to_bytes)
72+
expect(subject.openssl).to be_a(OpenSSL::PKey::PKey)
73+
expect(subject.public_key.openssl.raw_public_key).to eq(verify_key.raw_public_key)
7374
end
7475

7576
it "can parse openssh-generate keys" do
@@ -78,27 +79,4 @@
7879
expect(keys.size).to eq(1)
7980
expect(keys.first).to be_an(SSHData::PrivateKey::ED25519)
8081
end
81-
82-
it "fails cleanly if the ed25519 gem hasn't been loaded" do
83-
backup = Object.send(:remove_const, :Ed25519)
84-
key = openssh_key.first
85-
86-
begin
87-
expect {
88-
expect(key.sk).not_to be_nil
89-
expect(key.pk).not_to be_nil
90-
expect(key.public_key).not_to be_nil
91-
}.not_to raise_error
92-
93-
expect {
94-
described_class.generate
95-
}.to raise_error(SSHData::AlgorithmError)
96-
97-
expect {
98-
key.sign(message)
99-
}.to raise_error(SSHData::AlgorithmError)
100-
ensure
101-
Object.const_set(:Ed25519, backup)
102-
end
103-
end
10482
end

spec/public_key/ed25519_spec.rb

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
require_relative "../spec_helper"
22

33
describe SSHData::PublicKey::ED25519 do
4-
let(:signing_key) { Ed25519::SigningKey.generate }
5-
let(:verify_key) { signing_key.verify_key }
4+
let(:signing_key) { OpenSSL::PKey.generate_key("ED25519") }
5+
let(:verify_key) { OpenSSL::PKey.read(signing_key.public_to_pem) }
66

77
let(:msg) { "hello, world!" }
8-
let(:raw_sig) { signing_key.sign(msg) }
8+
let(:raw_sig) { signing_key.sign(nil, msg) }
99
let(:sig) { SSHData::Encoding.encode_signature(SSHData::PublicKey::ALGO_ED25519, raw_sig) }
1010

1111
let(:openssh_key) { SSHData::PublicKey.parse_openssh(fixture("ed25519_leaf_for_rsa_ca.pub")) }
1212

1313
subject do
1414
described_class.new(
1515
algo: SSHData::PublicKey::ALGO_ED25519,
16-
pk: verify_key.to_bytes
16+
pk: verify_key.raw_public_key
1717
)
1818
end
1919

2020
it "is equal to keys with the same params" do
2121
expect(subject).to eq(described_class.new(
2222
algo: SSHData::PublicKey::ALGO_ED25519,
23-
pk: verify_key.to_bytes
23+
pk: verify_key.raw_public_key
2424
))
2525
end
2626

2727
it "isnt equal to keys with different params" do
2828
expect(subject).not_to eq(described_class.new(
2929
algo: SSHData::PublicKey::ALGO_ED25519,
30-
pk: verify_key.to_bytes.reverse
30+
pk: verify_key.raw_public_key.reverse
3131
))
3232
end
3333

@@ -36,12 +36,12 @@
3636
end
3737

3838
it "has parameters" do
39-
expect(subject.pk).to eq(verify_key.to_bytes)
39+
expect(subject.pk).to eq(verify_key.raw_public_key)
4040
end
4141

42-
it "has an Ed25519 representation" do
43-
expect(subject.ed25519_key).to be_a(Ed25519::VerifyKey)
44-
expect(subject.ed25519_key.to_bytes).to eq(verify_key.to_bytes)
42+
it "has a pkey representation" do
43+
expect(subject.openssl).to be_a(OpenSSL::PKey::PKey)
44+
expect(subject.openssl.raw_public_key).to eq(verify_key.raw_public_key)
4545
end
4646

4747
it "can verify signatures" do
@@ -50,7 +50,7 @@
5050
end
5151

5252
it "can parse openssh-generate keys" do
53-
expect { openssh_key.ed25519_key }.not_to raise_error
53+
expect { openssh_key.openssl }.not_to raise_error
5454
end
5555

5656
it "can be rencoded" do
@@ -64,20 +64,4 @@
6464
)
6565
}.not_to raise_error
6666
end
67-
68-
it "fails cleanly if the ed25519 gem hasn't been loaded" do
69-
expect(described_class.enabled?).to be(true)
70-
backup = Object.send(:remove_const, :Ed25519)
71-
expect(described_class.enabled?).to be(false)
72-
73-
begin
74-
expect {
75-
SSHData::Certificate.parse_openssh(fixture("rsa_leaf_for_ed25519_ca-cert.pub"),
76-
unsafe_no_verify: false
77-
)
78-
}.to raise_error(SSHData::AlgorithmError)
79-
ensure
80-
Object.const_set(:Ed25519, backup)
81-
end
82-
end
8367
end

0 commit comments

Comments
 (0)