Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
#=> <SSHData::PublicKey::RSA>

cert_data = = File.read("~/.ssh/id_rsa-cert.pub")
cert = SSHData::Certificate.parse(cert_data)
cert = SSHData::Certificate.parse_openssh(cert_data)
#=> <SSHData::PublicKey::Certificate>

cert.key_id
Expand Down
13 changes: 7 additions & 6 deletions lib/ssh_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,7 +22,7 @@ def key_parts(key)
raise DecodeError, "bad data format"
end

[algo, raw, host]
[algo, raw, comment]
end

extend self
Expand Down
25 changes: 17 additions & 8 deletions lib/ssh_data/certificate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion lib/ssh_data/private_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
25 changes: 17 additions & 8 deletions lib/ssh_data/public_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we might not want to throw away the comment such that .openssh can round-trip an originally parsed key easily?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wouldn't hurt to keep it. The comment is OpenSSH specific though, so it seems "cleaner" to not keep it around...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

parsed = parse_raw(raw)
parsed = parse_rfc4253(raw)

if parsed.algo != algo
raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}"
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions lib/ssh_data/public_key/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 24 additions & 18 deletions spec/certificate_spec.rb
Original file line number Diff line number Diff line change
@@ -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

Expand Down
8 changes: 4 additions & 4 deletions spec/encoding_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions spec/fixtures/gen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ ruby <<RUBY
require "base64"

encoded = File.read("rsa_leaf_for_ed25519_ca-cert.pub")
algo, b64, host = encoded.split(" ", 3)
algo, b64, comment = encoded.split(" ", 3)
raw = Base64.decode64(b64)

# we flip bits in the last byte, since that's where the signature is.
raw[-1] = (raw[-1].ord ^ 0xff).chr

b64 = Base64.strict_encode64(raw)
encoded = [algo, b64, host].join(" ")
encoded = [algo, b64, comment].join(" ")

File.open("bad_signature-cert.pub", "w") { |f| f.write(encoded) }
RUBY
4 changes: 2 additions & 2 deletions spec/public_key/dsa_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
let(:ssh_sig) { described_class.ssh_signature(openssl_sig) }
let(:sig) { SSHData::Encoding.encode_signature(SSHData::PublicKey::ALGO_DSA, ssh_sig) }

let(:openssh_key) { SSHData::PublicKey.parse(fixture("dsa_leaf_for_rsa_ca.pub")) }
let(:openssh_key) { SSHData::PublicKey.parse_openssh(fixture("dsa_leaf_for_rsa_ca.pub")) }

subject do
described_class.new(
Expand Down Expand Up @@ -92,7 +92,7 @@

it "can verify certificate signatures" do
expect {
SSHData::Certificate.parse(fixture("rsa_leaf_for_dsa_ca-cert.pub"),
SSHData::Certificate.parse_openssh(fixture("rsa_leaf_for_dsa_ca-cert.pub"),
unsafe_no_verify: false
)
}.not_to raise_error
Expand Down
8 changes: 4 additions & 4 deletions spec/public_key/ecdsa_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require_relative "../spec_helper"

describe SSHData::PublicKey::ECDSA do
let(:openssh_key) { SSHData::PublicKey.parse(fixture("ecdsa_leaf_for_rsa_ca.pub")) }
let(:openssh_key) { SSHData::PublicKey.parse_openssh(fixture("ecdsa_leaf_for_rsa_ca.pub")) }

it "can parse openssh-generate keys" do
expect { openssh_key }.not_to raise_error
Expand All @@ -13,7 +13,7 @@

it "can verify certificate signatures" do
expect {
SSHData::Certificate.parse(fixture("rsa_leaf_for_ecdsa_ca-cert.pub"),
SSHData::Certificate.parse_openssh(fixture("rsa_leaf_for_ecdsa_ca-cert.pub"),
unsafe_no_verify: false
)
}.not_to raise_error
Expand All @@ -29,7 +29,7 @@
].join)].join(" ")

expect {
SSHData::PublicKey.parse(malformed)
SSHData::PublicKey.parse_openssh(malformed)
}.to raise_error(SSHData::DecodeError)
end

Expand Down Expand Up @@ -107,7 +107,7 @@
].join)].join(" ")

expect {
SSHData::PublicKey.parse(malformed)
SSHData::PublicKey.parse_openssh(malformed)
}.to raise_error(SSHData::DecodeError)
end
end
Expand Down
Loading