diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index 1636c46..a804764 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -41,10 +41,12 @@ class SSHData::Certificate end # Parse data into better types, where possible. - data[:valid_after] = Time.at(data[:valid_after]) - data[:valid_before] = Time.at(data[:valid_before]) - data[:public_key] = SSHData::PublicKey.from_data(data.delete(:key_data)) - data[:valid_principals], _ = SSHData::Encoding.decode_strings(data[:valid_principals]) + valid_after = Time.at(data.delete(:valid_after)) + valid_before = Time.at(data.delete(:valid_before)) + public_key = SSHData::PublicKey.from_data(data.delete(:key_data)) + valid_principals, _ = SSHData::Encoding.decode_strings(data.delete(:valid_principals)) + critical_options, _ = SSHData::Encoding.decode_options(data.delete(:critical_options)) + extensions, _ = SSHData::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) @@ -52,7 +54,7 @@ class SSHData::Certificate if read != sk_raw.bytesize raise SSHData::DecodeError, "unexpected trailing data" end - data[:ca_key] = SSHData::PublicKey.from_data(sk_data) + ca_key = SSHData::PublicKey.from_data(sk_data) unless unsafe_no_verify # The signature is the last field. The signature is calculated over all @@ -60,12 +62,20 @@ class SSHData::Certificate signed_data_len = raw.bytesize - data[:signature].bytesize - 4 signed_data = raw.byteslice(0, signed_data_len) - unless data[:ca_key].verify(signed_data, data[:signature]) + unless ca_key.verify(signed_data, data[:signature]) raise SSHData::VerifyError end end - new(**data) + 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 # Intialize a new Certificate instance. @@ -80,11 +90,12 @@ class SSHData::Certificate # type: - The certificate's Integer type field (one of TYPE_USER # or TYPE_HOST). # key_id: - The certificate's String key_id field. - # valid_principals: - The certificate's String valid_principals field. + # valid_principals: - The Array of Strings valid_principles field from the + # certificate. # valid_after: - The certificate's Time valid_after field. # valid_before: - The certificate's Time valid_before field. - # critical_options: - The certificate's String critical_options field. - # extensions: - The certificate's String extensions field. + # critical_options: - The Hash critical_options field from the certificate. + # extensions: - The Hash extensions field from the certificate. # reserved: - The certificate's String reserved field. # ca_key: - The issuing CA's public key as a PublicKey::Base # subclass instance. diff --git a/lib/ssh_data/encoding.rb b/lib/ssh_data/encoding.rb index 8705d81..26cbbdf 100644 --- a/lib/ssh_data/encoding.rb +++ b/lib/ssh_data/encoding.rb @@ -214,7 +214,7 @@ module SSHData::Encoding # Read a series of strings out of the provided data. # - # data - A binary String. + # data - A binary String. # # Returns an Array including the Array of decoded Strings and the Integer # number of bytes read. @@ -231,6 +231,34 @@ module SSHData::Encoding [strs, total_read] end + # Read a series of key/value pairs out of the provided data. + # + # data - A binary String. + # + # Returns an Array including the Hash of decoded keys/values and the Integer + # number of bytes read. + def decode_options(data) + total_read = 0 + opts = {} + + while data.bytesize > total_read + key, read = decode_string(data, total_read) + total_read += read + + value_data, read = decode_string(data, total_read) + total_read += read + + value_str, read = decode_string(value_data) + if read != value_data.bytesize + raise SSHData::DecodeError, "bad options data" + end + + opts[key] = value_str + end + + [opts, total_read] + end + # Read a multi-precision integer from the provided data. # # data - A binary String. diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index a882239..5e138fa 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -68,8 +68,8 @@ describe SSHData::Certificate do expect(rsa_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(rsa_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(rsa_cert.critical_options).to eq({"foo" => "bar"}) + expect(rsa_cert.extensions).to eq({"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) @@ -85,8 +85,8 @@ describe SSHData::Certificate do expect(dsa_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(dsa_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(dsa_cert.critical_options).to eq({"foo" => "bar"}) + expect(dsa_cert.extensions).to eq({"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) @@ -102,8 +102,8 @@ describe SSHData::Certificate do expect(ecdsa_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ecdsa_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(ecdsa_cert.critical_options).to eq({"foo" => "bar"}) + expect(ecdsa_cert.extensions).to eq({"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) @@ -119,8 +119,8 @@ describe SSHData::Certificate do expect(ed25519_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ed25519_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(ed25519_cert.critical_options).to eq({"foo" => "bar"}) + expect(ed25519_cert.extensions).to eq({"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) @@ -136,8 +136,8 @@ describe SSHData::Certificate do expect(rsa_ca_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(rsa_ca_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(rsa_ca_cert.critical_options).to eq({"foo" => "bar"}) + expect(rsa_ca_cert.extensions).to eq({"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) @@ -153,8 +153,8 @@ describe SSHData::Certificate do expect(dsa_ca_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(dsa_ca_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(dsa_ca_cert.critical_options).to eq({"foo" => "bar"}) + expect(dsa_ca_cert.extensions).to eq({"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) @@ -170,8 +170,8 @@ describe SSHData::Certificate do expect(ecdsa_ca_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ecdsa_ca_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(ecdsa_ca_cert.critical_options).to eq({"foo" => "bar"}) + expect(ecdsa_ca_cert.extensions).to eq({"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) @@ -187,8 +187,8 @@ describe SSHData::Certificate do expect(ed25519_ca_cert.valid_principals).to eq(["my-principal"]) 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("\x00\x00\x00\x03foo\x00\x00\x00\x07\x00\x00\x00\x03bar") - expect(ed25519_ca_cert.extensions).to eq("\x00\x00\x00\x03baz\x00\x00\x00\x08\x00\x00\x00\x04qwer") + expect(ed25519_ca_cert.critical_options).to eq({"foo" => "bar"}) + expect(ed25519_ca_cert.extensions).to eq({"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) diff --git a/spec/encoding_spec.rb b/spec/encoding_spec.rb index 76fb168..d551a76 100644 --- a/spec/encoding_spec.rb +++ b/spec/encoding_spec.rb @@ -17,6 +17,24 @@ describe SSHData::Encoding do let(:ecdsa_ca_data) { described_class.decode_certificate(fixture("rsa_leaf_for_ecdsa_ca-cert.pub", binary: true)).first } let(:ed25519_ca_data) { described_class.decode_certificate(fixture("rsa_leaf_for_ed25519_ca-cert.pub", binary: true)).first } + it "can decode options" do + opts = {"k1" => "v1", "k2" => "v2"} + encoded = opts.reduce("") do |cum, (k, v)| + cum + [ + described_class.encode_string(k), + described_class.encode_string(described_class.encode_string(v)) + ].join + end + + decoded, read = described_class.decode_options(encoded) + expect(decoded).to eq(opts) + expect(read).to eq(encoded.bytesize) + + decoded, read = described_class.decode_options("") + expect(decoded).to eq({}) + expect(read).to eq(0) + end + it "can decode a series of strings" do strs = %w(one two three) encoded = strs.map { |s| described_class.encode_string(s) }.join