This commit is contained in:
Ben Toews 2019-01-29 13:24:50 -07:00
Родитель 0f2ed098b1
Коммит fe36498374
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E9C423BE17EFEE70
13 изменённых файлов: 363 добавлений и 58 удалений

Просмотреть файл

@ -31,4 +31,5 @@ require "ssh_data/version"
require "ssh_data/error" require "ssh_data/error"
require "ssh_data/certificate" require "ssh_data/certificate"
require "ssh_data/public_key" require "ssh_data/public_key"
require "ssh_data/private_key"
require "ssh_data/encoding" require "ssh_data/encoding"

Просмотреть файл

@ -1,5 +1,25 @@
module SSHData module SSHData
module Encoding module Encoding
# Fields in an OpenSSL private key
# https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
OPENSSH_PRIVATE_KEY_MAGIC = "openssh-key-v1\x00"
OPENSSH_PRIVATE_KEY_FIELDS = [
[:ciphername, :string],
[:kdfname, :string],
[:kdfoptions, :string],
[:nkeys, :uint32],
]
# Fields in an RSA private key
RSA_PRIVATE_KEY_FIELDS = [
[:n, :mpint],
[:e, :mpint],
[:d, :mpint],
[:iqmp, :mpint],
[:p, :mpint],
[:q, :mpint],
]
# Fields in an RSA public key # Fields in an RSA public key
RSA_KEY_FIELDS = [ RSA_KEY_FIELDS = [
[:e, :mpint], [:e, :mpint],
@ -43,6 +63,97 @@ module SSHData
PublicKey::ALGO_ED25519 => ED25519_KEY_FIELDS, PublicKey::ALGO_ED25519 => ED25519_KEY_FIELDS,
} }
KEY_FIELDS_BY_PRIVATE_KEY_ALGO = {
PublicKey::ALGO_RSA => RSA_PRIVATE_KEY_FIELDS,
}
# Get the raw data from a PEM encoded blob.
#
# pem - The PEM encoded String to decode.
# type - The String PEM type we're expecting.
#
# Returns the decoded String.
def decode_pem(pem, type)
lines = pem.split("\n")
unless lines.shift == "-----BEGIN #{type}-----"
raise DecodeError, "bad PEM header"
end
unless lines.pop == "-----END #{type}-----"
raise DecodeError, "bad PEM footer"
end
Base64.strict_decode64(lines.join)
end
# Decode an OpenSSH private key.
#
# raw - The binary String private key.
#
# Returns an Array containing a Hash describing the private key and the
# Integer number of bytes read.
def decode_openssh_private_key(raw)
total_read = 0
magic = raw.byteslice(0, total_read + OPENSSH_PRIVATE_KEY_MAGIC.bytesize)
total_read += OPENSSH_PRIVATE_KEY_MAGIC.bytesize
unless magic == OPENSSH_PRIVATE_KEY_MAGIC
raise DecodeError, "bad OpenSSH private key"
end
data, read = decode_fields(raw, OPENSSH_PRIVATE_KEY_FIELDS, total_read)
total_read += read
# TODO: add support for encrypted private keys
unless data[:ciphername] == "none" && data[:kdfname] == "none"
raise DecryptError, "cannot decode encrypted private keys"
end
data[:public_keys], read = decode_n_strings(raw, data[:nkeys], total_read)
total_read += read
privs, read = decode_string(raw, total_read)
total_read += read
privs_read = 0
checkint1, read = decode_uint32(privs, privs_read)
privs_read += read
checkint2, read = decode_uint32(privs, privs_read)
privs_read += read
unless checkint1 == checkint2
raise DecryptError, "bad private key checksum"
end
data[:private_keys] = data[:nkeys].times.map do
algo, read = decode_string(privs, privs_read)
privs_read += read
unless fields = KEY_FIELDS_BY_PRIVATE_KEY_ALGO[algo]
raise AlgorithmError, "unknown algorithm: #{algo.inspect}"
end
priv_data, read = decode_fields(privs, fields, privs_read)
privs_read += read
comment, read = decode_string(privs, privs_read)
privs_read += read
priv_data.merge(algo: algo, comment: comment)
end
# padding at end is bytes 1, 2, 3, 4, etc...
padding = privs.byteslice(privs_read..-1)
unless padding.bytes.each_with_index.all? { |b, i| b == (i + 1) % 255 }
raise DecodeError, "bad padding: #{padding.inspect}"
end
[data, total_read]
end
# Decode the signature. # Decode the signature.
# #
# raw - The binary String signature as described by RFC4253 section 6.6. # raw - The binary String signature as described by RFC4253 section 6.6.
@ -51,7 +162,7 @@ module SSHData
# #
# Returns an Array containing the decoded algorithm String, the decoded binary # Returns an Array containing the decoded algorithm String, the decoded binary
# signature String, and the Integer number of bytes read. # signature String, and the Integer number of bytes read.
def self.decode_signature(raw, offset=0) def decode_signature(raw, offset=0)
total_read = 0 total_read = 0
algo, read = decode_string(raw, offset + total_read) algo, read = decode_string(raw, offset + total_read)
@ -82,7 +193,7 @@ module SSHData
# #
# Returns an Array containing a Hash describing the public key and the # Returns an Array containing a Hash describing the public key and the
# Integer number of bytes read. # Integer number of bytes read.
def self.decode_public_key(raw, algo=nil, offset=0) def decode_public_key(raw, algo=nil, offset=0)
total_read = 0 total_read = 0
if algo.nil? if algo.nil?
@ -91,7 +202,7 @@ module SSHData
end end
unless fields = KEY_FIELDS_BY_PUBLIC_KEY_ALGO[algo] unless fields = KEY_FIELDS_BY_PUBLIC_KEY_ALGO[algo]
raise AlgorithmError raise AlgorithmError, "unknown algorithm: #{algo}"
end end
data, read = decode_fields(raw, fields, offset + total_read) data, read = decode_fields(raw, fields, offset + total_read)
@ -110,7 +221,7 @@ module SSHData
# #
# Returns an Array containing a Hash describing the certificate and the # Returns an Array containing a Hash describing the certificate and the
# Integer number of bytes read. # Integer number of bytes read.
def self.decode_certificate(raw, offset=0) def decode_certificate(raw, offset=0)
total_read = 0 total_read = 0
data, read = decode_fields(raw, [ data, read = decode_fields(raw, [
@ -144,30 +255,30 @@ module SSHData
[data.merge(trailer), total_read] [data.merge(trailer), total_read]
end end
# Decode all of the given fields from data. # Decode all of the given fields from raw.
# #
# data - A binary String. # raw - A binary String.
# fields - An Array of Arrays, each containing a symbol describing the field # fields - An Array of Arrays, each containing a symbol describing the field
# and a Symbol describing the type of the field (:mpint, :string, # and a Symbol describing the type of the field (:mpint, :string,
# :uint64, or :uint32). # :uint64, or :uint32).
# offset - The offset into data at which to read (default 0). # offset - The offset into raw at which to read (default 0).
# #
# Returns an Array containing a Hash mapping the provided field keys to the # Returns an Array containing a Hash mapping the provided field keys to the
# decoded values and the Integer number of bytes read. # decoded values and the Integer number of bytes read.
def decode_fields(data, fields, offset=0) def decode_fields(raw, fields, offset=0)
hash = {} hash = {}
total_read = 0 total_read = 0
fields.each do |key, type| fields.each do |key, type|
value, read = case type value, read = case type
when :string when :string
decode_string(data, offset + total_read) decode_string(raw, offset + total_read)
when :mpint when :mpint
decode_mpint(data, offset + total_read) decode_mpint(raw, offset + total_read)
when :uint64 when :uint64
decode_uint64(data, offset + total_read) decode_uint64(raw, offset + total_read)
when :uint32 when :uint32
decode_uint32(data, offset + total_read) decode_uint32(raw, offset + total_read)
else else
raise DecodeError raise DecodeError
end end
@ -179,27 +290,27 @@ module SSHData
[hash, total_read] [hash, total_read]
end end
# Read a string out of the provided data. # Read a string out of the provided raw data.
# #
# data - A binary String. # raw - A binary String.
# offset - The offset into data at which to read (default 0). # offset - The offset into raw at which to read (default 0).
# #
# Returns an Array including the decoded String and the Integer number of # Returns an Array including the decoded String and the Integer number of
# bytes read. # bytes read.
def decode_string(data, offset=0) def decode_string(raw, offset=0)
if data.bytesize < offset + 4 if raw.bytesize < offset + 4
raise DecodeError, "data too short" raise DecodeError, "data too short"
end end
size_s = data.byteslice(offset, 4) size_s = raw.byteslice(offset, 4)
size = size_s.unpack("L>").first size = size_s.unpack("L>").first
if data.bytesize < offset + 4 + size if raw.bytesize < offset + 4 + size
raise DecodeError, "data too short" raise DecodeError, "data too short"
end end
string = data.byteslice(offset + 4, size) string = raw.byteslice(offset + 4, size)
[string, 4 + size] [string, 4 + size]
end end
@ -213,18 +324,19 @@ module SSHData
[string.bytesize, string].pack("L>A*") [string.bytesize, string].pack("L>A*")
end end
# Read a series of strings out of the provided data. # Read a series of strings out of the provided raw data.
# #
# data - A binary String. # raw - A binary String.
# offset - The offset into raw at which to read (default 0).
# #
# Returns an Array including the Array of decoded Strings and the Integer # Returns an Array including the Array of decoded Strings and the Integer
# number of bytes read. # number of bytes read.
def decode_strings(data) def decode_strings(raw, offset=0)
total_read = 0 total_read = 0
strs = [] strs = []
while data.bytesize > total_read while raw.bytesize > offset + total_read
str, read = decode_string(data, total_read) str, read = decode_string(raw, offset + total_read)
strs << str strs << str
total_read += read total_read += read
end end
@ -232,26 +344,46 @@ module SSHData
[strs, total_read] [strs, total_read]
end end
# Read a series of key/value pairs out of the provided data. # Read the specified number of strings out of the provided raw data.
# #
# data - A binary String. # raw - A binary String.
# n - The Integer number of Strings to read.
# offset - The offset into raw at which to read (default 0).
#
# Returns an Array including the Array of decoded Strings and the Integer
# number of bytes read.
def decode_n_strings(raw, n, offset=0)
total_read = 0
strs = []
n.times do |i|
strs[i], read = decode_string(raw, offset + total_read)
total_read += read
end
[strs, total_read]
end
# Read a series of key/value pairs out of the provided raw data.
#
# raw - A binary String.
# #
# Returns an Array including the Hash of decoded keys/values and the Integer # Returns an Array including the Hash of decoded keys/values and the Integer
# number of bytes read. # number of bytes read.
def decode_options(data) def decode_options(raw)
total_read = 0 total_read = 0
opts = {} opts = {}
while data.bytesize > total_read while raw.bytesize > total_read
key, read = decode_string(data, total_read) key, read = decode_string(raw, total_read)
total_read += read total_read += read
value_data, read = decode_string(data, total_read) value_raw, read = decode_string(raw, total_read)
total_read += read total_read += read
if value_data.bytesize > 0 if value_raw.bytesize > 0
opts[key], read = decode_string(value_data) opts[key], read = decode_string(value_raw)
if read != value_data.bytesize if read != value_raw.bytesize
raise DecodeError, "bad options data" raise DecodeError, "bad options data"
end end
else else
@ -262,27 +394,27 @@ module SSHData
[opts, total_read] [opts, total_read]
end end
# Read a multi-precision integer from the provided data. # Read a multi-precision integer from the provided raw data.
# #
# data - A binary String. # raw - A binary String.
# offset - The offset into data at which to read (default 0). # offset - The offset into raw at which to read (default 0).
# #
# Returns an Array including the decoded mpint as an OpenSSL::BN and the # Returns an Array including the decoded mpint as an OpenSSL::BN and the
# Integer number of bytes read. # Integer number of bytes read.
def decode_mpint(data, offset=0) def decode_mpint(raw, offset=0)
if data.bytesize < offset + 4 if raw.bytesize < offset + 4
raise DecodeError, "data too short" raise DecodeError, "data too short"
end end
str_size_s = data.byteslice(offset, 4) str_size_s = raw.byteslice(offset, 4)
str_size = str_size_s.unpack("L>").first str_size = str_size_s.unpack("L>").first
mpi_size = str_size + 4 mpi_size = str_size + 4
if data.bytesize < offset + mpi_size if raw.bytesize < offset + mpi_size
raise DecodeError, "data too short" raise DecodeError, "data too short"
end end
mpi_s = data.slice(offset, mpi_size) mpi_s = raw.slice(offset, mpi_size)
# This calls OpenSSL's BN_mpi2bn() function. As far as I can tell, this # 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 # matches up with with MPI type defined in RFC4251 Section 5 with the
@ -303,36 +435,36 @@ module SSHData
end end
# Read a uint64 from the provided data. # Read a uint64 from the provided raw data.
# #
# data - A binary String. # raw - A binary String.
# offset - The offset into data at which to read (default 0). # offset - The offset into raw at which to read (default 0).
# #
# Returns an Array including the decoded uint64 as an Integer and the # Returns an Array including the decoded uint64 as an Integer and the
# Integer number of bytes read. # Integer number of bytes read.
def decode_uint64(data, offset=0) def decode_uint64(raw, offset=0)
if data.bytesize < offset + 8 if raw.bytesize < offset + 8
raise DecodeError, "data too short" raise DecodeError, "data too short"
end end
uint64 = data.byteslice(offset, 8).unpack("Q>").first uint64 = raw.byteslice(offset, 8).unpack("Q>").first
[uint64, 8] [uint64, 8]
end end
# Read a uint32 from the provided data. # Read a uint32 from the provided raw data.
# #
# data - A binary String. # raw - A binary String.
# offset - The offset into data at which to read (default 0). # offset - The offset into raw at which to read (default 0).
# #
# Returns an Array including the decoded uint32 as an Integer and the # Returns an Array including the decoded uint32 as an Integer and the
# Integer number of bytes read. # Integer number of bytes read.
def decode_uint32(data, offset=0) def decode_uint32(raw, offset=0)
if data.bytesize < offset + 4 if raw.bytesize < offset + 4
raise DecodeError, "data too short" raise DecodeError, "data too short"
end end
uint32 = data.byteslice(offset, 4).unpack("L>").first uint32 = raw.byteslice(offset, 4).unpack("L>").first
[uint32, 4] [uint32, 4]
end end

Просмотреть файл

@ -3,4 +3,5 @@ module SSHData
DecodeError = Class.new(Error) DecodeError = Class.new(Error)
VerifyError = Class.new(Error) VerifyError = Class.new(Error)
AlgorithmError = Class.new(Error) AlgorithmError = Class.new(Error)
DecryptError = Class.new(Error)
end end

Просмотреть файл

@ -0,0 +1,34 @@
module SSHData
module PrivateKey
PEM_TYPE = "OPENSSH PRIVATE KEY"
# Parse an SSH public key.
#
# key - An SSH formatted public key, including algo, encoded key and optional
# user/host names.
#
# Returns a PublicKey::Base subclass instance.
def self.parse(key)
raw = Encoding.decode_pem(key, PEM_TYPE)
data, read = Encoding.decode_openssh_private_key(raw)
unless read == raw.bytesize
raise DecodeError, "unexpected trailing data"
end
from_data(data)
end
def self.from_data(data)
case data[:algo]
when PublicKey::ALGO_RSA
RSA.new(**data)
else
raise DecodeError, "unkown algo: #{data[:algo].inspect}"
end
end
end
end
require "ssh_data/private_key/base"
require "ssh_data/private_key/rsa"

Просмотреть файл

@ -0,0 +1,12 @@
module SSHData
module PrivateKey
class Base
attr_reader :algo, :comment
def initialize(**kwargs)
@algo = kwargs[:algo]
@comment = kwargs[:comment]
end
end
end
end

Просмотреть файл

@ -0,0 +1,63 @@
module SSHData
module PrivateKey
class RSA < Base
attr_reader :n, :e, :d, :iqmp, :p, :q
def initialize(algo:, n:, e:, d:, iqmp:, p:, q:, comment:)
unless algo == PublicKey::ALGO_RSA
raise DecodeError, "bad algorithm: #{algo.inspect}"
end
@n = n
@e = e
@d = d
@iqmp = iqmp
@p = p
@q = q
super(algo: algo, comment: comment)
end
def public_key
PublicKey::RSA.new(algo: algo, e: e, n: n)
end
def openssl
OpenSSL::PKey::RSA.new(asn1.to_der)
end
private
# CRT coefficient for faster RSA operations. Used by OpenSSL, but not
# OpenSSH.
#
# Returns an OpenSSL::BN instance.
def dmp1
d % (p - 1)
end
# CRT coefficient for faster RSA operations. Used by OpenSSL, but not
# OpenSSH.
#
# Returns an OpenSSL::BN instance.
def dmq1
d % (q - 1)
end
def asn1
OpenSSL::ASN1::Sequence.new([
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(0)),
OpenSSL::ASN1::Integer.new(n),
OpenSSL::ASN1::Integer.new(e),
OpenSSL::ASN1::Integer.new(d),
OpenSSL::ASN1::Integer.new(p),
OpenSSL::ASN1::Integer.new(q),
OpenSSL::ASN1::Integer.new(dmp1),
OpenSSL::ASN1::Integer.new(dmq1),
OpenSSL::ASN1::Integer.new(iqmp),
])
end
end
end
end

Просмотреть файл

@ -4,7 +4,7 @@ module SSHData
attr_reader :algo attr_reader :algo
def initialize(**kwargs) def initialize(**kwargs)
raise "implement me" @algo = kwargs[:algo]
end end
def verify(signed_data, signature) def verify(signed_data, signature)

Просмотреть файл

@ -54,13 +54,14 @@ module SSHData
raise DecodeError, "bad algorithm: #{algo.inspect}" raise DecodeError, "bad algorithm: #{algo.inspect}"
end end
@algo = algo
@p = p @p = p
@q = q @q = q
@g = g @g = g
@y = y @y = y
@openssl = OpenSSL::PKey::DSA.new(asn1.to_der) @openssl = OpenSSL::PKey::DSA.new(asn1.to_der)
super(algo: algo)
end end
# Verify an SSH signature. # Verify an SSH signature.

Просмотреть файл

@ -64,7 +64,6 @@ module SSHData
raise DecodeError, "bad curve: #{curve.inspect}" raise DecodeError, "bad curve: #{curve.inspect}"
end end
@algo = algo
@curve = curve @curve = curve
@public_key = public_key @public_key = public_key
@ -73,6 +72,8 @@ module SSHData
rescue ArgumentError rescue ArgumentError
raise DecodeError, "bad key data" raise DecodeError, "bad key data"
end end
super(algo: algo)
end end
# Verify an SSH signature. # Verify an SSH signature.

Просмотреть файл

@ -14,12 +14,13 @@ module SSHData
raise DecodeError, "bad algorithm: #{algo.inspect}" raise DecodeError, "bad algorithm: #{algo.inspect}"
end end
@algo = algo
@pk = pk @pk = pk
if self.class.enabled? if self.class.enabled?
@ed25519_key = Ed25519::VerifyKey.new(pk) @ed25519_key = Ed25519::VerifyKey.new(pk)
end end
super(algo: algo)
end end
# Verify an SSH signature. # Verify an SSH signature.

Просмотреть файл

@ -13,6 +13,8 @@ module SSHData
@n = n @n = n
@openssl = OpenSSL::PKey::RSA.new(asn1.to_der) @openssl = OpenSSL::PKey::RSA.new(asn1.to_der)
super(algo: algo)
end end
# Verify an SSH signature. # Verify an SSH signature.

2
spec/fixtures/gen.sh поставляемый
Просмотреть файл

@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
ssh-keygen -trsa -N "passw0rd" -f ./encrypted_rsa
ssh-keygen -trsa -N "" -f ./rsa_ca ssh-keygen -trsa -N "" -f ./rsa_ca
ssh-keygen -tdsa -N "" -f ./dsa_ca ssh-keygen -tdsa -N "" -f ./dsa_ca
ssh-keygen -tecdsa -N "" -f ./ecdsa_ca ssh-keygen -tecdsa -N "" -f ./ecdsa_ca

Просмотреть файл

@ -0,0 +1,55 @@
require_relative "../spec_helper"
describe SSHData::PrivateKey::RSA do
let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
let(:public_key) { private_key.public_key }
let(:params) { private_key.params }
let(:openssh_key) { SSHData::PublicKey.parse(fixture("rsa_leaf_for_rsa_ca.pub")) }
let(:comment) { "asdf" }
subject do
described_class.new(
algo: SSHData::PublicKey::ALGO_RSA,
n: params["n"],
e: params["e"],
d: params["d"],
iqmp: params["iqmp"],
p: params["p"],
q: params["q"],
comment: comment,
)
end
it "has an algo" do
expect(subject.algo).to eq(SSHData::PublicKey::ALGO_RSA)
end
it "has params" do
expect(subject.n).to eq(params["n"])
expect(subject.e).to eq(params["e"])
expect(subject.d).to eq(params["d"])
expect(subject.iqmp).to eq(params["iqmp"])
expect(subject.p).to eq(params["p"])
expect(subject.q).to eq(params["q"])
end
it "has a comment" do
expect(subject.comment).to eq(comment)
end
it "has an openssl representation" do
expect(subject.openssl).to be_a(OpenSSL::PKey::RSA)
expect(subject.openssl.to_der).to eq(private_key.to_der)
end
it "has a public key" do
expect(subject.public_key).to be_a(SSHData::PublicKey::RSA)
expect(subject.public_key.openssl.to_der).to eq(public_key.to_der)
end
it "can parse openssh-generate keys" do
expect { openssh_key }.not_to raise_error
end
end