From ce06217fed1ccf5aabc3269560bb3313155118db Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 12:04:21 -0700 Subject: [PATCH 01/10] rename PublicKey::Base#raw to #rfc4253 --- lib/ssh_data/public_key/base.rb | 10 +++++----- lib/ssh_data/public_key/dsa.rb | 4 ++-- lib/ssh_data/public_key/ecdsa.rb | 4 ++-- lib/ssh_data/public_key/ed25519.rb | 4 ++-- lib/ssh_data/public_key/rsa.rb | 4 ++-- spec/public_key/dsa_spec.rb | 2 +- spec/public_key/ecdsa_spec.rb | 2 +- spec/public_key/ed25519_spec.rb | 2 +- spec/public_key/rsa_spec.rb | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/ssh_data/public_key/base.rb b/lib/ssh_data/public_key/base.rb index ce16e43..e431c2b 100644 --- a/lib/ssh_data/public_key/base.rb +++ b/lib/ssh_data/public_key/base.rb @@ -16,10 +16,10 @@ def initialize(**kwargs) def fingerprint(md5: false) if md5 # colon separated, hex encoded md5 digest - OpenSSL::Digest::MD5.digest(raw).unpack("H2" * 16).join(":") + OpenSSL::Digest::MD5.digest(rfc4253).unpack("H2" * 16).join(":") else # base64 encoded sha256 digest with b64 padding stripped - Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(raw))[0...-1] + Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(rfc4253))[0...-1] end end @@ -33,10 +33,10 @@ def verify(signed_data, signature) raise "implement me" end - # Raw encoding of public key. + # RFC4253 binary encoding of public key. # # Returns a binary String. - def raw + def rfc4253 raise "implement me" end @@ -46,7 +46,7 @@ def raw # # Returns a String key. def openssh(comment: nil) - [algo, Base64.strict_encode64(raw), comment].compact.join(" ") + [algo, Base64.strict_encode64(rfc4253), comment].compact.join(" ") end # Is this public key equal to another public key? diff --git a/lib/ssh_data/public_key/dsa.rb b/lib/ssh_data/public_key/dsa.rb index 6a97b59..c39b598 100644 --- a/lib/ssh_data/public_key/dsa.rb +++ b/lib/ssh_data/public_key/dsa.rb @@ -80,10 +80,10 @@ def verify(signed_data, signature) openssl.verify(OpenSSL::Digest::SHA1.new, openssl_sig, signed_data) end - # Raw encoding of public key. + # RFC4253 binary encoding of public key. # # Returns a binary String. - def raw + def rfc4253 Encoding.encode_fields( [:string, algo], [:mpint, p], diff --git a/lib/ssh_data/public_key/ecdsa.rb b/lib/ssh_data/public_key/ecdsa.rb index 9f8c3ff..37e93c6 100644 --- a/lib/ssh_data/public_key/ecdsa.rb +++ b/lib/ssh_data/public_key/ecdsa.rb @@ -100,10 +100,10 @@ def verify(signed_data, signature) openssl.verify(digest.new, openssl_sig, signed_data) end - # Raw encoding of public key. + # RFC4253 binary encoding of public key. # # Returns a binary String. - def raw + def rfc4253 Encoding.encode_fields( [:string, algo], [:string, curve], diff --git a/lib/ssh_data/public_key/ed25519.rb b/lib/ssh_data/public_key/ed25519.rb index a7b9953..4aa37fc 100644 --- a/lib/ssh_data/public_key/ed25519.rb +++ b/lib/ssh_data/public_key/ed25519.rb @@ -46,10 +46,10 @@ def verify(signed_data, signature) end end - # Raw encoding of public key. + # RFC4253 binary encoding of public key. # # Returns a binary String. - def raw + def rfc4253 Encoding.encode_fields( [:string, algo], [:string, pk], diff --git a/lib/ssh_data/public_key/rsa.rb b/lib/ssh_data/public_key/rsa.rb index e83169e..3d52702 100644 --- a/lib/ssh_data/public_key/rsa.rb +++ b/lib/ssh_data/public_key/rsa.rb @@ -32,10 +32,10 @@ def verify(signed_data, signature) openssl.verify(OpenSSL::Digest::SHA1.new, raw_sig, signed_data) end - # Raw encoding of public key. + # RFC4253 binary encoding of public key. # # Returns a binary String. - def raw + def rfc4253 Encoding.encode_fields( [:string, algo], [:mpint, e], diff --git a/spec/public_key/dsa_spec.rb b/spec/public_key/dsa_spec.rb index 4d12373..dba9b1e 100644 --- a/spec/public_key/dsa_spec.rb +++ b/spec/public_key/dsa_spec.rb @@ -87,7 +87,7 @@ end it "can be rencoded" do - expect(openssh_key.raw).to eq(fixture("dsa_leaf_for_rsa_ca.pub", binary: true)) + expect(openssh_key.rfc4253).to eq(fixture("dsa_leaf_for_rsa_ca.pub", binary: true)) end it "can verify certificate signatures" do diff --git a/spec/public_key/ecdsa_spec.rb b/spec/public_key/ecdsa_spec.rb index fc0176b..c5e4fe4 100644 --- a/spec/public_key/ecdsa_spec.rb +++ b/spec/public_key/ecdsa_spec.rb @@ -8,7 +8,7 @@ end it "can be rencoded" do - expect(openssh_key.raw).to eq(fixture("ecdsa_leaf_for_rsa_ca.pub", binary: true)) + expect(openssh_key.rfc4253).to eq(fixture("ecdsa_leaf_for_rsa_ca.pub", binary: true)) end it "can verify certificate signatures" do diff --git a/spec/public_key/ed25519_spec.rb b/spec/public_key/ed25519_spec.rb index 0ada4e8..e17e0f7 100644 --- a/spec/public_key/ed25519_spec.rb +++ b/spec/public_key/ed25519_spec.rb @@ -54,7 +54,7 @@ end it "can be rencoded" do - expect(openssh_key.raw).to eq(fixture("ed25519_leaf_for_rsa_ca.pub", binary: true)) + expect(openssh_key.rfc4253).to eq(fixture("ed25519_leaf_for_rsa_ca.pub", binary: true)) end it "can verify certificate signatures" do diff --git a/spec/public_key/rsa_spec.rb b/spec/public_key/rsa_spec.rb index 5b4f930..5e21e03 100644 --- a/spec/public_key/rsa_spec.rb +++ b/spec/public_key/rsa_spec.rb @@ -60,7 +60,7 @@ end it "can be rencoded" do - expect(openssh_key.raw).to eq(fixture("rsa_leaf_for_rsa_ca.pub", binary: true)) + expect(openssh_key.rfc4253).to eq(fixture("rsa_leaf_for_rsa_ca.pub", binary: true)) end it "can verify certificate signatures" do From a2a17f933775fb9257fdf77538382405f2ff5325 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 13:45:26 -0700 Subject: [PATCH 02/10] move some encoding logic out of the Certificate module --- lib/ssh_data/certificate.rb | 21 +--- lib/ssh_data/encoding.rb | 144 +++++++++++++++++--------- spec/encoding_spec.rb | 201 ++++++++++++++++++++---------------- 3 files changed, 211 insertions(+), 155 deletions(-) diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index 97ef2a4..48b6bd6 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -59,20 +59,8 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false) end # Parse data into better types, where possible. - valid_after = Time.at(data.delete(:valid_after)) - valid_before = Time.at(data.delete(:valid_before)) - public_key = PublicKey.from_data(data.delete(:key_data)) - valid_principals, _ = Encoding.decode_strings(data.delete(:valid_principals)) - critical_options, _ = Encoding.decode_options(data.delete(:critical_options)) - extensions, _ = Encoding.decode_options(data.delete(:extensions)) - - # The signature key is encoded as a string, but we can parse it. - sk_raw = data.delete(:signature_key) - sk_data, read = Encoding.decode_public_key(sk_raw) - if read != sk_raw.bytesize - raise DecodeError, "unexpected trailing data" - end - ca_key = PublicKey.from_data(sk_data) + public_key = PublicKey.from_data(data.delete(:public_key)) + ca_key = PublicKey.from_data(data.delete(:signature_key)) unless unsafe_no_verify # The signature is the last field. The signature is calculated over all @@ -86,12 +74,7 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false) end new(**data.merge( - valid_after: valid_after, - valid_before: valid_before, public_key: public_key, - valid_principals: valid_principals, - critical_options: critical_options, - extensions: extensions, ca_key: ca_key, )) end diff --git a/lib/ssh_data/encoding.rb b/lib/ssh_data/encoding.rb index f8bd4a8..52ff72e 100644 --- a/lib/ssh_data/encoding.rb +++ b/lib/ssh_data/encoding.rb @@ -161,7 +161,7 @@ def decode_openssh_private_key(raw) raise DecryptError, "cannot decode encrypted private keys" end - data[:public_keys], read = decode_n_strings(raw, data[:nkeys], total_read) + data[:public_keys], read = decode_n_strings(raw, total_read, data[:nkeys]) total_read += read privs, read = decode_string(raw, total_read) @@ -244,7 +244,7 @@ def encode_signature(algo, signature) # # Returns an Array containing a Hash describing the public key and the # Integer number of bytes read. - def decode_public_key(raw, algo=nil, offset=0) + def decode_public_key(raw, offset=0, algo=nil) total_read = 0 if algo.nil? @@ -264,6 +264,27 @@ def decode_public_key(raw, algo=nil, offset=0) [data, total_read] end + # Decode the fields in a public key encoded as an SSH string. + # + # raw - Binary public key as described by RFC4253 section 6.6 wrapped in + # an SSH string.. + # algo - String public key algorithm identifier (optional). + # offset - Integer number of bytes into `raw` at which we should start + # reading. + # + # Returns an Array containing a Hash describing the public key and the + # Integer number of bytes read. + def decode_string_public_key(raw, offset=0, algo=nil) + key_raw, str_read = decode_string(raw, offset) + key, cert_read = decode_public_key(key_raw, 0, algo) + + if cert_read != key_raw.bytesize + raise DecodeError, "unexpected trailing data" + end + + [key, str_read] + end + # Decode the fields in a certificate. # # raw - Binary String certificate as described by RFC4253 section 6.6. @@ -275,35 +296,33 @@ def decode_public_key(raw, algo=nil, offset=0) def decode_certificate(raw, offset=0) total_read = 0 - data, read = decode_fields(raw, [ - [:algo, :string], - [:nonce, :string], - ], offset + total_read) + algo, read = decode_string(raw, offset + total_read) total_read += read - unless key_algo = PUBLIC_KEY_ALGO_BY_CERT_ALGO[data[:algo]] - raise AlgorithmError, "unknown algorithm: #{key_algo.inspect}" + unless key_algo = PUBLIC_KEY_ALGO_BY_CERT_ALGO[algo] + raise AlgorithmError, "unknown algorithm: #{algo.inspect}" end - data[:key_data], read = decode_public_key(raw, key_algo, offset + total_read) - total_read += read - - trailer, read = decode_fields(raw, [ + data, read = decode_fields(raw, [ + [:nonce, :string], + [:public_key, :public_key, key_algo], [:serial, :uint64], [:type, :uint32], [:key_id, :string], - [:valid_principals, :string], - [:valid_after, :uint64], - [:valid_before, :uint64], - [:critical_options, :string], - [:extensions, :string], + [:valid_principals, :list], + [:valid_after, :time], + [:valid_before, :time], + [:critical_options, :options], + [:extensions, :options], [:reserved, :string], - [:signature_key, :string], + [:signature_key, :string_public_key], [:signature, :string], ], offset + total_read) total_read += read - [data.merge(trailer), total_read] + data[:algo] = algo + + [data, total_read] end # Decode all of the given fields from raw. @@ -320,21 +339,29 @@ def decode_fields(raw, fields, offset=0) hash = {} total_read = 0 - fields.each do |key, type| - value, read = case type + fields.each do |key, type, *args| + hash[key], read = case type when :string - decode_string(raw, offset + total_read) + decode_string(raw, offset + total_read, *args) + when :list + decode_list(raw, offset + total_read, *args) when :mpint - decode_mpint(raw, offset + total_read) + decode_mpint(raw, offset + total_read, *args) + when :time + decode_time(raw, offset + total_read, *args) when :uint64 - decode_uint64(raw, offset + total_read) + decode_uint64(raw, offset + total_read, *args) when :uint32 - decode_uint32(raw, offset + total_read) + decode_uint32(raw, offset + total_read, *args) + when :public_key + decode_public_key(raw, offset + total_read, *args) + when :string_public_key + decode_string_public_key(raw, offset + total_read, *args) + when :options + decode_options(raw, offset + total_read, *args) else raise DecodeError end - - hash[key] = value total_read += read end @@ -405,28 +432,34 @@ def encode_string(value) # # Returns an Array including the Array of decoded Strings and the Integer # number of bytes read. - def decode_strings(raw, offset=0) - total_read = 0 - strs = [] + def decode_list(raw, offset=0) + list_raw, str_read = decode_string(raw, offset) - while raw.bytesize > offset + total_read - str, read = decode_string(raw, offset + total_read) - strs << str - total_read += read + list_read = 0 + list = [] + + while list_raw.bytesize > list_read + value, read = decode_string(list_raw, list_read) + list << value + list_read += read end - [strs, total_read] + if list_read != list_raw.bytesize + raise DecodeError, "bad strings list" + end + + [list, str_read] end # Read the specified number of strings out of the provided raw data. # # raw - A binary String. - # n - The Integer number of Strings to read. # offset - The offset into raw at which to read (default 0). + # n - The Integer number of Strings to read. # # Returns an Array including the Array of decoded Strings and the Integer # number of bytes read. - def decode_n_strings(raw, n, offset=0) + def decode_n_strings(raw, offset=0, n) total_read = 0 strs = [] @@ -440,20 +473,23 @@ def decode_n_strings(raw, n, offset=0) # Read a series of key/value pairs out of the provided raw data. # - # raw - A binary String. + # raw - A binary String. + # offset - The offset into raw at which to read (default 0). # # Returns an Array including the Hash of decoded keys/values and the Integer # number of bytes read. - def decode_options(raw) - total_read = 0 + def decode_options(raw, offset=0) + opts_raw, str_read = decode_string(raw, offset) + + opts_read = 0 opts = {} - while raw.bytesize > total_read - key, read = decode_string(raw, total_read) - total_read += read + while opts_raw.bytesize > opts_read + key, read = decode_string(opts_raw, opts_read) + opts_read += read - value_raw, read = decode_string(raw, total_read) - total_read += read + value_raw, read = decode_string(opts_raw, opts_read) + opts_read += read if value_raw.bytesize > 0 opts[key], read = decode_string(value_raw) @@ -465,7 +501,11 @@ def decode_options(raw) end end - [opts, total_read] + if opts_read != opts_raw.bytesize + raise DecodeError, "bad options" + end + + [opts, str_read] end # Read a multi-precision integer from the provided raw data. @@ -508,6 +548,18 @@ def encode_mpint(value) value.to_s(0) end + # Read a time from the provided raw data. + # + # raw - A binary String. + # offset - The offset into raw at which to read (default 0). + # + # Returns an Array including the decoded Time and the Integer number of + # bytes read. + def decode_time(raw, offset=0) + time_raw, read = decode_uint64(raw, offset) + [Time.at(time_raw), read] + end + # Read a uint64 from the provided raw data. # # raw - A binary String. diff --git a/spec/encoding_spec.rb b/spec/encoding_spec.rb index d24cbf4..b828e9a 100644 --- a/spec/encoding_spec.rb +++ b/spec/encoding_spec.rb @@ -280,7 +280,7 @@ raw = fixture("rsa_leaf_for_rsa_ca.pub", binary: true) with_prefix = [prefix, raw].join - data, _ = described_class.decode_public_key(with_prefix, nil, prefix.bytesize).first + data, _ = described_class.decode_public_key(with_prefix, prefix.bytesize, nil).first expect(data).to eq(rsa_data) end @@ -288,7 +288,7 @@ it "can skip the algo when decoding a public key" do raw = fixture("rsa_leaf_for_rsa_ca.pub", binary: true) algo, offset = described_class.decode_string(raw) - data, _ = described_class.decode_public_key(raw, algo, offset) + data, _ = described_class.decode_public_key(raw, offset, algo) expect(data).to eq(rsa_data) end @@ -319,6 +319,17 @@ end end + describe("#decode_string_public_key") do + let(:string_public_key) { described_class.encode_string(fixture("rsa_leaf_for_rsa_ca.pub", binary: true)) } + let(:public_key) { described_class.decode_public_key(fixture("rsa_leaf_for_rsa_ca.pub", binary: true)).first } + + subject { described_class.decode_string_public_key(string_public_key).first } + + it "matches normal decoding" do + expect(subject).to eq(public_key) + end + end + describe("#decode_certificate") do let(:rsa_data) { described_class.decode_certificate(fixture("rsa_leaf_for_rsa_ca-cert.pub", binary: true)).first } let(:dsa_data) { described_class.decode_certificate(fixture("dsa_leaf_for_rsa_ca-cert.pub", binary: true)).first } @@ -357,22 +368,21 @@ expect(rsa_data[:nonce]).to be_a(String) expect(rsa_data[:nonce].length).to eq(32) - expect(rsa_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) - expect(rsa_data[:key_data][:e]).to be_a(OpenSSL::BN) - expect(rsa_data[:key_data][:n]).to be_a(OpenSSL::BN) + expect(rsa_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) + expect(rsa_data[:public_key][:e]).to be_a(OpenSSL::BN) + expect(rsa_data[:public_key][:n]).to be_a(OpenSSL::BN) expect(rsa_data[:serial]).to eq(123) expect(rsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(rsa_data[:key_id]).to eq("my-ident") - expect(rsa_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(rsa_data[:valid_after]).to eq(0) - expect(rsa_data[:valid_before]).to eq((2**64)-1) - expect(rsa_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(rsa_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(rsa_data[:valid_principals]).to eq(["p1", "p2"]) + expect(rsa_data[:valid_after]).to eq(Time.at(0)) + expect(rsa_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(rsa_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(rsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(rsa_data[:reserved]).to eq("") - expect(rsa_data[:signature_key]).to be_a(String) - expect(rsa_data[:signature_key].bytesize).to eq(279) + expect(rsa_data[:signature_key]).to be_a(Hash) expect(rsa_data[:signature]).to be_a(String) expect(rsa_data[:signature].bytesize).to eq(271) @@ -384,24 +394,23 @@ expect(dsa_data[:nonce]).to be_a(String) expect(dsa_data[:nonce].length).to eq(32) - expect(dsa_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_DSA) - expect(dsa_data[:key_data][:p]).to be_a(OpenSSL::BN) - expect(dsa_data[:key_data][:q]).to be_a(OpenSSL::BN) - expect(dsa_data[:key_data][:g]).to be_a(OpenSSL::BN) - expect(dsa_data[:key_data][:y]).to be_a(OpenSSL::BN) + expect(dsa_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_DSA) + expect(dsa_data[:public_key][:p]).to be_a(OpenSSL::BN) + expect(dsa_data[:public_key][:q]).to be_a(OpenSSL::BN) + expect(dsa_data[:public_key][:g]).to be_a(OpenSSL::BN) + expect(dsa_data[:public_key][:y]).to be_a(OpenSSL::BN) expect(dsa_data[:serial]).to eq(123) expect(dsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(dsa_data[:key_id]).to eq("my-ident") - expect(dsa_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(dsa_data[:valid_after]).to eq(0) - expect(dsa_data[:valid_before]).to eq((2**64)-1) - expect(dsa_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(dsa_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(dsa_data[:valid_principals]).to eq(["p1", "p2"]) + expect(dsa_data[:valid_after]).to eq(Time.at(0)) + expect(dsa_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(dsa_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(dsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(dsa_data[:reserved]).to eq("") - expect(dsa_data[:signature_key]).to be_a(String) - expect(dsa_data[:signature_key].bytesize).to eq(279) + expect(dsa_data[:signature_key]).to be_a(Hash) expect(dsa_data[:signature]).to be_a(String) expect(dsa_data[:signature].bytesize).to eq(271) @@ -413,22 +422,21 @@ expect(ecdsa_data[:nonce]).to be_a(String) expect(ecdsa_data[:nonce].length).to eq(32) - expect(ecdsa_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_ECDSA256) - expect(ecdsa_data[:key_data][:curve]).to eq("nistp256") - expect(ecdsa_data[:key_data][:public_key]).to be_a(String) + expect(ecdsa_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_ECDSA256) + expect(ecdsa_data[:public_key][:curve]).to eq("nistp256") + expect(ecdsa_data[:public_key][:public_key]).to be_a(String) expect(ecdsa_data[:serial]).to eq(123) expect(ecdsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ecdsa_data[:key_id]).to eq("my-ident") - expect(ecdsa_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(ecdsa_data[:valid_after]).to eq(0) - expect(ecdsa_data[:valid_before]).to eq((2**64)-1) - expect(ecdsa_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ecdsa_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(ecdsa_data[:valid_principals]).to eq(["p1", "p2"]) + expect(ecdsa_data[:valid_after]).to eq(Time.at(0)) + expect(ecdsa_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ecdsa_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(ecdsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ecdsa_data[:reserved]).to eq("") - expect(ecdsa_data[:signature_key]).to be_a(String) - expect(ecdsa_data[:signature_key].bytesize).to eq(279) + expect(ecdsa_data[:signature_key]).to be_a(Hash) expect(ecdsa_data[:signature]).to be_a(String) expect(ecdsa_data[:signature].bytesize).to eq(271) @@ -440,21 +448,20 @@ expect(ed25519_data[:nonce]).to be_a(String) expect(ed25519_data[:nonce].length).to eq(32) - expect(ed25519_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_ED25519) - expect(ed25519_data[:key_data][:pk]).to be_a(String) + expect(ed25519_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_ED25519) + expect(ed25519_data[:public_key][:pk]).to be_a(String) expect(ed25519_data[:serial]).to eq(123) expect(ed25519_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ed25519_data[:key_id]).to eq("my-ident") - expect(ed25519_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(ed25519_data[:valid_after]).to eq(0) - expect(ed25519_data[:valid_before]).to eq((2**64)-1) - expect(ed25519_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ed25519_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(ed25519_data[:valid_principals]).to eq(["p1", "p2"]) + expect(ed25519_data[:valid_after]).to eq(Time.at(0)) + expect(ed25519_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ed25519_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(ed25519_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ed25519_data[:reserved]).to eq("") - expect(ed25519_data[:signature_key]).to be_a(String) - expect(ed25519_data[:signature_key].bytesize).to eq(279) + expect(ed25519_data[:signature_key]).to be_a(Hash) expect(ed25519_data[:signature]).to be_a(String) expect(ed25519_data[:signature].bytesize).to eq(271) @@ -466,22 +473,21 @@ expect(rsa_ca_data[:nonce]).to be_a(String) expect(rsa_ca_data[:nonce].length).to eq(32) - expect(rsa_ca_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) - expect(rsa_ca_data[:key_data][:e]).to be_a(OpenSSL::BN) - expect(rsa_ca_data[:key_data][:n]).to be_a(OpenSSL::BN) + expect(rsa_ca_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) + expect(rsa_ca_data[:public_key][:e]).to be_a(OpenSSL::BN) + expect(rsa_ca_data[:public_key][:n]).to be_a(OpenSSL::BN) expect(rsa_ca_data[:serial]).to eq(123) expect(rsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(rsa_ca_data[:key_id]).to eq("my-ident") - expect(rsa_ca_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(rsa_ca_data[:valid_after]).to eq(0) - expect(rsa_ca_data[:valid_before]).to eq((2**64)-1) - expect(rsa_ca_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(rsa_ca_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(rsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) + expect(rsa_ca_data[:valid_after]).to eq(Time.at(0)) + expect(rsa_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(rsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(rsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(rsa_ca_data[:reserved]).to eq("") - expect(rsa_ca_data[:signature_key]).to be_a(String) - expect(rsa_ca_data[:signature_key].bytesize).to eq(279) + expect(rsa_ca_data[:signature_key]).to be_a(Hash) expect(rsa_ca_data[:signature]).to be_a(String) expect(rsa_ca_data[:signature].bytesize).to eq(271) @@ -493,21 +499,21 @@ expect(dsa_ca_data[:nonce]).to be_a(String) expect(dsa_ca_data[:nonce].length).to eq(32) - expect(dsa_ca_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) - expect(dsa_ca_data[:key_data][:e]).to be_a(OpenSSL::BN) - expect(dsa_ca_data[:key_data][:n]).to be_a(OpenSSL::BN) + expect(dsa_ca_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) + expect(dsa_ca_data[:public_key][:e]).to be_a(OpenSSL::BN) + expect(dsa_ca_data[:public_key][:n]).to be_a(OpenSSL::BN) expect(dsa_ca_data[:serial]).to eq(123) expect(dsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(dsa_ca_data[:key_id]).to eq("my-ident") - expect(dsa_ca_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(dsa_ca_data[:valid_after]).to eq(0) - expect(dsa_ca_data[:valid_before]).to eq((2**64)-1) - expect(dsa_ca_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(dsa_ca_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(dsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) + expect(dsa_ca_data[:valid_after]).to eq(Time.at(0)) + expect(dsa_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(dsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(dsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(dsa_ca_data[:reserved]).to eq("") - expect(dsa_ca_data[:signature_key]).to be_a(String) + expect(dsa_ca_data[:signature_key]).to be_a(Hash) expect(dsa_ca_data[:signature]).to be_a(String) end @@ -517,21 +523,21 @@ expect(ecdsa_ca_data[:nonce]).to be_a(String) expect(ecdsa_ca_data[:nonce].length).to eq(32) - expect(ecdsa_ca_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) - expect(ecdsa_ca_data[:key_data][:e]).to be_a(OpenSSL::BN) - expect(ecdsa_ca_data[:key_data][:n]).to be_a(OpenSSL::BN) + expect(ecdsa_ca_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) + expect(ecdsa_ca_data[:public_key][:e]).to be_a(OpenSSL::BN) + expect(ecdsa_ca_data[:public_key][:n]).to be_a(OpenSSL::BN) expect(ecdsa_ca_data[:serial]).to eq(123) expect(ecdsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ecdsa_ca_data[:key_id]).to eq("my-ident") - expect(ecdsa_ca_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(ecdsa_ca_data[:valid_after]).to eq(0) - expect(ecdsa_ca_data[:valid_before]).to eq((2**64)-1) - expect(ecdsa_ca_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ecdsa_ca_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(ecdsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) + expect(ecdsa_ca_data[:valid_after]).to eq(Time.at(0)) + expect(ecdsa_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ecdsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(ecdsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ecdsa_ca_data[:reserved]).to eq("") - expect(ecdsa_ca_data[:signature_key]).to be_a(String) + expect(ecdsa_ca_data[:signature_key]).to be_a(Hash) expect(ecdsa_ca_data[:signature]).to be_a(String) end @@ -541,21 +547,21 @@ expect(ed25519_ca_data[:nonce]).to be_a(String) expect(ed25519_ca_data[:nonce].length).to eq(32) - expect(ed25519_ca_data[:key_data][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) - expect(ed25519_ca_data[:key_data][:e]).to be_a(OpenSSL::BN) - expect(ed25519_ca_data[:key_data][:n]).to be_a(OpenSSL::BN) + expect(ed25519_ca_data[:public_key][:algo]).to eq(SSHData::PublicKey::ALGO_RSA) + expect(ed25519_ca_data[:public_key][:e]).to be_a(OpenSSL::BN) + expect(ed25519_ca_data[:public_key][:n]).to be_a(OpenSSL::BN) expect(ed25519_ca_data[:serial]).to eq(123) expect(ed25519_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ed25519_ca_data[:key_id]).to eq("my-ident") - expect(ed25519_ca_data[:valid_principals]).to eq("\x00\x00\x00\x02p1\x00\x00\x00\x02p2") - expect(ed25519_ca_data[:valid_after]).to eq(0) - expect(ed25519_ca_data[:valid_before]).to eq((2**64)-1) - expect(ed25519_ca_data[:critical_options]).to eq("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ed25519_ca_data[:extensions]).to eq("\x00\x00\x00\x15permit-X11-forwarding\x00\x00\x00\x00\x00\x00\x00\x03baz\x00\x00\x00\b\x00\x00\x00\x04qwer") + expect(ed25519_ca_data[:valid_principals]).to eq(["p1", "p2"]) + expect(ed25519_ca_data[:valid_after]).to eq(Time.at(0)) + expect(ed25519_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ed25519_ca_data[:critical_options]).to eq({"foo"=>"bar"}) + expect(ed25519_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ed25519_ca_data[:reserved]).to eq("") - expect(ed25519_ca_data[:signature_key]).to be_a(String) + expect(ed25519_ca_data[:signature_key]).to be_a(Hash) expect(ed25519_ca_data[:signature]).to be_a(String) end end @@ -563,34 +569,39 @@ describe("#decode_options") do it "can decode options" do opts = {"k1" => "v1", "k2" => "v2"} - encoded = opts.reduce("") do |cum, (k, v)| + raw_opts = opts.reduce("") do |cum, (k, v)| cum + [ described_class.encode_string(k), described_class.encode_string(described_class.encode_string(v)) ].join end + encoded = described_class.encode_string(raw_opts) decoded, read = described_class.decode_options(encoded) expect(decoded).to eq(opts) expect(read).to eq(encoded.bytesize) - decoded, read = described_class.decode_options("") + encoded = described_class.encode_string("") + decoded, read = described_class.decode_options(encoded) expect(decoded).to eq({}) - expect(read).to eq(0) + expect(read).to eq(encoded.bytesize) end end - describe("#decode_strings") do + describe("#decode_list") do it "can decode a series of strings" do strs = %w(one two three) - encoded = strs.map { |s| described_class.encode_string(s) }.join - decoded, read = described_class.decode_strings(encoded) + list_raw = strs.map { |s| described_class.encode_string(s) }.join + + encoded = described_class.encode_string(list_raw) + decoded, read = described_class.decode_list(encoded) expect(decoded).to eq(strs) expect(read).to eq(encoded.bytesize) - decoded, read = described_class.decode_strings("") + encoded = described_class.encode_string("") + decoded, read = described_class.decode_list(encoded) expect(decoded).to eq([]) - expect(read).to eq(0) + expect(read).to eq(encoded.bytesize) end end @@ -598,11 +609,11 @@ it "can decode a series of strings" do strs = %w(one two three) encoded = strs.map { |s| described_class.encode_string(s) }.join - decoded, read = described_class.decode_n_strings(encoded, 2) + decoded, read = described_class.decode_n_strings(encoded, 0, 2) expect(decoded).to eq(strs[0..1]) expect(read).to eq(encoded.bytesize - 9) - decoded, read = described_class.decode_n_strings("", 0) + decoded, read = described_class.decode_n_strings("", 0, 0) expect(decoded).to eq([]) expect(read).to eq(0) end @@ -624,4 +635,14 @@ expect(i2).to eq(i1) end end + + describe("#decode_time") do + it "can round trip" do + t1 = Time.at((rand * 1000000000).to_i) + t2, read = described_class.decode_time([t1.to_i].pack("Q>")) + + expect(t2).to eq(t1) + expect(read).to eq(8) + end + end end From 7fdb72a330737c796692ca2989f8fd0d007638f4 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 15:52:26 -0700 Subject: [PATCH 03/10] add methods for encoding more types --- lib/ssh_data/encoding.rb | 130 ++++++++++++------- spec/encoding_spec.rb | 263 +++++++++++++++++++++++++++++++-------- 2 files changed, 296 insertions(+), 97 deletions(-) diff --git a/lib/ssh_data/encoding.rb b/lib/ssh_data/encoding.rb index 52ff72e..542b27b 100644 --- a/lib/ssh_data/encoding.rb +++ b/lib/ssh_data/encoding.rb @@ -379,12 +379,18 @@ def encode_fields(*fields) case type when :string encode_string(value) + when :list + encode_list(value) when :mpint encode_mpint(value) + when :time + encode_time(value) when :uint64 encode_uint64(value) when :uint32 encode_uint32(value) + when :options + encode_options(value) else raise DecodeError end @@ -451,6 +457,76 @@ def decode_list(raw, offset=0) [list, str_read] end + # Encode a list of strings. + # + # value - The Array of Strings to encode. + # + # Returns an encoded representation of the list. + def encode_list(value) + encode_string(value.map { |s| encode_string(s) }.join) + end + + # Read a multi-precision integer from the provided raw data. + # + # raw - A binary String. + # offset - The offset into raw at which to read (default 0). + # + # Returns an Array including the decoded mpint as an OpenSSL::BN and the + # Integer number of bytes read. + def decode_mpint(raw, offset=0) + if raw.bytesize < offset + 4 + raise DecodeError, "data too short" + end + + str_size_s = raw.byteslice(offset, 4) + str_size = str_size_s.unpack("L>").first + mpi_size = str_size + 4 + + if raw.bytesize < offset + mpi_size + raise DecodeError, "data too short" + end + + mpi_s = raw.slice(offset, mpi_size) + + # This calls OpenSSL's BN_mpi2bn() function. As far as I can tell, this + # matches up with with MPI type defined in RFC4251 Section 5 with the + # exception that OpenSSL doesn't enforce minimal length. We could enforce + # this ourselves, but it doesn't seem worth the added complexity. + mpi = OpenSSL::BN.new(mpi_s, 0) + + [mpi, mpi_size] + end + + # Encode a BN as an mpint. + # + # value - The OpenSSL::BN value to encode. + # + # Returns an encoded representation of the BN. + def encode_mpint(value) + value.to_s(0) + end + + # Read a time from the provided raw data. + # + # raw - A binary String. + # offset - The offset into raw at which to read (default 0). + # + # Returns an Array including the decoded Time and the Integer number of + # bytes read. + def decode_time(raw, offset=0) + time_raw, read = decode_uint64(raw, offset) + [Time.at(time_raw), read] + end + + # Encode a time. + # + # value - The Time value to encode. + # + # Returns an encoded representation of the Time. + def encode_time(value) + encode_uint64(value.to_i) + end + # Read the specified number of strings out of the provided raw data. # # raw - A binary String. @@ -508,56 +584,18 @@ def decode_options(raw, offset=0) [opts, str_read] end - # Read a multi-precision integer from the provided raw data. + # Encode series of key/value pairs. # - # raw - A binary String. - # offset - The offset into raw at which to read (default 0). + # value - The Hash value to encode. # - # Returns an Array including the decoded mpint as an OpenSSL::BN and the - # Integer number of bytes read. - def decode_mpint(raw, offset=0) - if raw.bytesize < offset + 4 - raise DecodeError, "data too short" + # Returns an encoded representation of the Hash. + def encode_options(value) + opts_raw = value.reduce("") do |encoded, (key, value)| + value_str = value == true ? "" : encode_string(value) + encoded + encode_string(key) + encode_string(value_str) end - str_size_s = raw.byteslice(offset, 4) - str_size = str_size_s.unpack("L>").first - mpi_size = str_size + 4 - - if raw.bytesize < offset + mpi_size - raise DecodeError, "data too short" - end - - mpi_s = raw.slice(offset, mpi_size) - - # This calls OpenSSL's BN_mpi2bn() function. As far as I can tell, this - # matches up with with MPI type defined in RFC4251 Section 5 with the - # exception that OpenSSL doesn't enforce minimal length. We could enforce - # this ourselves, but it doesn't seem worth the added complexity. - mpi = OpenSSL::BN.new(mpi_s, 0) - - [mpi, mpi_size] - end - - # Encoding a BN as an mpint. - # - # value - The OpenSSL::BN value to encode. - # - # Returns an encoded representation of the BN. - def encode_mpint(value) - value.to_s(0) - end - - # Read a time from the provided raw data. - # - # raw - A binary String. - # offset - The offset into raw at which to read (default 0). - # - # Returns an Array including the decoded Time and the Integer number of - # bytes read. - def decode_time(raw, offset=0) - time_raw, read = decode_uint64(raw, offset) - [Time.at(time_raw), read] + encode_string(opts_raw) end # Read a uint64 from the provided raw data. diff --git a/spec/encoding_spec.rb b/spec/encoding_spec.rb index b828e9a..9f8f9d3 100644 --- a/spec/encoding_spec.rb +++ b/spec/encoding_spec.rb @@ -2,6 +2,8 @@ require_relative "./spec_helper" describe SSHData::Encoding do + let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) } + describe "#pem_type" do let(:type) { "FOO BAR" } let(:head) { "-----BEGIN #{type}-----" } @@ -566,43 +568,229 @@ end end - describe("#decode_options") do - it "can decode options" do - opts = {"k1" => "v1", "k2" => "v2"} - raw_opts = opts.reduce("") do |cum, (k, v)| - cum + [ - described_class.encode_string(k), - described_class.encode_string(described_class.encode_string(v)) - ].join + describe("strings") do + test_cases = [] + + test_cases << [ + :normal, # name + "foobar", # raw + "\x00\x00\x00\x06foobar", # encoded + ] + + test_cases << [ + :empty, # name + "", # raw + "\x00\x00\x00\x00", # encoded + ] + + test_cases.each do |name, raw, encoded| + describe("#{name} values") do + it "can decode" do + raw2, read = described_class.decode_string(encoded + junk) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can at an offset" do + raw2, read = described_class.decode_string(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can encode" do + encoded2 = described_class.encode_string(raw) + expect(encoded2).to eq(encoded) + end + end + end + end + + describe("lists") do + test_cases = [] + + test_cases << [ + :normal, # name + %w(one two three), # raw + "\x00\x00\x00\x17\x00\x00\x00\x03one\x00\x00\x00\x03two\x00\x00\x00\x05three", # encoded + ] + + test_cases << [ + :empty, # name + %w(), # raw + "\x00\x00\x00\x00", # encoded + ] + + test_cases.each do |name, raw, encoded| + describe("#{name} values") do + it "can decode" do + raw2, read = described_class.decode_list(encoded + junk) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can decode at an offset" do + raw2, read = described_class.decode_list(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can encode" do + encoded2 = described_class.encode_list(raw) + expect(encoded2).to eq(encoded) + end + end + end + end + + describe("mpint") do + test_cases = [] + + test_cases << [ + :positive, # name + OpenSSL::BN.new(0x01020304), # raw + String.new("\x00\x00\x00\x04\x01\x02\x03\x04", encoding: Encoding::ASCII_8BIT) # encoded + ] + + test_cases << [ + :zero, # name + OpenSSL::BN.new(0x00), # raw + String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT) # encoded + ] + + test_cases.each do |name, raw, encoded| + describe("#{name} values") do + it "can decode" do + raw2, read = described_class.decode_mpint(encoded + junk) + expect(raw2.to_i).to eq(raw.to_i) + expect(read).to eq(encoded.bytesize) + end + + it "can decode at an offset" do + raw2, read = described_class.decode_mpint(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can encode" do + encoded2 = described_class.encode_mpint(raw) + expect(encoded2).to eq(encoded) + end end + end + end + + describe("time") do + let(:raw) { Time.at((rand * 1000000000).to_i) } + let(:encoded) { [raw.to_i].pack("Q>") } + let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) } - encoded = described_class.encode_string(raw_opts) - decoded, read = described_class.decode_options(encoded) - expect(decoded).to eq(opts) + it "can decode" do + raw2, read = described_class.decode_time(encoded + junk) + expect(raw2).to eq(raw) expect(read).to eq(encoded.bytesize) + end - encoded = described_class.encode_string("") - decoded, read = described_class.decode_options(encoded) - expect(decoded).to eq({}) + it "can decode at an offset" do + raw2, read = described_class.decode_time(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) expect(read).to eq(encoded.bytesize) end + + it "can encode" do + encoded2 = described_class.encode_time(raw) + expect(encoded2).to eq(encoded) + end end - describe("#decode_list") do - it "can decode a series of strings" do - strs = %w(one two three) - list_raw = strs.map { |s| described_class.encode_string(s) }.join + describe("uint64") do + let(:raw) { 0x1234567890abcdef } + let(:encoded) { String.new("\x12\x34\x56\x78\x90\xab\xcd\xef", encoding: Encoding::ASCII_8BIT) } + let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) } - encoded = described_class.encode_string(list_raw) - decoded, read = described_class.decode_list(encoded) - expect(decoded).to eq(strs) + it "can decode" do + raw2, read = described_class.decode_uint64(encoded + junk) + expect(raw2).to eq(raw) expect(read).to eq(encoded.bytesize) + end - encoded = described_class.encode_string("") - decoded, read = described_class.decode_list(encoded) - expect(decoded).to eq([]) + it "can decode at an offset" do + raw2, read = described_class.decode_uint64(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) expect(read).to eq(encoded.bytesize) end + + it "can encode" do + encoded2 = described_class.encode_uint64(raw) + expect(encoded2).to eq(encoded) + end + end + + describe("uint32") do + let(:raw) { 0x12345678 } + let(:encoded) { String.new("\x12\x34\x56\x78", encoding: Encoding::ASCII_8BIT) } + let(:junk) { String.new("\xff\xff", encoding: Encoding::ASCII_8BIT) } + + it "can decode" do + raw2, read = described_class.decode_uint32(encoded + junk) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can decode at an offset" do + raw2, read = described_class.decode_uint32(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can encode" do + encoded2 = described_class.encode_uint32(raw) + expect(encoded2).to eq(encoded) + end + end + + describe("options") do + test_cases = [] + + test_cases << [ + :normal, # name + {"k1" => "v1", "k2" => true, "k3" => "v3"}, # raw + [ # encoded + "\x00\x00\x00\x2a", + "\x00\x00\x00\x02", "k1", + "\x00\x00\x00\x06", "\x00\x00\x00\x02", "v1", + "\x00\x00\x00\x02", "k2", + "\x00\x00\x00\x00", + "\x00\x00\x00\x02", "k3", + "\x00\x00\x00\x06", "\x00\x00\x00\x02", "v3", + ].join + ] + + test_cases << [ + :empty, # name + {}, # raw + "\x00\x00\x00\x00" # encoded + ] + + test_cases.each do |name, raw, encoded| + describe("#{name} values") do + it "can decode" do + raw2, read = described_class.decode_options(encoded + junk) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can at an offset" do + raw2, read = described_class.decode_options(junk + encoded + junk, junk.bytesize) + expect(raw2).to eq(raw) + expect(read).to eq(encoded.bytesize) + end + + it "can encode" do + encoded2 = described_class.encode_options(raw) + expect(encoded2).to eq(encoded) + end + end + end end describe("#decode_n_strings") do @@ -618,31 +806,4 @@ expect(read).to eq(0) end end - - describe("#decode_string") do - it "can round trip" do - s1 = "foobar" - s2, read = described_class.decode_string(described_class.encode_string(s1)) - expect(s2).to eq(s1) - expect(read).to eq(s1.length + 4) - end - end - - describe("#decode_mpint") do - it "can round trip" do - i1 = OpenSSL::BN.new(SecureRandom.bytes((rand * 100).to_i), 2) - i2, read = described_class.decode_mpint(described_class.encode_mpint(i1)) - expect(i2).to eq(i1) - end - end - - describe("#decode_time") do - it "can round trip" do - t1 = Time.at((rand * 1000000000).to_i) - t2, read = described_class.decode_time([t1.to_i].pack("Q>")) - - expect(t2).to eq(t1) - expect(read).to eq(8) - end - end end From cfc5029bfbac4406e41b133f757ed12578d32076 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 16:27:00 -0700 Subject: [PATCH 04/10] re-encode certificates back into openssh format --- lib/ssh_data/certificate.rb | 44 ++++++ lib/ssh_data/encoding.rb | 4 +- lib/ssh_data/public_key/base.rb | 2 +- lib/ssh_data/public_key/dsa.rb | 2 +- lib/ssh_data/public_key/ecdsa.rb | 2 +- lib/ssh_data/public_key/ed25519.rb | 2 +- lib/ssh_data/public_key/rsa.rb | 2 +- spec/certificate_spec.rb | 245 ++++++++++++----------------- 8 files changed, 150 insertions(+), 153 deletions(-) diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index 48b6bd6..e807c88 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -119,5 +119,49 @@ def initialize(algo:, nonce:, public_key:, serial:, type:, key_id:, valid_princi @ca_key = ca_key @signature = signature end + + # OpenSSH certificate in authorized_keys format (see sshd(8) manual page). + # + # comment - Optional String comment to append. + # + # Returns a String key. + def openssh(comment: nil) + [algo, Base64.strict_encode64(rfc4253), comment].compact.join(" ") + end + + # RFC4253 binary encoding of the certificate. + # + # Returns a binary String. + def rfc4253 + Encoding.encode_fields( + [:string, algo], + [:string, nonce], + [:raw, public_key_without_algo], + [:uint64, serial], + [:uint32, type], + [:string, key_id], + [:list, valid_principals], + [:time, valid_after], + [:time, valid_before], + [:options, critical_options], + [:options, extensions], + [:string, reserved], + [:string, ca_key.rfc4253], + [:string, signature], + ) + end + + private + + # Helper for getting the RFC4253 encoded public key with the first field + # (the algorithm) stripped off. + # + # Returns a String. + def public_key_without_algo + key = public_key.rfc4253 + _, algo_len = Encoding.decode_string(key) + key.byteslice(algo_len..-1) + end + end end diff --git a/lib/ssh_data/encoding.rb b/lib/ssh_data/encoding.rb index 542b27b..910a841 100644 --- a/lib/ssh_data/encoding.rb +++ b/lib/ssh_data/encoding.rb @@ -377,6 +377,8 @@ def decode_fields(raw, fields, offset=0) def encode_fields(*fields) fields.map do |type, value| case type + when :raw + value when :string encode_string(value) when :list @@ -392,7 +394,7 @@ def encode_fields(*fields) when :options encode_options(value) else - raise DecodeError + raise DecodeError, "bad type: #{type}" end end.join end diff --git a/lib/ssh_data/public_key/base.rb b/lib/ssh_data/public_key/base.rb index e431c2b..96fd77b 100644 --- a/lib/ssh_data/public_key/base.rb +++ b/lib/ssh_data/public_key/base.rb @@ -33,7 +33,7 @@ def verify(signed_data, signature) raise "implement me" end - # RFC4253 binary encoding of public key. + # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 diff --git a/lib/ssh_data/public_key/dsa.rb b/lib/ssh_data/public_key/dsa.rb index c39b598..0c96a91 100644 --- a/lib/ssh_data/public_key/dsa.rb +++ b/lib/ssh_data/public_key/dsa.rb @@ -80,7 +80,7 @@ def verify(signed_data, signature) openssl.verify(OpenSSL::Digest::SHA1.new, openssl_sig, signed_data) end - # RFC4253 binary encoding of public key. + # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 diff --git a/lib/ssh_data/public_key/ecdsa.rb b/lib/ssh_data/public_key/ecdsa.rb index 37e93c6..cd6078b 100644 --- a/lib/ssh_data/public_key/ecdsa.rb +++ b/lib/ssh_data/public_key/ecdsa.rb @@ -100,7 +100,7 @@ def verify(signed_data, signature) openssl.verify(digest.new, openssl_sig, signed_data) end - # RFC4253 binary encoding of public key. + # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 diff --git a/lib/ssh_data/public_key/ed25519.rb b/lib/ssh_data/public_key/ed25519.rb index 4aa37fc..34fa870 100644 --- a/lib/ssh_data/public_key/ed25519.rb +++ b/lib/ssh_data/public_key/ed25519.rb @@ -46,7 +46,7 @@ def verify(signed_data, signature) end end - # RFC4253 binary encoding of public key. + # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 diff --git a/lib/ssh_data/public_key/rsa.rb b/lib/ssh_data/public_key/rsa.rb index 3d52702..b6f5fe9 100644 --- a/lib/ssh_data/public_key/rsa.rb +++ b/lib/ssh_data/public_key/rsa.rb @@ -32,7 +32,7 @@ def verify(signed_data, signature) openssl.verify(OpenSSL::Digest::SHA1.new, raw_sig, signed_data) end - # RFC4253 binary encoding of public key. + # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index c2fc29a..925c70d 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -1,16 +1,6 @@ require_relative "./spec_helper" describe SSHData::Certificate do - let(:rsa_cert) { described_class.parse_openssh(fixture("rsa_leaf_for_rsa_ca-cert.pub")) } - let(:dsa_cert) { described_class.parse_openssh(fixture("dsa_leaf_for_rsa_ca-cert.pub")) } - let(:ecdsa_cert) { described_class.parse_openssh(fixture("ecdsa_leaf_for_rsa_ca-cert.pub")) } - let(:ed25519_cert) { described_class.parse_openssh(fixture("ed25519_leaf_for_rsa_ca-cert.pub")) } - - let(:rsa_ca_cert) { described_class.parse_openssh(fixture("rsa_leaf_for_rsa_ca-cert.pub")) } - let(:dsa_ca_cert) { described_class.parse_openssh(fixture("rsa_leaf_for_dsa_ca-cert.pub")) } - let(:ecdsa_ca_cert) { described_class.parse_openssh(fixture("rsa_leaf_for_ecdsa_ca-cert.pub")) } - let(:ed25519_ca_cert) { described_class.parse_openssh(fixture("rsa_leaf_for_ed25519_ca-cert.pub")) } - let(:min_time) { Time.at(0) } let(:max_time) { Time.at((2**64)-1) } @@ -42,7 +32,7 @@ cert = [algo, b64, comment].join(" ") expect { - described_class.parse_openssh(cert, unsafe_no_verify: true) + described_class.parse_openssh(cert) }.to raise_error(SSHData::DecodeError) end @@ -51,7 +41,7 @@ cert = [SSHData::Certificate::ALGO_ED25519, b64, comment].join(" ") expect { - described_class.parse_openssh(cert, unsafe_no_verify: true) + described_class.parse_openssh(cert) }.to raise_error(SSHData::DecodeError) end @@ -60,143 +50,104 @@ cert = [type, b64].join(" ") expect { - described_class.parse_openssh(cert, unsafe_no_verify: true) + described_class.parse_openssh(cert) }.not_to raise_error end - it "parses RSA certs" do - expect(rsa_cert.algo).to eq(SSHData::Certificate::ALGO_RSA) - expect(rsa_cert.nonce).to be_a(String) - expect(rsa_cert.public_key).to be_a(SSHData::PublicKey::RSA) - expect(rsa_cert.serial).to eq(123) - expect(rsa_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(rsa_cert.key_id).to eq("my-ident") - expect(rsa_cert.valid_principals).to eq(["p1", "p2"]) - expect(rsa_cert.valid_after).to eq(min_time) - expect(rsa_cert.valid_before).to eq(max_time) - expect(rsa_cert.critical_options).to eq({"foo" => "bar"}) - expect(rsa_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(rsa_cert.reserved).to eq("") - expect(rsa_cert.ca_key).to be_a(SSHData::PublicKey::RSA) - expect(rsa_cert.signature).to be_a(String) - end - - it "parses DSA certs" do - expect(dsa_cert.algo).to eq(SSHData::Certificate::ALGO_DSA) - expect(dsa_cert.nonce).to be_a(String) - expect(dsa_cert.public_key).to be_a(SSHData::PublicKey::DSA) - expect(dsa_cert.serial).to eq(123) - expect(dsa_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(dsa_cert.key_id).to eq("my-ident") - expect(dsa_cert.valid_principals).to eq(["p1", "p2"]) - expect(dsa_cert.valid_after).to eq(min_time) - expect(dsa_cert.valid_before).to eq(max_time) - expect(dsa_cert.critical_options).to eq({"foo" => "bar"}) - expect(dsa_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(dsa_cert.reserved).to eq("") - expect(dsa_cert.ca_key).to be_a(SSHData::PublicKey::RSA) - expect(dsa_cert.signature).to be_a(String) - end - - it "parses ECDSA certs" do - expect(ecdsa_cert.algo).to eq(SSHData::Certificate::ALGO_ECDSA256) - expect(ecdsa_cert.nonce).to be_a(String) - expect(ecdsa_cert.public_key).to be_a(SSHData::PublicKey::ECDSA) - expect(ecdsa_cert.serial).to eq(123) - expect(ecdsa_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(ecdsa_cert.key_id).to eq("my-ident") - expect(ecdsa_cert.valid_principals).to eq(["p1", "p2"]) - expect(ecdsa_cert.valid_after).to eq(min_time) - expect(ecdsa_cert.valid_before).to eq(max_time) - expect(ecdsa_cert.critical_options).to eq({"foo" => "bar"}) - expect(ecdsa_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(ecdsa_cert.reserved).to eq("") - expect(ecdsa_cert.ca_key).to be_a(SSHData::PublicKey::RSA) - expect(ecdsa_cert.signature).to be_a(String) - end - - it "parses ED25519 certs" do - expect(ed25519_cert.algo).to eq(SSHData::Certificate::ALGO_ED25519) - expect(ed25519_cert.nonce).to be_a(String) - expect(ed25519_cert.public_key).to be_a(SSHData::PublicKey::ED25519) - expect(ed25519_cert.serial).to eq(123) - expect(ed25519_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(ed25519_cert.key_id).to eq("my-ident") - expect(ed25519_cert.valid_principals).to eq(["p1", "p2"]) - expect(ed25519_cert.valid_after).to eq(min_time) - expect(ed25519_cert.valid_before).to eq(max_time) - expect(ed25519_cert.critical_options).to eq({"foo" => "bar"}) - expect(ed25519_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(ed25519_cert.reserved).to eq("") - expect(ed25519_cert.ca_key).to be_a(SSHData::PublicKey::RSA) - expect(ed25519_cert.signature).to be_a(String) - end - - it "parses certs issued by RSA CAs" do - expect(rsa_ca_cert.algo).to eq(SSHData::Certificate::ALGO_RSA) - expect(rsa_ca_cert.nonce).to be_a(String) - expect(rsa_ca_cert.public_key).to be_a(SSHData::PublicKey::RSA) - expect(rsa_ca_cert.serial).to eq(123) - expect(rsa_ca_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(rsa_ca_cert.key_id).to eq("my-ident") - expect(rsa_ca_cert.valid_principals).to eq(["p1", "p2"]) - expect(rsa_ca_cert.valid_after).to eq(min_time) - expect(rsa_ca_cert.valid_before).to eq(max_time) - expect(rsa_ca_cert.critical_options).to eq({"foo" => "bar"}) - expect(rsa_ca_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(rsa_ca_cert.reserved).to eq("") - expect(rsa_ca_cert.ca_key).to be_a(SSHData::PublicKey::RSA) - expect(rsa_ca_cert.signature).to be_a(String) - end - - it "parses certs issued by DSA CAs" do - expect(dsa_ca_cert.algo).to eq(SSHData::Certificate::ALGO_RSA) - expect(dsa_ca_cert.nonce).to be_a(String) - expect(dsa_ca_cert.public_key).to be_a(SSHData::PublicKey::RSA) - expect(dsa_ca_cert.serial).to eq(123) - expect(dsa_ca_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(dsa_ca_cert.key_id).to eq("my-ident") - expect(dsa_ca_cert.valid_principals).to eq(["p1", "p2"]) - expect(dsa_ca_cert.valid_after).to eq(min_time) - expect(dsa_ca_cert.valid_before).to eq(max_time) - expect(dsa_ca_cert.critical_options).to eq({"foo" => "bar"}) - expect(dsa_ca_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(dsa_ca_cert.reserved).to eq("") - expect(dsa_ca_cert.ca_key).to be_a(SSHData::PublicKey::DSA) - expect(dsa_ca_cert.signature).to be_a(String) - end - - it "parses certs issued by ECDSA CAs" do - expect(ecdsa_ca_cert.algo).to eq(SSHData::Certificate::ALGO_RSA) - expect(ecdsa_ca_cert.nonce).to be_a(String) - expect(ecdsa_ca_cert.public_key).to be_a(SSHData::PublicKey::RSA) - expect(ecdsa_ca_cert.serial).to eq(123) - expect(ecdsa_ca_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(ecdsa_ca_cert.key_id).to eq("my-ident") - expect(ecdsa_ca_cert.valid_principals).to eq(["p1", "p2"]) - expect(ecdsa_ca_cert.valid_after).to eq(min_time) - expect(ecdsa_ca_cert.valid_before).to eq(max_time) - expect(ecdsa_ca_cert.critical_options).to eq({"foo" => "bar"}) - expect(ecdsa_ca_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(ecdsa_ca_cert.reserved).to eq("") - expect(ecdsa_ca_cert.ca_key).to be_a(SSHData::PublicKey::ECDSA) - expect(ecdsa_ca_cert.signature).to be_a(String) - end - - it "parses certs issued by ED25519 CAs" do - expect(ed25519_ca_cert.algo).to eq(SSHData::Certificate::ALGO_RSA) - expect(ed25519_ca_cert.nonce).to be_a(String) - expect(ed25519_ca_cert.public_key).to be_a(SSHData::PublicKey::RSA) - expect(ed25519_ca_cert.serial).to eq(123) - expect(ed25519_ca_cert.type).to eq(SSHData::Certificate::TYPE_USER) - expect(ed25519_ca_cert.key_id).to eq("my-ident") - expect(ed25519_ca_cert.valid_principals).to eq(["p1", "p2"]) - expect(ed25519_ca_cert.valid_after).to eq(min_time) - expect(ed25519_ca_cert.valid_before).to eq(max_time) - expect(ed25519_ca_cert.critical_options).to eq({"foo" => "bar"}) - expect(ed25519_ca_cert.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) - expect(ed25519_ca_cert.reserved).to eq("") - expect(ed25519_ca_cert.ca_key).to be_a(SSHData::PublicKey::ED25519) - expect(ed25519_ca_cert.signature).to be_a(String) + test_cases = [] + + test_cases << [ + :rsa_cert, # name + "rsa_leaf_for_rsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_RSA, # algo + SSHData::PublicKey::RSA, # public key type + SSHData::PublicKey::RSA # ca key type + ] + + test_cases << [ + :dsa_cert, # name + "dsa_leaf_for_rsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_DSA, # algo + SSHData::PublicKey::DSA, # public key type + SSHData::PublicKey::RSA # ca key type + ] + + test_cases << [ + :ecdsa_cert, # name + "ecdsa_leaf_for_rsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_ECDSA256, # algo + SSHData::PublicKey::ECDSA, # public key type + SSHData::PublicKey::RSA # ca key type + ] + + test_cases << [ + :ed25519_cert, # name + "ed25519_leaf_for_rsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_ED25519, # algo + SSHData::PublicKey::ED25519, # public key type + SSHData::PublicKey::RSA # ca key type + ] + + test_cases << [ + :rsa_ca, # name + "rsa_leaf_for_rsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_RSA, # algo + SSHData::PublicKey::RSA, # public key type + SSHData::PublicKey::RSA # ca key type + ] + + test_cases << [ + :dsa_ca, # name + "rsa_leaf_for_dsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_RSA, # algo + SSHData::PublicKey::RSA, # public key type + SSHData::PublicKey::DSA # ca key type + ] + + test_cases << [ + :ecdsa_ca, # name + "rsa_leaf_for_ecdsa_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_RSA, # algo + SSHData::PublicKey::RSA, # public key type + SSHData::PublicKey::ECDSA # ca key type + ] + + test_cases << [ + :ed25519_ca, # name + "rsa_leaf_for_ed25519_ca-cert.pub", # fixture + SSHData::Certificate::ALGO_RSA, # algo + SSHData::PublicKey::RSA, # public key type + SSHData::PublicKey::ED25519 # ca key type + ] + + + test_cases.each do |name, fixture_name, algo, public_key_class, ca_key_class| + describe(name) do + let(:openssh) { fixture(fixture_name).strip } + let(:comment) { SSHData.key_parts(openssh).last } + + subject { SSHData::Certificate.parse_openssh(openssh) } + + it "parses correctly" do + expect(subject.algo).to eq(algo) + expect(subject.nonce).to be_a(String) + expect(subject.public_key).to be_a(public_key_class) + expect(subject.serial).to eq(123) + expect(subject.type).to eq(SSHData::Certificate::TYPE_USER) + expect(subject.key_id).to eq("my-ident") + expect(subject.valid_principals).to eq(["p1", "p2"]) + expect(subject.valid_after).to eq(min_time) + expect(subject.valid_before).to eq(max_time) + expect(subject.critical_options).to eq({"foo" => "bar"}) + expect(subject.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) + expect(subject.reserved).to eq("") + expect(subject.ca_key).to be_a(ca_key_class) + expect(subject.signature).to be_a(String) + end + + it "encodes correctly" do + expect(subject.openssh(comment: comment)).to eq(openssh) + end + end end end From a9212976e1f3720b2caf6107b3b14c27c7bdaa24 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 16:58:54 -0700 Subject: [PATCH 05/10] calculate signed_data from serialized cert --- lib/ssh_data.rb | 1 + lib/ssh_data/certificate.rb | 41 ++++++++++++++++++++++--------------- lib/ssh_data/encoding.rb | 9 ++++++++ spec/certificate_spec.rb | 7 ++----- spec/encoding_spec.rb | 32 ++++++++++++++--------------- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/lib/ssh_data.rb b/lib/ssh_data.rb index 0097a63..f164107 100644 --- a/lib/ssh_data.rb +++ b/lib/ssh_data.rb @@ -1,5 +1,6 @@ require "openssl" require "base64" +require "securerandom" module SSHData # Break down a key in OpenSSH authorized_keys format (see sshd(8) manual diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index e807c88..ccc43f6 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -1,5 +1,9 @@ module SSHData class Certificate + # Special values for valid_before and valid_after. + FOREVER = Time.at(0) + ALWAYS = Time.at((2**64)-1) + # Integer certificate types TYPE_USER = 1 TYPE_HOST = 2 @@ -62,21 +66,9 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false) public_key = PublicKey.from_data(data.delete(:public_key)) ca_key = PublicKey.from_data(data.delete(:signature_key)) - unless unsafe_no_verify - # The signature is the last field. The signature is calculated over all - # preceding data. - signed_data_len = raw.bytesize - data[:signature].bytesize - 4 - signed_data = raw.byteslice(0, signed_data_len) - - unless ca_key.verify(signed_data, data[:signature]) - raise VerifyError - end + new(**data.merge(public_key: public_key, ca_key: ca_key)).tap do |cert| + raise VerifyError unless unsafe_no_verify || cert.verify end - - new(**data.merge( - public_key: public_key, - ca_key: ca_key, - )) end # Intialize a new Certificate instance. @@ -103,9 +95,9 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false) # signature: - The certificate's String signature field. # # Returns nothing. - def initialize(algo:, nonce:, public_key:, serial:, type:, key_id:, valid_principals:, valid_after:, valid_before:, critical_options:, extensions:, reserved:, ca_key:, signature:) - @algo = algo - @nonce = nonce + def initialize(public_key:, key_id:, algo: nil, nonce: nil, serial: 0, type: TYPE_USER, valid_principals: [], valid_after: FOREVER, valid_before: ALWAYS, critical_options: {}, extensions: {}, reserved: "", ca_key: nil, signature: "") + @algo = algo || Encoding::CERT_ALGO_BY_PUBLIC_KEY_ALGO[public_key.algo] + @nonce = nonce || SecureRandom.random_bytes(32) @public_key = public_key @serial = serial @type = type @@ -151,8 +143,23 @@ def rfc4253 ) end + # Verify the certificate's signature. + # + # Returns boolean. + def verify + ca_key.verify(signed_data, signature) + end + private + # The portion of the certificate over which the signature is calculated. + # + # Returns a binary String. + def signed_data + siglen = self.signature.bytesize + 4 + rfc4253.byteslice(0...-siglen) + end + # Helper for getting the RFC4253 encoded public key with the first field # (the algorithm) stripped off. # diff --git a/lib/ssh_data/encoding.rb b/lib/ssh_data/encoding.rb index 910a841..5836a8b 100644 --- a/lib/ssh_data/encoding.rb +++ b/lib/ssh_data/encoding.rb @@ -76,6 +76,15 @@ module Encoding Certificate::ALGO_ED25519 => PublicKey::ALGO_ED25519, } + CERT_ALGO_BY_PUBLIC_KEY_ALGO = { + PublicKey::ALGO_RSA => Certificate::ALGO_RSA, + PublicKey::ALGO_DSA => Certificate::ALGO_DSA, + PublicKey::ALGO_ECDSA256 => Certificate::ALGO_ECDSA256, + PublicKey::ALGO_ECDSA384 => Certificate::ALGO_ECDSA384, + PublicKey::ALGO_ECDSA521 => Certificate::ALGO_ECDSA521, + PublicKey::ALGO_ED25519 => Certificate::ALGO_ED25519, + } + KEY_FIELDS_BY_PUBLIC_KEY_ALGO = { PublicKey::ALGO_RSA => RSA_KEY_FIELDS, PublicKey::ALGO_DSA => DSA_KEY_FIELDS, diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index 925c70d..22c2d3e 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -1,9 +1,6 @@ require_relative "./spec_helper" describe SSHData::Certificate do - let(:min_time) { Time.at(0) } - let(:max_time) { Time.at((2**64)-1) } - it "supports the deprecated Certificate.parse method" do expect { described_class.parse(fixture("rsa_leaf_for_rsa_ca-cert.pub")) @@ -136,8 +133,8 @@ expect(subject.type).to eq(SSHData::Certificate::TYPE_USER) expect(subject.key_id).to eq("my-ident") expect(subject.valid_principals).to eq(["p1", "p2"]) - expect(subject.valid_after).to eq(min_time) - expect(subject.valid_before).to eq(max_time) + expect(subject.valid_after).to eq(SSHData::Certificate::FOREVER) + expect(subject.valid_before).to eq(SSHData::Certificate::ALWAYS) expect(subject.critical_options).to eq({"foo" => "bar"}) expect(subject.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) expect(subject.reserved).to eq("") diff --git a/spec/encoding_spec.rb b/spec/encoding_spec.rb index 9f8f9d3..630bbad 100644 --- a/spec/encoding_spec.rb +++ b/spec/encoding_spec.rb @@ -378,8 +378,8 @@ expect(rsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(rsa_data[:key_id]).to eq("my-ident") expect(rsa_data[:valid_principals]).to eq(["p1", "p2"]) - expect(rsa_data[:valid_after]).to eq(Time.at(0)) - expect(rsa_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(rsa_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(rsa_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(rsa_data[:critical_options]).to eq({"foo"=>"bar"}) expect(rsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(rsa_data[:reserved]).to eq("") @@ -406,8 +406,8 @@ expect(dsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(dsa_data[:key_id]).to eq("my-ident") expect(dsa_data[:valid_principals]).to eq(["p1", "p2"]) - expect(dsa_data[:valid_after]).to eq(Time.at(0)) - expect(dsa_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(dsa_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(dsa_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(dsa_data[:critical_options]).to eq({"foo"=>"bar"}) expect(dsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(dsa_data[:reserved]).to eq("") @@ -432,8 +432,8 @@ expect(ecdsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ecdsa_data[:key_id]).to eq("my-ident") expect(ecdsa_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ecdsa_data[:valid_after]).to eq(Time.at(0)) - expect(ecdsa_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ecdsa_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(ecdsa_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(ecdsa_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ecdsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ecdsa_data[:reserved]).to eq("") @@ -457,8 +457,8 @@ expect(ed25519_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ed25519_data[:key_id]).to eq("my-ident") expect(ed25519_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ed25519_data[:valid_after]).to eq(Time.at(0)) - expect(ed25519_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ed25519_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(ed25519_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(ed25519_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ed25519_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ed25519_data[:reserved]).to eq("") @@ -483,8 +483,8 @@ expect(rsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(rsa_ca_data[:key_id]).to eq("my-ident") expect(rsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(rsa_ca_data[:valid_after]).to eq(Time.at(0)) - expect(rsa_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(rsa_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(rsa_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(rsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(rsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(rsa_ca_data[:reserved]).to eq("") @@ -509,8 +509,8 @@ expect(dsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(dsa_ca_data[:key_id]).to eq("my-ident") expect(dsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(dsa_ca_data[:valid_after]).to eq(Time.at(0)) - expect(dsa_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(dsa_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(dsa_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(dsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(dsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(dsa_ca_data[:reserved]).to eq("") @@ -533,8 +533,8 @@ expect(ecdsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ecdsa_ca_data[:key_id]).to eq("my-ident") expect(ecdsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ecdsa_ca_data[:valid_after]).to eq(Time.at(0)) - expect(ecdsa_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ecdsa_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(ecdsa_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(ecdsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ecdsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ecdsa_ca_data[:reserved]).to eq("") @@ -557,8 +557,8 @@ expect(ed25519_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ed25519_ca_data[:key_id]).to eq("my-ident") expect(ed25519_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ed25519_ca_data[:valid_after]).to eq(Time.at(0)) - expect(ed25519_ca_data[:valid_before]).to eq(Time.at((2**64)-1)) + expect(ed25519_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) + expect(ed25519_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) expect(ed25519_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ed25519_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ed25519_ca_data[:reserved]).to eq("") From e46b72a533c1710819261b00249bb8545ce843c9 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 17:19:15 -0700 Subject: [PATCH 06/10] allow private keys to sign --- lib/ssh_data/private_key/dsa.rb | 11 +++++++++++ lib/ssh_data/private_key/ecdsa.rb | 11 +++++++++++ lib/ssh_data/private_key/ed25519.rb | 10 ++++++++++ lib/ssh_data/private_key/rsa.rb | 10 ++++++++++ lib/ssh_data/public_key/base.rb | 11 ++++++++++- lib/ssh_data/public_key/ecdsa.rb | 7 +++++++ spec/private_key/dsa_spec.rb | 5 +++++ spec/private_key/ecdsa_spec.rb | 5 +++++ spec/private_key/ed25519_spec.rb | 5 +++++ spec/private_key/rsa_spec.rb | 5 +++++ 10 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/ssh_data/private_key/dsa.rb b/lib/ssh_data/private_key/dsa.rb index df4c2bc..6d45f1f 100644 --- a/lib/ssh_data/private_key/dsa.rb +++ b/lib/ssh_data/private_key/dsa.rb @@ -33,6 +33,17 @@ def initialize(algo:, p:, q:, g:, x:, y:, comment:) @public_key = PublicKey::DSA.new(algo: algo, p: p, q: q, g: g, y: y) end + # Make an SSH signature. + # + # signed_data - The String message over which to calculated the signature. + # + # Returns a binary String signature. + def sign(signed_data) + openssl_sig = openssl.sign(OpenSSL::Digest::SHA1.new, signed_data) + raw_sig = PublicKey::DSA.ssh_signature(openssl_sig) + Encoding.encode_signature(algo, raw_sig) + end + private def asn1 diff --git a/lib/ssh_data/private_key/ecdsa.rb b/lib/ssh_data/private_key/ecdsa.rb index 710c6d1..b571529 100644 --- a/lib/ssh_data/private_key/ecdsa.rb +++ b/lib/ssh_data/private_key/ecdsa.rb @@ -44,6 +44,17 @@ def initialize(algo:, curve:, public_key:, private_key:, comment:) ) end + # Make an SSH signature. + # + # signed_data - The String message over which to calculated the signature. + # + # Returns a binary String signature. + def sign(signed_data) + openssl_sig = openssl.sign(public_key.digest.new, signed_data) + raw_sig = PublicKey::ECDSA.ssh_signature(openssl_sig) + Encoding.encode_signature(algo, raw_sig) + end + private def asn1 diff --git a/lib/ssh_data/private_key/ed25519.rb b/lib/ssh_data/private_key/ed25519.rb index fa06d45..6da687a 100644 --- a/lib/ssh_data/private_key/ed25519.rb +++ b/lib/ssh_data/private_key/ed25519.rb @@ -28,6 +28,16 @@ def initialize(algo:, pk:, sk:, comment:) @public_key = PublicKey::ED25519.new(algo: algo, pk: pk) end + + # Make an SSH signature. + # + # signed_data - The String message over which to calculated the signature. + # + # Returns a binary String signature. + def sign(signed_data) + raw_sig = ed25519_key.sign(signed_data) + Encoding.encode_signature(algo, raw_sig) + end end end end diff --git a/lib/ssh_data/private_key/rsa.rb b/lib/ssh_data/private_key/rsa.rb index 26b0393..70cdcec 100644 --- a/lib/ssh_data/private_key/rsa.rb +++ b/lib/ssh_data/private_key/rsa.rb @@ -35,6 +35,16 @@ def initialize(algo:, n:, e:, d:, iqmp:, p:, q:, comment:) @public_key = PublicKey::RSA.new(algo: algo, e: e, n: n) end + # Make an SSH signature. + # + # signed_data - The String message over which to calculated the signature. + # + # Returns a binary String signature. + def sign(signed_data) + raw_sig = openssl.sign(OpenSSL::Digest::SHA1.new, signed_data) + Encoding.encode_signature(algo, raw_sig) + end + private # CRT coefficient for faster RSA operations. Used by OpenSSL, but not diff --git a/lib/ssh_data/public_key/base.rb b/lib/ssh_data/public_key/base.rb index 96fd77b..3dd1bf1 100644 --- a/lib/ssh_data/public_key/base.rb +++ b/lib/ssh_data/public_key/base.rb @@ -23,10 +23,19 @@ def fingerprint(md5: false) end end + # Make an SSH signature. + # + # signed_data - The String message over which to calculated the signature. + # + # Returns a binary String signature. + def sign(signed_data) + raise "implement me" + end + # Verify an SSH signature. # # signed_data - The String message that the signature was calculated over. - # signature - The binarty String signature with SSH encoding. + # signature - The binary String signature with SSH encoding. # # Returns boolean. def verify(signed_data, signature) diff --git a/lib/ssh_data/public_key/ecdsa.rb b/lib/ssh_data/public_key/ecdsa.rb index cd6078b..cd23db8 100644 --- a/lib/ssh_data/public_key/ecdsa.rb +++ b/lib/ssh_data/public_key/ecdsa.rb @@ -120,6 +120,13 @@ def ==(other) super && other.curve == curve && other.public_key_bytes == public_key_bytes end + # The digest algorithm to use with this key's curve. + # + # Returns an OpenSSL::Digest. + def digest + DIGEST_FOR_CURVE[curve] + end + private def asn1 diff --git a/spec/private_key/dsa_spec.rb b/spec/private_key/dsa_spec.rb index 734bfb0..f26bbc4 100644 --- a/spec/private_key/dsa_spec.rb +++ b/spec/private_key/dsa_spec.rb @@ -4,11 +4,16 @@ let(:private_key) { OpenSSL::PKey::DSA.generate(1024) } let(:public_key) { private_key.public_key } let(:params) { private_key.params } + let(:message) { "hello, world!" } let(:openssh_key) { SSHData::PrivateKey.parse(fixture("dsa_leaf_for_rsa_ca")) } subject { described_class.from_openssl(private_key) } + it "can sign messages" do + expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) + end + it "has an algo" do expect(subject.algo).to eq(SSHData::PublicKey::ALGO_DSA) end diff --git a/spec/private_key/ecdsa_spec.rb b/spec/private_key/ecdsa_spec.rb index 0dc8ef4..a58148d 100644 --- a/spec/private_key/ecdsa_spec.rb +++ b/spec/private_key/ecdsa_spec.rb @@ -17,9 +17,14 @@ let(:private_key) { OpenSSL::PKey::EC.new(openssl_curve).tap(&:generate_key) } let(:public_key) { OpenSSL::PKey::EC.new(private_key.to_der).tap { |k| k.private_key = nil } } let(:comment) { "asdf" } + let(:message) { "hello, world!" } subject { described_class.from_openssl(private_key) } + it "can sign messages" do + expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) + end + it "has an algo" do expect(subject.algo).to eq(algo) end diff --git a/spec/private_key/ed25519_spec.rb b/spec/private_key/ed25519_spec.rb index 1a7bdc3..3f9614a 100644 --- a/spec/private_key/ed25519_spec.rb +++ b/spec/private_key/ed25519_spec.rb @@ -4,6 +4,7 @@ let(:signing_key) { Ed25519::SigningKey.generate } let(:verify_key) { signing_key.verify_key } let(:comment) { "asdf" } + let(:message) { "hello, world!" } let(:openssh_key) { SSHData::PrivateKey.parse(fixture("ed25519_leaf_for_rsa_ca")) } @@ -16,6 +17,10 @@ ) end + it "can sign messages" do + expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) + end + it "has an algo" do expect(subject.algo).to eq(SSHData::PublicKey::ALGO_ED25519) end diff --git a/spec/private_key/rsa_spec.rb b/spec/private_key/rsa_spec.rb index 4c32c19..b28000e 100644 --- a/spec/private_key/rsa_spec.rb +++ b/spec/private_key/rsa_spec.rb @@ -4,11 +4,16 @@ let(:private_key) { OpenSSL::PKey::RSA.generate(2048) } let(:public_key) { private_key.public_key } let(:params) { private_key.params } + let(:message) { "hello, world!" } let(:openssh_key) { SSHData::PrivateKey.parse(fixture("rsa_leaf_for_rsa_ca")) } subject { described_class.from_openssl(private_key) } + it "can sign messages" do + expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) + end + it "has an algo" do expect(subject.algo).to eq(SSHData::PublicKey::ALGO_RSA) end From 689f9f953349600a257c550f477c6157965106d8 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 17:36:36 -0700 Subject: [PATCH 07/10] allow certificates to be signed by private keys --- lib/ssh_data/certificate.rb | 10 ++++++++++ lib/ssh_data/private_key/ed25519.rb | 14 ++++++++++++++ spec/certificate_spec.rb | 27 ++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index ccc43f6..21d23a8 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -143,6 +143,16 @@ def rfc4253 ) end + # Sign this certificate with a private key. + # + # private_key - An SSHData::PrivateKey::Base subclass instance. + # + # Returns nothing. + def sign(private_key) + @ca_key = private_key.public_key + @signature = private_key.sign(signed_data) + end + # Verify the certificate's signature. # # Returns boolean. diff --git a/lib/ssh_data/private_key/ed25519.rb b/lib/ssh_data/private_key/ed25519.rb index 6da687a..414058d 100644 --- a/lib/ssh_data/private_key/ed25519.rb +++ b/lib/ssh_data/private_key/ed25519.rb @@ -3,6 +3,20 @@ module PrivateKey class ED25519 < Base attr_reader :pk, :sk, :ed25519_key + # Create from a ::Ed25519::SigningKey instance. + # + # key - A ::Ed25519::SigningKey instance. + # + # Returns a ED25519 instance. + def self.from_ed25519(key) + new( + algo: PublicKey::ALGO_ED25519, + pk: key.verify_key.to_bytes, + sk: key.to_bytes + key.verify_key.to_bytes, + comment: "", + ) + end + def initialize(algo:, pk:, sk:, comment:) unless algo == PublicKey::ALGO_ED25519 raise DecodeError, "bad algorithm: #{algo.inspect}" diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index 22c2d3e..9c9f1b3 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -1,6 +1,11 @@ require_relative "./spec_helper" describe SSHData::Certificate do + let(:rsa_ca) { SSHData::PrivateKey::RSA.from_openssl(OpenSSL::PKey::RSA.generate(2048)) } + let(:dsa_ca) { SSHData::PrivateKey::DSA.from_openssl(OpenSSL::PKey::DSA.generate(1024)) } + let(:ecdsa_ca) { SSHData::PrivateKey::ECDSA.from_openssl(OpenSSL::PKey::EC.new("prime256v1").tap(&:generate_key)) } + let(:ed25519_ca) { SSHData::PrivateKey::ED25519.from_ed25519(Ed25519::SigningKey.generate) } + it "supports the deprecated Certificate.parse method" do expect { described_class.parse(fixture("rsa_leaf_for_rsa_ca-cert.pub")) @@ -117,7 +122,6 @@ SSHData::PublicKey::ED25519 # ca key type ] - test_cases.each do |name, fixture_name, algo, public_key_class, ca_key_class| describe(name) do let(:openssh) { fixture(fixture_name).strip } @@ -145,6 +149,27 @@ it "encodes correctly" do expect(subject.openssh(comment: comment)).to eq(openssh) end + + it "can be signed with an RSA key" do + expect { subject.sign(rsa_ca) }.to change {subject.signature} + expect(subject.verify).to eq(true) + end + + it "can be signed with an DSA key" do + expect { subject.sign(dsa_ca) }.to change {subject.signature} + expect(subject.verify).to eq(true) + end + + it "can be signed with an ECDSA key" do + expect { subject.sign(ecdsa_ca) }.to change {subject.signature} + expect(subject.verify).to eq(true) + end + + it "can be signed with an ED25519 key" do + expect { subject.sign(ed25519_ca) }.to change {subject.signature} + expect(subject.verify).to eq(true) + end + end end end From 214e2cbb230547faa3065a7b0186a7d6d509c680 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Mon, 25 Feb 2019 08:54:36 -0700 Subject: [PATCH 08/10] helpers for generating private keys --- lib/ssh_data/private_key/base.rb | 16 ++++++++++++++++ lib/ssh_data/private_key/dsa.rb | 12 ++++++++++++ lib/ssh_data/private_key/ecdsa.rb | 19 +++++++++++++++++++ lib/ssh_data/private_key/ed25519.rb | 7 +++++++ lib/ssh_data/private_key/rsa.rb | 21 +++++++++++++++++++++ lib/ssh_data/public_key/ecdsa.rb | 22 +++++++++++++--------- spec/certificate_spec.rb | 8 ++++---- spec/private_key/dsa_spec.rb | 6 ++++++ spec/private_key/ecdsa_spec.rb | 12 ++++++++++++ spec/private_key/ed25519_spec.rb | 6 ++++++ spec/private_key/rsa_spec.rb | 18 ++++++++++++++++++ 11 files changed, 134 insertions(+), 13 deletions(-) diff --git a/lib/ssh_data/private_key/base.rb b/lib/ssh_data/private_key/base.rb index d14523e..8347b9f 100644 --- a/lib/ssh_data/private_key/base.rb +++ b/lib/ssh_data/private_key/base.rb @@ -7,6 +7,22 @@ def initialize(**kwargs) @algo = kwargs[:algo] @comment = kwargs[:comment] end + + # Generate a new private key. + # + # Returns a PublicKey::Base subclass instance. + def self.generate(**kwargs) + raise "implement me" + end + + # Make an SSH signature. + # + # signed_data - The String message over which to calculated the signature. + # + # Returns a binary String signature. + def sign(signed_data) + raise "implement me" + end end end end diff --git a/lib/ssh_data/private_key/dsa.rb b/lib/ssh_data/private_key/dsa.rb index 6d45f1f..e9c3fd0 100644 --- a/lib/ssh_data/private_key/dsa.rb +++ b/lib/ssh_data/private_key/dsa.rb @@ -3,6 +3,18 @@ module PrivateKey class DSA < Base attr_reader :p, :q, :g, :x, :y, :openssl + # Generate a new private key. + # + # Returns a PublicKey::Base subclass instance. + def self.generate + from_openssl(OpenSSL::PKey::DSA.generate(1024)) + end + + # Import an openssl private key. + # + # key - An OpenSSL::PKey::DSA instance. + # + # Returns a DSA instance. def self.from_openssl(key) new( algo: PublicKey::ALGO_DSA, diff --git a/lib/ssh_data/private_key/ecdsa.rb b/lib/ssh_data/private_key/ecdsa.rb index b571529..5a24c3f 100644 --- a/lib/ssh_data/private_key/ecdsa.rb +++ b/lib/ssh_data/private_key/ecdsa.rb @@ -3,6 +3,25 @@ module PrivateKey class ECDSA < Base attr_reader :curve, :public_key_bytes, :private_key_bytes, :openssl + # Generate a new private key. + # + # curve - The String curve to use. One of SSHData::PublicKey::NISTP256, + # SSHData::PublicKey::NISTP384, or SSHData::PublicKey::NISTP521. + # + # Returns a PublicKey::Base subclass instance. + def self.generate(curve) + openssl_curve = PublicKey::ECDSA::OPENSSL_CURVE_NAME_FOR_CURVE[curve] + raise AlgorithmError, "unknown curve: #{curve}" if openssl_curve.nil? + + openssl_key = OpenSSL::PKey::EC.new(openssl_curve).tap(&:generate_key) + from_openssl(openssl_key) + end + + # Import an openssl private key. + # + # key - An OpenSSL::PKey::EC instance. + # + # Returns a DSA instance. def self.from_openssl(key) curve = PublicKey::ECDSA::CURVE_FOR_OPENSSL_CURVE_NAME[key.group.curve_name] algo = "ecdsa-sha2-#{curve}" diff --git a/lib/ssh_data/private_key/ed25519.rb b/lib/ssh_data/private_key/ed25519.rb index 414058d..374a4fd 100644 --- a/lib/ssh_data/private_key/ed25519.rb +++ b/lib/ssh_data/private_key/ed25519.rb @@ -3,6 +3,13 @@ module PrivateKey class ED25519 < Base attr_reader :pk, :sk, :ed25519_key + # Generate a new private key. + # + # Returns a PublicKey::Base subclass instance. + def self.generate + from_ed25519(Ed25519::SigningKey.generate) + end + # Create from a ::Ed25519::SigningKey instance. # # key - A ::Ed25519::SigningKey instance. diff --git a/lib/ssh_data/private_key/rsa.rb b/lib/ssh_data/private_key/rsa.rb index 70cdcec..3f7a3b4 100644 --- a/lib/ssh_data/private_key/rsa.rb +++ b/lib/ssh_data/private_key/rsa.rb @@ -3,6 +3,27 @@ module PrivateKey class RSA < Base attr_reader :n, :e, :d, :iqmp, :p, :q, :openssl + + # Generate a new private key. + # + # size - The Integer key size to generate. + # unsafe_allow_small_key: - Bool of whether to allow keys of less than + # 2048 bits. + # + # Returns a PublicKey::Base subclass instance. + def self.generate(size, unsafe_allow_small_key: false) + unless size >= 2048 || unsafe_allow_small_key + raise AlgorithmError, "key too small" + end + + from_openssl(OpenSSL::PKey::RSA.generate(size)) + end + + # Import an openssl private key. + # + # key - An OpenSSL::PKey::DSA instance. + # + # Returns a DSA instance. def self.from_openssl(key) new( algo: PublicKey::ALGO_RSA, diff --git a/lib/ssh_data/public_key/ecdsa.rb b/lib/ssh_data/public_key/ecdsa.rb index cd23db8..ea46e52 100644 --- a/lib/ssh_data/public_key/ecdsa.rb +++ b/lib/ssh_data/public_key/ecdsa.rb @@ -3,22 +3,26 @@ module PublicKey class ECDSA < Base attr_reader :curve, :public_key_bytes, :openssl + NISTP256 = "nistp256" + NISTP384 = "nistp384" + NISTP521 = "nistp521" + OPENSSL_CURVE_NAME_FOR_CURVE = { - "nistp256" => "prime256v1", - "nistp384" => "secp384r1", - "nistp521" => "secp521r1", + NISTP256 => "prime256v1", + NISTP384 => "secp384r1", + NISTP521 => "secp521r1", } CURVE_FOR_OPENSSL_CURVE_NAME = { - "prime256v1" => "nistp256", - "secp384r1" => "nistp384", - "secp521r1" => "nistp521", + "prime256v1" => NISTP256, + "secp384r1" => NISTP384, + "secp521r1" => NISTP521, } DIGEST_FOR_CURVE = { - "nistp256" => OpenSSL::Digest::SHA256, - "nistp384" => OpenSSL::Digest::SHA384, - "nistp521" => OpenSSL::Digest::SHA512, + NISTP256 => OpenSSL::Digest::SHA256, + NISTP384 => OpenSSL::Digest::SHA384, + NISTP521 => OpenSSL::Digest::SHA512, } # Convert an SSH encoded ECDSA signature to DER encoding for verification with diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index 9c9f1b3..89a4aa9 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -1,10 +1,10 @@ require_relative "./spec_helper" describe SSHData::Certificate do - let(:rsa_ca) { SSHData::PrivateKey::RSA.from_openssl(OpenSSL::PKey::RSA.generate(2048)) } - let(:dsa_ca) { SSHData::PrivateKey::DSA.from_openssl(OpenSSL::PKey::DSA.generate(1024)) } - let(:ecdsa_ca) { SSHData::PrivateKey::ECDSA.from_openssl(OpenSSL::PKey::EC.new("prime256v1").tap(&:generate_key)) } - let(:ed25519_ca) { SSHData::PrivateKey::ED25519.from_ed25519(Ed25519::SigningKey.generate) } + let(:rsa_ca) { SSHData::PrivateKey::RSA.generate(2048) } + let(:dsa_ca) { SSHData::PrivateKey::DSA.generate } + let(:ecdsa_ca) { SSHData::PrivateKey::ECDSA.generate("nistp256") } + let(:ed25519_ca) { SSHData::PrivateKey::ED25519.generate } it "supports the deprecated Certificate.parse method" do expect { diff --git a/spec/private_key/dsa_spec.rb b/spec/private_key/dsa_spec.rb index f26bbc4..2de4673 100644 --- a/spec/private_key/dsa_spec.rb +++ b/spec/private_key/dsa_spec.rb @@ -10,6 +10,12 @@ subject { described_class.from_openssl(private_key) } + it "can be generated" do + expect { + described_class.generate + }.not_to raise_error + end + it "can sign messages" do expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) end diff --git a/spec/private_key/ecdsa_spec.rb b/spec/private_key/ecdsa_spec.rb index a58148d..05c8d62 100644 --- a/spec/private_key/ecdsa_spec.rb +++ b/spec/private_key/ecdsa_spec.rb @@ -3,6 +3,12 @@ describe SSHData::PrivateKey::ECDSA do let(:openssh_key) { SSHData::PrivateKey.parse(fixture("ecdsa_leaf_for_rsa_ca")) } + it "can raises AlgorithmError for unknown curves" do + expect { + described_class.generate("foo") + }.to raise_error(SSHData::AlgorithmError) + end + it "can parse openssh-generate keys" do keys = openssh_key expect(keys).to be_an(Array) @@ -21,6 +27,12 @@ subject { described_class.from_openssl(private_key) } + it "can be generated" do + expect { + described_class.generate(ssh_curve) + }.not_to raise_error + end + it "can sign messages" do expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) end diff --git a/spec/private_key/ed25519_spec.rb b/spec/private_key/ed25519_spec.rb index 3f9614a..a411115 100644 --- a/spec/private_key/ed25519_spec.rb +++ b/spec/private_key/ed25519_spec.rb @@ -17,6 +17,12 @@ ) end + it "can be generated" do + expect { + described_class.generate + }.not_to raise_error + end + it "can sign messages" do expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) end diff --git a/spec/private_key/rsa_spec.rb b/spec/private_key/rsa_spec.rb index b28000e..1352758 100644 --- a/spec/private_key/rsa_spec.rb +++ b/spec/private_key/rsa_spec.rb @@ -10,6 +10,24 @@ subject { described_class.from_openssl(private_key) } + it "can be generated" do + expect { + described_class.generate(2048) + }.not_to raise_error + end + + it "raises AlgorithmError on small key sizes" do + expect { + described_class.generate(1024) + }.to raise_error(SSHData::AlgorithmError) + end + + it "can generate small keys if unsafe_allow_small_key is passed" do + expect { + described_class.generate(1024, unsafe_allow_small_key: true) + }.not_to raise_error + end + it "can sign messages" do expect(subject.public_key.verify(message, subject.sign(message))).to eq(true) end From 8ee17d2716261e272be6c54bbccb670d0ad57408 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Mon, 25 Feb 2019 09:04:44 -0700 Subject: [PATCH 09/10] helper for issuing certificates using private keys --- lib/ssh_data/private_key/base.rb | 9 +++++++++ spec/private_key_spec.rb | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/lib/ssh_data/private_key/base.rb b/lib/ssh_data/private_key/base.rb index 8347b9f..79bbbba 100644 --- a/lib/ssh_data/private_key/base.rb +++ b/lib/ssh_data/private_key/base.rb @@ -23,6 +23,15 @@ def self.generate(**kwargs) def sign(signed_data) raise "implement me" end + + # Issue a certificate using this private key. + # + # kwargs - See SSHData::Certificate.new. + # + # Returns a SSHData::Certificate instance. + def issue_certificate(**kwargs) + Certificate.new(**kwargs).tap { |c| c.sign(self) } + end end end end diff --git a/spec/private_key_spec.rb b/spec/private_key_spec.rb index a11d2eb..a5caaac 100644 --- a/spec/private_key_spec.rb +++ b/spec/private_key_spec.rb @@ -21,6 +21,11 @@ it "generates a SHA256 fingerprint matching ssh-keygen" do expect(subject.public_key.fingerprint).to eq(sha256_fpr) end + + it "can issue a certificate" do + cert_key = SSHData::PrivateKey::ECDSA.generate("nistp256").public_key + subject.issue_certificate(public_key: cert_key, key_id: "some ident") + end end end From 78dbb1b2d6faae47826f001bf515317455f92dd4 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Mon, 25 Feb 2019 09:14:01 -0700 Subject: [PATCH 10/10] address feedback from @ptoomey3 --- lib/ssh_data.rb | 1 - lib/ssh_data/certificate.rb | 8 +++++--- spec/certificate_spec.rb | 4 ++-- spec/encoding_spec.rb | 32 ++++++++++++++++---------------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/ssh_data.rb b/lib/ssh_data.rb index f164107..0097a63 100644 --- a/lib/ssh_data.rb +++ b/lib/ssh_data.rb @@ -1,6 +1,5 @@ require "openssl" require "base64" -require "securerandom" module SSHData # Break down a key in OpenSSH authorized_keys format (see sshd(8) manual diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index 21d23a8..3925297 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -1,8 +1,10 @@ +require "securerandom" + module SSHData class Certificate # Special values for valid_before and valid_after. - FOREVER = Time.at(0) - ALWAYS = Time.at((2**64)-1) + BEGINNING_OF_TIME = Time.at(0) + END_OF_TIME = Time.at((2**64)-1) # Integer certificate types TYPE_USER = 1 @@ -95,7 +97,7 @@ def self.parse_rfc4253(raw, unsafe_no_verify: false) # signature: - The certificate's String signature field. # # Returns nothing. - def initialize(public_key:, key_id:, algo: nil, nonce: nil, serial: 0, type: TYPE_USER, valid_principals: [], valid_after: FOREVER, valid_before: ALWAYS, critical_options: {}, extensions: {}, reserved: "", ca_key: nil, signature: "") + def initialize(public_key:, key_id:, algo: nil, nonce: nil, serial: 0, type: TYPE_USER, valid_principals: [], valid_after: BEGINNING_OF_TIME, valid_before: END_OF_TIME, critical_options: {}, extensions: {}, reserved: "", ca_key: nil, signature: "") @algo = algo || Encoding::CERT_ALGO_BY_PUBLIC_KEY_ALGO[public_key.algo] @nonce = nonce || SecureRandom.random_bytes(32) @public_key = public_key diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index 89a4aa9..7aa2202 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -137,8 +137,8 @@ expect(subject.type).to eq(SSHData::Certificate::TYPE_USER) expect(subject.key_id).to eq("my-ident") expect(subject.valid_principals).to eq(["p1", "p2"]) - expect(subject.valid_after).to eq(SSHData::Certificate::FOREVER) - expect(subject.valid_before).to eq(SSHData::Certificate::ALWAYS) + expect(subject.valid_after).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(subject.valid_before).to eq(SSHData::Certificate::END_OF_TIME) expect(subject.critical_options).to eq({"foo" => "bar"}) expect(subject.extensions).to eq({"permit-X11-forwarding" => true, "baz" => "qwer"}) expect(subject.reserved).to eq("") diff --git a/spec/encoding_spec.rb b/spec/encoding_spec.rb index 630bbad..52d408d 100644 --- a/spec/encoding_spec.rb +++ b/spec/encoding_spec.rb @@ -378,8 +378,8 @@ expect(rsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(rsa_data[:key_id]).to eq("my-ident") expect(rsa_data[:valid_principals]).to eq(["p1", "p2"]) - expect(rsa_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(rsa_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(rsa_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(rsa_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(rsa_data[:critical_options]).to eq({"foo"=>"bar"}) expect(rsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(rsa_data[:reserved]).to eq("") @@ -406,8 +406,8 @@ expect(dsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(dsa_data[:key_id]).to eq("my-ident") expect(dsa_data[:valid_principals]).to eq(["p1", "p2"]) - expect(dsa_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(dsa_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(dsa_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(dsa_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(dsa_data[:critical_options]).to eq({"foo"=>"bar"}) expect(dsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(dsa_data[:reserved]).to eq("") @@ -432,8 +432,8 @@ expect(ecdsa_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ecdsa_data[:key_id]).to eq("my-ident") expect(ecdsa_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ecdsa_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(ecdsa_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(ecdsa_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(ecdsa_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(ecdsa_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ecdsa_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ecdsa_data[:reserved]).to eq("") @@ -457,8 +457,8 @@ expect(ed25519_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ed25519_data[:key_id]).to eq("my-ident") expect(ed25519_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ed25519_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(ed25519_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(ed25519_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(ed25519_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(ed25519_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ed25519_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ed25519_data[:reserved]).to eq("") @@ -483,8 +483,8 @@ expect(rsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(rsa_ca_data[:key_id]).to eq("my-ident") expect(rsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(rsa_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(rsa_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(rsa_ca_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(rsa_ca_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(rsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(rsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(rsa_ca_data[:reserved]).to eq("") @@ -509,8 +509,8 @@ expect(dsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(dsa_ca_data[:key_id]).to eq("my-ident") expect(dsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(dsa_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(dsa_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(dsa_ca_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(dsa_ca_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(dsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(dsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(dsa_ca_data[:reserved]).to eq("") @@ -533,8 +533,8 @@ expect(ecdsa_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ecdsa_ca_data[:key_id]).to eq("my-ident") expect(ecdsa_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ecdsa_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(ecdsa_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(ecdsa_ca_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(ecdsa_ca_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(ecdsa_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ecdsa_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ecdsa_ca_data[:reserved]).to eq("") @@ -557,8 +557,8 @@ expect(ed25519_ca_data[:type]).to eq(SSHData::Certificate::TYPE_USER) expect(ed25519_ca_data[:key_id]).to eq("my-ident") expect(ed25519_ca_data[:valid_principals]).to eq(["p1", "p2"]) - expect(ed25519_ca_data[:valid_after]).to eq(SSHData::Certificate::FOREVER) - expect(ed25519_ca_data[:valid_before]).to eq(SSHData::Certificate::ALWAYS) + expect(ed25519_ca_data[:valid_after]).to eq(SSHData::Certificate::BEGINNING_OF_TIME) + expect(ed25519_ca_data[:valid_before]).to eq(SSHData::Certificate::END_OF_TIME) expect(ed25519_ca_data[:critical_options]).to eq({"foo"=>"bar"}) expect(ed25519_ca_data[:extensions]).to eq({"permit-X11-forwarding"=>true, "baz"=>"qwer"}) expect(ed25519_ca_data[:reserved]).to eq("")