From cfc5029bfbac4406e41b133f757ed12578d32076 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Thu, 21 Feb 2019 16:27:00 -0700 Subject: [PATCH] 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 | 231 ++++++++++++----------------- 8 files changed, 143 insertions(+), 146 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 @@ module SSHData @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 @@ module SSHData 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 @@ module SSHData 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 @@ module SSHData 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 @@ module SSHData 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 @@ module SSHData 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 @@ module SSHData 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 @@ module SSHData 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 @@ describe SSHData::Certificate do 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 @@ describe SSHData::Certificate do 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 @@ describe SSHData::Certificate do 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 + test_cases = [] - 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 + 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 + ] - 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 + 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 + ] - 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 + 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 + ] - 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 + 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 + ] - 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 + 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 + ] - 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 + 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 + ] - 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 << [ + :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