diff --git a/README.md b/README.md index eace9f5..91cedbc 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ gem install ssh_data require "ssh_data" key_data = File.read("~/.ssh/id_rsa.pub") -key = SSHData::PublicKey.parse(key_data) +key = SSHData::PublicKey.parse_openssh(key_data) #=> cert_data = = File.read("~/.ssh/id_rsa-cert.pub") -cert = SSHData::Certificate.parse(cert_data) +cert = SSHData::Certificate.parse_openssh(cert_data) #=> cert.key_id diff --git a/lib/ssh_data.rb b/lib/ssh_data.rb index ad69858..0097a63 100644 --- a/lib/ssh_data.rb +++ b/lib/ssh_data.rb @@ -2,15 +2,16 @@ require "base64" module SSHData - # Break down a public key or certificate into its algorith, raw key, and host. + # Break down a key in OpenSSH authorized_keys format (see sshd(8) manual + # page). # - # key - An SSH formatted public key or certificate, including algo, encoded - # key and optional user/host names. + # key - An OpenSSH formatted public key or certificate, including algo, + # base64 encoded key and optional comment. # # Returns an Array containing the algorithm String , the raw key or - # certificate String and the host String or nil. + # certificate String and the comment String or nil. def key_parts(key) - algo, b64, host = key.strip.split(" ", 3) + algo, b64, comment = key.strip.split(" ", 3) if algo.nil? || b64.nil? raise DecodeError, "bad data format" end @@ -21,7 +22,7 @@ def key_parts(key) raise DecodeError, "bad data format" end - [algo, raw, host] + [algo, raw, comment] end extend self diff --git a/lib/ssh_data/certificate.rb b/lib/ssh_data/certificate.rb index e07a594..97ef2a4 100644 --- a/lib/ssh_data/certificate.rb +++ b/lib/ssh_data/certificate.rb @@ -12,21 +12,27 @@ class Certificate ALGO_ECDSA521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com" ALGO_ED25519 = "ssh-ed25519-cert-v01@openssh.com" + ALGOS = [ + ALGO_RSA, ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521, + ALGO_ED25519 + ] + attr_reader :algo, :nonce, :public_key, :serial, :type, :key_id, :valid_principals, :valid_after, :valid_before, :critical_options, :extensions, :reserved, :ca_key, :signature - # Parse an SSH certificate. + # Parse an OpenSSH certificate in authorized_keys format (see sshd(8) manual + # page). # - # cert - An SSH formatted certificate, including key algo, - # encoded key and optional user/host names. + # cert - An OpenSSH formatted certificate, including key algo, + # base64 encoded key and optional comment. # unsafe_no_verify: - Bool of whether to skip verifying certificate signature # (Default false) # # Returns a Certificate instance. - def self.parse(cert, unsafe_no_verify: false) + def self.parse_openssh(cert, unsafe_no_verify: false) algo, raw, _ = SSHData.key_parts(cert) - parsed = parse_raw(raw, unsafe_no_verify: unsafe_no_verify) + parsed = parse_rfc4253(raw, unsafe_no_verify: unsafe_no_verify) if parsed.algo != algo raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}" @@ -35,14 +41,17 @@ def self.parse(cert, unsafe_no_verify: false) parsed end - # Parse an SSH certificate. + # Deprecated + singleton_class.send(:alias_method, :parse, :parse_openssh) + + # Parse an RFC 4253 binary SSH certificate. # - # cert - A raw binary certificate String. + # cert - A RFC 4253 binary certificate String. # unsafe_no_verify: - Bool of whether to skip verifying certificate # signature (Default false) # # Returns a Certificate instance. - def self.parse_raw(raw, unsafe_no_verify: false) + def self.parse_rfc4253(raw, unsafe_no_verify: false) data, read = Encoding.decode_certificate(raw) if read != raw.bytesize diff --git a/lib/ssh_data/private_key.rb b/lib/ssh_data/private_key.rb index 934599e..af90836 100644 --- a/lib/ssh_data/private_key.rb +++ b/lib/ssh_data/private_key.rb @@ -8,7 +8,7 @@ module PrivateKey # Parse an SSH private key. # - # key - An PEM encoded OpenSSH private key. + # key - A PEM or OpenSSH encoded private key. # # Returns an Array of PrivateKey::Base subclass instances. def self.parse(key) @@ -31,6 +31,11 @@ def self.parse(key) raise DecodeError, "bad private key. maybe encrypted?" end + # Parse an OpenSSH formatted private key. + # + # key - An OpenSSH encoded private key. + # + # Returns an Array of PrivateKey::Base subclass instances. def self.parse_openssh(key) raw = Encoding.decode_pem(key, OPENSSH_PEM_TYPE) diff --git a/lib/ssh_data/public_key.rb b/lib/ssh_data/public_key.rb index fb509e8..56f89cf 100644 --- a/lib/ssh_data/public_key.rb +++ b/lib/ssh_data/public_key.rb @@ -8,15 +8,21 @@ module PublicKey ALGO_ECDSA521 = "ecdsa-sha2-nistp521" ALGO_ED25519 = "ssh-ed25519" - # Parse an SSH public key. + ALGOS = [ + ALGO_RSA, ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521, + ALGO_ED25519 + ] + + # Parse an OpenSSH public key in authorized_keys format (see sshd(8) manual + # page). # - # key - An SSH formatted public key, including algo, encoded key and optional - # user/host names. + # key - An OpenSSH formatted public key, including algo, base64 encoded key + # and optional comment. # # Returns a PublicKey::Base subclass instance. - def self.parse(key) + def self.parse_openssh(key) algo, raw, _ = SSHData.key_parts(key) - parsed = parse_raw(raw) + parsed = parse_rfc4253(raw) if parsed.algo != algo raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}" @@ -25,12 +31,15 @@ def self.parse(key) parsed end - # Parse an SSH public key. + # Deprecated + singleton_class.send(:alias_method, :parse, :parse_openssh) + + # Parse an RFC 4253 binary SSH public key. # - # key - A raw binary public key String. + # key - A RFC 4253 binary public key String. # # Returns a PublicKey::Base subclass instance. - def self.parse_raw(raw) + def self.parse_rfc4253(raw) data, read = Encoding.decode_public_key(raw) if read != raw.bytesize diff --git a/lib/ssh_data/public_key/base.rb b/lib/ssh_data/public_key/base.rb index 7b2f63c..ce16e43 100644 --- a/lib/ssh_data/public_key/base.rb +++ b/lib/ssh_data/public_key/base.rb @@ -40,6 +40,15 @@ def raw raise "implement me" end + # OpenSSH public key 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(raw), comment].compact.join(" ") + end + # Is this public key equal to another public key? # # other - Another SSHData::PublicKey::Base instance to compare with. diff --git a/spec/certificate_spec.rb b/spec/certificate_spec.rb index c6f4f2d..c2fc29a 100644 --- a/spec/certificate_spec.rb +++ b/spec/certificate_spec.rb @@ -1,60 +1,66 @@ require_relative "./spec_helper" describe SSHData::Certificate do - let(:rsa_cert) { described_class.parse(fixture("rsa_leaf_for_rsa_ca-cert.pub")) } - let(:dsa_cert) { described_class.parse(fixture("dsa_leaf_for_rsa_ca-cert.pub")) } - let(:ecdsa_cert) { described_class.parse(fixture("ecdsa_leaf_for_rsa_ca-cert.pub")) } - let(:ed25519_cert) { described_class.parse(fixture("ed25519_leaf_for_rsa_ca-cert.pub")) } + 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(fixture("rsa_leaf_for_rsa_ca-cert.pub")) } - let(:dsa_ca_cert) { described_class.parse(fixture("rsa_leaf_for_dsa_ca-cert.pub")) } - let(:ecdsa_ca_cert) { described_class.parse(fixture("rsa_leaf_for_ecdsa_ca-cert.pub")) } - let(:ed25519_ca_cert) { described_class.parse(fixture("rsa_leaf_for_ed25519_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) } + it "supports the deprecated Certificate.parse method" do + expect { + described_class.parse(fixture("rsa_leaf_for_rsa_ca-cert.pub")) + }.not_to raise_error + end + it "raises on invalid signatures" do expect { - described_class.parse(fixture("bad_signature-cert.pub")) + described_class.parse_openssh(fixture("bad_signature-cert.pub")) }.to raise_error(SSHData::VerifyError) end it "doesn't validate signatures if provided unsafe_no_verify flag" do expect { - described_class.parse(fixture("bad_signature-cert.pub"), + described_class.parse_openssh(fixture("bad_signature-cert.pub"), unsafe_no_verify: true ) }.not_to raise_error end it "raises on trailing data" do - algo, b64, host = fixture("rsa_leaf_for_rsa_ca-cert.pub").split(" ", 3) + algo, b64, comment = fixture("rsa_leaf_for_rsa_ca-cert.pub").split(" ", 3) raw = Base64.decode64(b64) raw += "foobar" b64 = Base64.strict_encode64(raw) - cert = [algo, b64, host].join(" ") + cert = [algo, b64, comment].join(" ") expect { - described_class.parse(cert, unsafe_no_verify: true) + described_class.parse_openssh(cert, unsafe_no_verify: true) }.to raise_error(SSHData::DecodeError) end it "raises on type mismatch" do - _, b64, host = fixture("rsa_leaf_for_rsa_ca-cert.pub").split(" ", 3) - cert = [SSHData::Certificate::ALGO_ED25519, b64, host].join(" ") + _, b64, comment = fixture("rsa_leaf_for_rsa_ca-cert.pub").split(" ", 3) + cert = [SSHData::Certificate::ALGO_ED25519, b64, comment].join(" ") expect { - described_class.parse(cert, unsafe_no_verify: true) + described_class.parse_openssh(cert, unsafe_no_verify: true) }.to raise_error(SSHData::DecodeError) end - it "doesn't require the user/host names" do + it "doesn't require the comment" do type, b64, _ = fixture("rsa_leaf_for_rsa_ca-cert.pub").split(" ", 3) cert = [type, b64].join(" ") expect { - described_class.parse(cert, unsafe_no_verify: true) + described_class.parse_openssh(cert, unsafe_no_verify: true) }.not_to raise_error end diff --git a/spec/encoding_spec.rb b/spec/encoding_spec.rb index 2933a9b..d24cbf4 100644 --- a/spec/encoding_spec.rb +++ b/spec/encoding_spec.rb @@ -110,7 +110,7 @@ expect(rsa_data[:public_keys]).to be_a(Array) expect(rsa_data[:public_keys].length).to eq(1) expect { - SSHData::PublicKey.parse_raw(rsa_data[:public_keys].first) + SSHData::PublicKey.parse_rfc4253(rsa_data[:public_keys].first) }.not_to raise_error expect(rsa_data[:checkint1]).to be_a(Integer) @@ -133,7 +133,7 @@ expect(dsa_data[:public_keys]).to be_a(Array) expect(dsa_data[:public_keys].length).to eq(1) expect { - SSHData::PublicKey.parse_raw(dsa_data[:public_keys].first) + SSHData::PublicKey.parse_rfc4253(dsa_data[:public_keys].first) }.not_to raise_error expect(dsa_data[:checkint1]).to be_a(Integer) @@ -156,7 +156,7 @@ expect(ecdsa_data[:public_keys]).to be_a(Array) expect(ecdsa_data[:public_keys].length).to eq(1) expect { - SSHData::PublicKey.parse_raw(ecdsa_data[:public_keys].first) + SSHData::PublicKey.parse_rfc4253(ecdsa_data[:public_keys].first) }.not_to raise_error expect(ecdsa_data[:checkint1]).to be_a(Integer) @@ -179,7 +179,7 @@ expect(ed25519_data[:public_keys]).to be_a(Array) expect(ed25519_data[:public_keys].length).to eq(1) expect { - SSHData::PublicKey.parse_raw(ed25519_data[:public_keys].first) + SSHData::PublicKey.parse_rfc4253(ed25519_data[:public_keys].first) }.not_to raise_error expect(ed25519_data[:checkint1]).to be_a(Integer) diff --git a/spec/fixtures/gen.sh b/spec/fixtures/gen.sh index 888503f..a2b4d20 100755 --- a/spec/fixtures/gen.sh +++ b/spec/fixtures/gen.sh @@ -41,14 +41,14 @@ ruby <