ssh-data-1.3.0/0000755000175100017510000000000014222500447012245 5ustar pravipravissh-data-1.3.0/LICENSE.md0000644000175100017510000000205514222500447013653 0ustar pravipraviMIT License Copyright (c) 2019 GitHub, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ssh-data-1.3.0/lib/0000755000175100017510000000000014222500447013013 5ustar pravipravissh-data-1.3.0/lib/ssh_data.rb0000644000175100017510000000163414222500447015132 0ustar pravipravirequire "openssl" require "base64" module SSHData # Break down a key in OpenSSH authorized_keys format (see sshd(8) manual # page). # # 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 comment String or nil. def key_parts(key) algo, b64, comment = key.strip.split(" ", 3) if algo.nil? || b64.nil? raise DecodeError, "bad data format" end raw = begin Base64.strict_decode64(b64) rescue ArgumentError raise DecodeError, "bad data format" end [algo, raw, comment] end extend self end require "ssh_data/version" require "ssh_data/error" require "ssh_data/certificate" require "ssh_data/public_key" require "ssh_data/private_key" require "ssh_data/encoding" require "ssh_data/signature" ssh-data-1.3.0/lib/ssh_data/0000755000175100017510000000000014222500447014601 5ustar pravipravissh-data-1.3.0/lib/ssh_data/private_key/0000755000175100017510000000000014222500447017123 5ustar pravipravissh-data-1.3.0/lib/ssh_data/private_key/rsa.rb0000644000175100017510000000546014222500447020242 0ustar pravipravimodule SSHData module PrivateKey class RSA < Base attr_reader :n, :e, :d, :iqmp, :p, :q, :openssl # Generate a new private key. # # size - The Integer key size to generate. # unsafe_allow_small_key: - Bool of whether to allow keys of less than # 2048 bits. # # Returns a PublicKey::Base subclass instance. def self.generate(size, unsafe_allow_small_key: false) unless size >= 2048 || unsafe_allow_small_key raise AlgorithmError, "key too small" end from_openssl(OpenSSL::PKey::RSA.generate(size)) end # Import an openssl private key. # # key - An OpenSSL::PKey::DSA instance. # # Returns a DSA instance. def self.from_openssl(key) new( algo: PublicKey::ALGO_RSA, n: key.params["n"], e: key.params["e"], d: key.params["d"], iqmp: key.params["iqmp"], p: key.params["p"], q: key.params["q"], comment: "", ) end 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) @openssl = OpenSSL::PKey::RSA.new(asn1.to_der) @public_key = PublicKey::RSA.new(algo: algo, e: e, n: n) end # Make an SSH signature. # # signed_data - The String message over which to calculated the signature. # # Returns a binary String signature. def sign(signed_data, algo: nil) algo ||= self.algo digest = PublicKey::RSA::ALGO_DIGESTS[algo] raise AlgorithmError if digest.nil? raw_sig = openssl.sign(digest.new, signed_data) Encoding.encode_signature(algo, raw_sig) 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(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 ssh-data-1.3.0/lib/ssh_data/private_key/ed25519.rb0000644000175100017510000000356314222500447020455 0ustar pravipravimodule SSHData module PrivateKey class ED25519 < Base attr_reader :pk, :sk, :ed25519_key # Generate a new private key. # # Returns a PublicKey::Base subclass instance. def self.generate PublicKey::ED25519.ed25519_gem_required! from_ed25519(Ed25519::SigningKey.generate) end # Create from a ::Ed25519::SigningKey instance. # # key - A ::Ed25519::SigningKey instance. # # Returns a ED25519 instance. def self.from_ed25519(key) new( algo: PublicKey::ALGO_ED25519, pk: key.verify_key.to_bytes, sk: key.to_bytes + key.verify_key.to_bytes, comment: "", ) end def initialize(algo:, pk:, sk:, comment:) unless algo == PublicKey::ALGO_ED25519 raise DecodeError, "bad algorithm: #{algo.inspect}" end # openssh stores the pk twice, once as half of the sk... if sk.bytesize != 64 || sk.byteslice(32, 32) != pk raise DecodeError, "bad sk" end @pk = pk @sk = sk super(algo: algo, comment: comment) if PublicKey::ED25519.enabled? @ed25519_key = Ed25519::SigningKey.new(sk.byteslice(0, 32)) if @ed25519_key.verify_key.to_bytes != pk raise DecodeError, "bad pk" end end @public_key = PublicKey::ED25519.new(algo: algo, pk: pk) end # Make an SSH signature. # # signed_data - The String message over which to calculated the signature. # # Returns a binary String signature. def sign(signed_data, algo: nil) PublicKey::ED25519.ed25519_gem_required! algo ||= self.algo raise AlgorithmError unless algo == self.algo raw_sig = ed25519_key.sign(signed_data) Encoding.encode_signature(algo, raw_sig) end end end end ssh-data-1.3.0/lib/ssh_data/private_key/ecdsa.rb0000644000175100017510000000574014222500447020535 0ustar pravipravimodule SSHData module PrivateKey class ECDSA < Base attr_reader :curve, :public_key_bytes, :private_key_bytes, :openssl # Generate a new private key. # # curve - The String curve to use. One of SSHData::PublicKey::NISTP256, # SSHData::PublicKey::NISTP384, or SSHData::PublicKey::NISTP521. # # Returns a PublicKey::Base subclass instance. def self.generate(curve) openssl_curve = PublicKey::ECDSA::OPENSSL_CURVE_NAME_FOR_CURVE[curve] raise AlgorithmError, "unknown curve: #{curve}" if openssl_curve.nil? openssl_key = OpenSSL::PKey::EC.new(openssl_curve).tap(&:generate_key) from_openssl(openssl_key) end # Import an openssl private key. # # key - An OpenSSL::PKey::EC instance. # # Returns a DSA instance. def self.from_openssl(key) curve = PublicKey::ECDSA::CURVE_FOR_OPENSSL_CURVE_NAME[key.group.curve_name] algo = "ecdsa-sha2-#{curve}" new( algo: algo, curve: curve, public_key: key.public_key.to_bn.to_s(2), private_key: key.private_key, comment: "", ) end def initialize(algo:, curve:, public_key:, private_key:, comment:) unless [PublicKey::ALGO_ECDSA256, PublicKey::ALGO_ECDSA384, PublicKey::ALGO_ECDSA521].include?(algo) raise DecodeError, "bad algorithm: #{algo.inspect}" end unless algo == "ecdsa-sha2-#{curve}" raise DecodeError, "bad curve: #{curve.inspect}" end @curve = curve @public_key_bytes = public_key @private_key_bytes = private_key super(algo: algo, comment: comment) @openssl = begin OpenSSL::PKey::EC.new(asn1.to_der) rescue ArgumentError raise DecodeError, "bad key data" end @public_key = PublicKey::ECDSA.new( algo: algo, curve: curve, public_key: public_key_bytes ) end # Make an SSH signature. # # signed_data - The String message over which to calculated the signature. # # Returns a binary String signature. def sign(signed_data, algo: nil) algo ||= self.algo raise AlgorithmError unless algo == self.algo openssl_sig = openssl.sign(public_key.digest.new, signed_data) raw_sig = PublicKey::ECDSA.ssh_signature(openssl_sig) Encoding.encode_signature(algo, raw_sig) end private def asn1 unless name = PublicKey::ECDSA::OPENSSL_CURVE_NAME_FOR_CURVE[curve] raise DecodeError, "unknown curve: #{curve.inspect}" end OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Integer.new(1), OpenSSL::ASN1::OctetString.new(private_key_bytes.to_s(2)), OpenSSL::ASN1::ObjectId.new(name, 0, :EXPLICIT, :CONTEXT_SPECIFIC), OpenSSL::ASN1::BitString.new(public_key_bytes, 1, :EXPLICIT, :CONTEXT_SPECIFIC) ]) end end end end ssh-data-1.3.0/lib/ssh_data/private_key/base.rb0000644000175100017510000000213614222500447020364 0ustar pravipravimodule SSHData module PrivateKey class Base attr_reader :algo, :comment, :public_key def initialize(**kwargs) @algo = kwargs[:algo] @comment = kwargs[:comment] end # Generate a new private key. # # Returns a PublicKey::Base subclass instance. def self.generate(**kwargs) raise "implement me" end # Make an SSH signature. # # signed_data - The String message over which to calculated the signature. # algo: - Optionally specify the signature algorithm to use. # # Returns a binary String signature. def sign(signed_data, algo: nil) raise "implement me" end # Issue a certificate using this private key. # # signature_algo: - Optionally specify the signature algorithm to use. # kwargs - See SSHData::Certificate.new. # # Returns a SSHData::Certificate instance. def issue_certificate(signature_algo: nil, **kwargs) Certificate.new(**kwargs).tap { |c| c.sign(self, algo: signature_algo) } end end end end ssh-data-1.3.0/lib/ssh_data/private_key/dsa.rb0000644000175100017510000000445714222500447020231 0ustar pravipravi module SSHData module PrivateKey class DSA < Base attr_reader :p, :q, :g, :x, :y, :openssl # Generate a new private key. # # Returns a PublicKey::Base subclass instance. def self.generate openssl_key = if defined?(OpenSSL::PKey.generate_parameters) dsa_parameters = OpenSSL::PKey.generate_parameters("DSA", { dsa_paramgen_bits: 1024, dsa_paramgen_q_bits: 160 }) OpenSSL::PKey.generate_key(dsa_parameters) else OpenSSL::PKey::DSA.generate(1024) end from_openssl(openssl_key) end # Import an openssl private key. # # key - An OpenSSL::PKey::DSA instance. # # Returns a DSA instance. def self.from_openssl(key) new( algo: PublicKey::ALGO_DSA, p: key.params["p"], q: key.params["q"], g: key.params["g"], y: key.params["pub_key"], x: key.params["priv_key"], comment: "", ) end def initialize(algo:, p:, q:, g:, x:, y:, comment:) unless algo == PublicKey::ALGO_DSA raise DecodeError, "bad algorithm: #{algo.inspect}" end @p = p @q = q @g = g @x = x @y = y super(algo: algo, comment: comment) @openssl = OpenSSL::PKey::DSA.new(asn1.to_der) @public_key = PublicKey::DSA.new(algo: algo, p: p, q: q, g: g, y: y) end # Make an SSH signature. # # signed_data - The String message over which to calculated the signature. # # Returns a binary String signature. def sign(signed_data, algo: nil) algo ||= self.algo raise AlgorithmError unless algo == self.algo openssl_sig = openssl.sign(OpenSSL::Digest::SHA1.new, signed_data) raw_sig = PublicKey::DSA.ssh_signature(openssl_sig) Encoding.encode_signature(algo, raw_sig) end private def asn1 OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Integer.new(0), OpenSSL::ASN1::Integer.new(p), OpenSSL::ASN1::Integer.new(q), OpenSSL::ASN1::Integer.new(g), OpenSSL::ASN1::Integer.new(y), OpenSSL::ASN1::Integer.new(x), ]) end end end end ssh-data-1.3.0/lib/ssh_data/encoding.rb0000644000175100017510000005226714222500447016730 0ustar pravipravimodule SSHData 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_SIGNATURE_MAGIC = "SSHSIG" OPENSSH_SIGNATURE_VERSION = 0x01 OPENSSH_SIGNATURE_FIELDS = [ [:sigversion, :uint32], [:publickey, :string], [:namespace, :string], [:reserved, :string], [:hash_algorithm, :string], [:signature, :string], ] 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 a DSA private key DSA_PRIVATE_KEY_FIELDS = [ [:p, :mpint], [:q, :mpint], [:g, :mpint], [:y, :mpint], [:x, :mpint] ] # Fields in a ECDSA private key ECDSA_PRIVATE_KEY_FIELDS = [ [:curve, :string], [:public_key, :string], [:private_key, :mpint], ] # Fields in a ED25519 private key ED25519_PRIVATE_KEY_FIELDS = [ [:pk, :string], [:sk, :string] ] # Fields in an RSA public key RSA_KEY_FIELDS = [ [:e, :mpint], [:n, :mpint] ] # Fields in a DSA public key DSA_KEY_FIELDS = [ [:p, :mpint], [:q, :mpint], [:g, :mpint], [:y, :mpint] ] # Fields in an ECDSA public key ECDSA_KEY_FIELDS = [ [:curve, :string], [:public_key, :string] ] # Fields in an SK-ECDSA public key SKECDSA_KEY_FIELDS = [ [:curve, :string], [:public_key, :string], [:application, :string] ] # Fields in a ED25519 public key ED25519_KEY_FIELDS = [ [:pk, :string] ] # Fields in a SK-ED25519 public key SKED25519_KEY_FIELDS = [ [:pk, :string], [:application, :string] ] PUBLIC_KEY_ALGO_BY_CERT_ALGO = { Certificate::ALGO_RSA => PublicKey::ALGO_RSA, Certificate::ALGO_DSA => PublicKey::ALGO_DSA, Certificate::ALGO_ECDSA256 => PublicKey::ALGO_ECDSA256, Certificate::ALGO_ECDSA384 => PublicKey::ALGO_ECDSA384, Certificate::ALGO_ECDSA521 => PublicKey::ALGO_ECDSA521, Certificate::ALGO_ED25519 => PublicKey::ALGO_ED25519, Certificate::ALGO_SKECDSA256 => PublicKey::ALGO_SKECDSA256, Certificate::ALGO_SKED25519 => PublicKey::ALGO_SKED25519, } CERT_ALGO_BY_PUBLIC_KEY_ALGO = { PublicKey::ALGO_RSA => Certificate::ALGO_RSA, PublicKey::ALGO_DSA => Certificate::ALGO_DSA, PublicKey::ALGO_ECDSA256 => Certificate::ALGO_ECDSA256, PublicKey::ALGO_ECDSA384 => Certificate::ALGO_ECDSA384, PublicKey::ALGO_ECDSA521 => Certificate::ALGO_ECDSA521, PublicKey::ALGO_ED25519 => Certificate::ALGO_ED25519, PublicKey::ALGO_SKECDSA256 => Certificate::ALGO_SKECDSA256, PublicKey::ALGO_SKED25519 => Certificate::ALGO_SKED25519, } KEY_FIELDS_BY_PUBLIC_KEY_ALGO = { PublicKey::ALGO_RSA => RSA_KEY_FIELDS, PublicKey::ALGO_DSA => DSA_KEY_FIELDS, PublicKey::ALGO_ECDSA256 => ECDSA_KEY_FIELDS, PublicKey::ALGO_ECDSA384 => ECDSA_KEY_FIELDS, PublicKey::ALGO_ECDSA521 => ECDSA_KEY_FIELDS, PublicKey::ALGO_ED25519 => ED25519_KEY_FIELDS, PublicKey::ALGO_SKED25519 => SKED25519_KEY_FIELDS, PublicKey::ALGO_SKECDSA256 => SKECDSA_KEY_FIELDS, } KEY_FIELDS_BY_PRIVATE_KEY_ALGO = { PublicKey::ALGO_RSA => RSA_PRIVATE_KEY_FIELDS, PublicKey::ALGO_DSA => DSA_PRIVATE_KEY_FIELDS, PublicKey::ALGO_ECDSA256 => ECDSA_PRIVATE_KEY_FIELDS, PublicKey::ALGO_ECDSA384 => ECDSA_PRIVATE_KEY_FIELDS, PublicKey::ALGO_ECDSA521 => ECDSA_PRIVATE_KEY_FIELDS, PublicKey::ALGO_ED25519 => ED25519_PRIVATE_KEY_FIELDS, } # Get the type from a PEM encoded blob. # # pem - A PEM encoded String. # # Returns a String PEM type. def pem_type(pem) head = pem.split("\n", 2).first.strip head_prefix = "-----BEGIN " head_suffix = "-----" unless head.start_with?(head_prefix) && head.end_with?(head_suffix) raise DecodeError, "bad PEM encoding" end type_size = head.bytesize - head_prefix.bytesize - head_suffix.bytesize head.byteslice(head_prefix.bytesize, type_size) end # 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").map(&:strip) unless lines.shift == "-----BEGIN #{type}-----" raise DecodeError, "bad PEM header" end unless lines.pop == "-----END #{type}-----" raise DecodeError, "bad PEM footer" end begin Base64.strict_decode64(lines.join) rescue ArgumentError raise DecodeError, "bad PEM data" end 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(total_read, OPENSSH_PRIVATE_KEY_MAGIC.bytesize) unless magic == OPENSSH_PRIVATE_KEY_MAGIC raise DecodeError, "bad OpenSSH private key" end total_read += OPENSSH_PRIVATE_KEY_MAGIC.bytesize 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, total_read, data[:nkeys]) total_read += read privs, read = decode_string(raw, total_read) total_read += read privs_read = 0 data[:checkint1], read = decode_uint32(privs, privs_read) privs_read += read data[:checkint2], read = decode_uint32(privs, privs_read) privs_read += read unless data[:checkint1] == data[: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... data[:padding] = privs.byteslice(privs_read..-1) unless data[:padding].bytes.each_with_index.all? { |b, i| b == (i + 1) % 255 } raise DecodeError, "bad padding: #{data[:padding].inspect}" end [data, total_read] end # Decode the signature. # # raw - The binary String signature as described by RFC4253 section 6.6. # offset - Integer number of bytes into `raw` at which we should start # reading. # # Returns an Array containing the decoded algorithm String, the decoded binary # signature String, and the Integer number of bytes read. def decode_signature(raw, offset=0) total_read = 0 algo, read = decode_string(raw, offset + total_read) total_read += read sig, read = decode_string(raw, offset + total_read) total_read += read [algo, sig, total_read] end # Encoding a signature. # # algo - The String signature algorithm. # signature - The String signature blob. # # Returns an encoded String. def encode_signature(algo, signature) encode_string(algo) + encode_string(signature) end # Decode the fields in a public key. # # raw - Binary String public key as described by RFC4253 section 6.6. # algo - String public key algorithm identifier (optional). # offset - Integer number of bytes into `raw` at which we should start # reading. # # Returns an Array containing a Hash describing the public key and the # Integer number of bytes read. def decode_public_key(raw, offset=0, algo=nil) total_read = 0 if algo.nil? algo, read = decode_string(raw, offset + total_read) total_read += read end unless fields = KEY_FIELDS_BY_PUBLIC_KEY_ALGO[algo] raise AlgorithmError, "unknown algorithm: #{algo.inspect}" end data, read = decode_fields(raw, fields, offset + total_read) total_read += read data[:algo] = algo [data, total_read] end # Decode the fields in a public key encoded as an SSH string. # # raw - Binary public key as described by RFC4253 section 6.6 wrapped in # an SSH string.. # algo - String public key algorithm identifier (optional). # offset - Integer number of bytes into `raw` at which we should start # reading. # # Returns an Array containing a Hash describing the public key and the # Integer number of bytes read. def decode_string_public_key(raw, offset=0, algo=nil) key_raw, str_read = decode_string(raw, offset) key, cert_read = decode_public_key(key_raw, 0, algo) if cert_read != key_raw.bytesize raise DecodeError, "unexpected trailing data" end [key, str_read] end def decode_openssh_signature(raw, offset=0) total_read = 0 magic = raw.byteslice(offset, OPENSSH_SIGNATURE_MAGIC.bytesize) unless magic == OPENSSH_SIGNATURE_MAGIC raise DecodeError, "bad OpenSSH signature" end total_read += OPENSSH_SIGNATURE_MAGIC.bytesize offset += total_read data, read = decode_fields(raw, OPENSSH_SIGNATURE_FIELDS, offset) total_read += read [data, total_read] end # Decode the fields in a certificate. # # raw - Binary String certificate as described by RFC4253 section 6.6. # offset - Integer number of bytes into `raw` at which we should start # reading. # # Returns an Array containing a Hash describing the certificate and the # Integer number of bytes read. def decode_certificate(raw, offset=0) total_read = 0 algo, read = decode_string(raw, offset + total_read) total_read += read unless key_algo = PUBLIC_KEY_ALGO_BY_CERT_ALGO[algo] raise AlgorithmError, "unknown algorithm: #{algo.inspect}" end data, read = decode_fields(raw, [ [:nonce, :string], [:public_key, :public_key, key_algo], [:serial, :uint64], [:type, :uint32], [:key_id, :string], [:valid_principals, :list], [:valid_after, :time], [:valid_before, :time], [:critical_options, :options], [:extensions, :options], [:reserved, :string], [:signature_key, :string_public_key], [:signature, :string], ], offset + total_read) total_read += read data[:algo] = algo [data, total_read] end # Decode all of the given fields from raw. # # raw - A binary String. # fields - An Array of Arrays, each containing a symbol describing the field # and a Symbol describing the type of the field (:mpint, :string, # :uint64, or :uint32). # offset - The offset into raw at which to read (default 0). # # Returns an Array containing a Hash mapping the provided field keys to the # decoded values and the Integer number of bytes read. def decode_fields(raw, fields, offset=0) hash = {} total_read = 0 fields.each do |key, type, *args| hash[key], read = case type when :string decode_string(raw, offset + total_read, *args) when :list decode_list(raw, offset + total_read, *args) when :mpint decode_mpint(raw, offset + total_read, *args) when :time decode_time(raw, offset + total_read, *args) when :uint64 decode_uint64(raw, offset + total_read, *args) when :uint32 decode_uint32(raw, offset + total_read, *args) when :public_key decode_public_key(raw, offset + total_read, *args) when :string_public_key decode_string_public_key(raw, offset + total_read, *args) when :options decode_options(raw, offset + total_read, *args) else raise DecodeError end total_read += read end [hash, total_read] end # Encode the series of fiends into a binary string. # # fields - A series of Arrays, each containing a Symbol type and a value to # encode. # # Returns a binary String. def encode_fields(*fields) fields.map do |type, value| case type when :raw value when :string encode_string(value) when :list encode_list(value) when :mpint encode_mpint(value) when :time encode_time(value) when :uint64 encode_uint64(value) when :uint32 encode_uint32(value) when :options encode_options(value) else raise DecodeError, "bad type: #{type}" end end.join end # Read a string out of the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the decoded String and the Integer number of # bytes read. def decode_string(raw, offset=0) if raw.bytesize < offset + 4 raise DecodeError, "data too short" end size_s = raw.byteslice(offset, 4) size = size_s.unpack("L>").first if raw.bytesize < offset + 4 + size raise DecodeError, "data too short" end string = raw.byteslice(offset + 4, size) [string, 4 + size] end # Encoding a string. # # value - The String value to encode. # # Returns an encoded representation of the String. def encode_string(value) [value.bytesize, value].pack("L>A*") end # Read a series of strings out of the provided raw data. # # 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 # number of bytes read. def decode_list(raw, offset=0) list_raw, str_read = decode_string(raw, offset) list_read = 0 list = [] while list_raw.bytesize > list_read value, read = decode_string(list_raw, list_read) list << value list_read += read end if list_read != list_raw.bytesize raise DecodeError, "bad strings list" end [list, str_read] end # Encode a list of strings. # # value - The Array of Strings to encode. # # Returns an encoded representation of the list. def encode_list(value) encode_string(value.map { |s| encode_string(s) }.join) end # Read a multi-precision integer from the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the decoded mpint as an OpenSSL::BN and the # Integer number of bytes read. def decode_mpint(raw, offset=0) if raw.bytesize < offset + 4 raise DecodeError, "data too short" end str_size_s = raw.byteslice(offset, 4) str_size = str_size_s.unpack("L>").first mpi_size = str_size + 4 if raw.bytesize < offset + mpi_size raise DecodeError, "data too short" end mpi_s = raw.slice(offset, mpi_size) # 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 # exception that OpenSSL doesn't enforce minimal length. We could enforce # this ourselves, but it doesn't seem worth the added complexity. mpi = OpenSSL::BN.new(mpi_s, 0) [mpi, mpi_size] end # Encode a BN as an mpint. # # value - The OpenSSL::BN value to encode. # # Returns an encoded representation of the BN. def encode_mpint(value) value.to_s(0) end # Read a time from the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the decoded Time and the Integer number of # bytes read. def decode_time(raw, offset=0) time_raw, read = decode_uint64(raw, offset) [Time.at(time_raw), read] end # Encode a time. # # value - The Time value to encode. # # Returns an encoded representation of the Time. def encode_time(value) encode_uint64(value.to_i) end # Read the specified number of strings out of the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # n - The Integer number of Strings to read. # # Returns an Array including the Array of decoded Strings and the Integer # number of bytes read. def decode_n_strings(raw, offset=0, n) 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. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the Hash of decoded keys/values and the Integer # number of bytes read. def decode_options(raw, offset=0) opts_raw, str_read = decode_string(raw, offset) opts_read = 0 opts = {} while opts_raw.bytesize > opts_read key, read = decode_string(opts_raw, opts_read) opts_read += read value_raw, read = decode_string(opts_raw, opts_read) opts_read += read if value_raw.bytesize > 0 opts[key], read = decode_string(value_raw) if read != value_raw.bytesize raise DecodeError, "bad options data" end else opts[key] = true end end if opts_read != opts_raw.bytesize raise DecodeError, "bad options" end [opts, str_read] end # Encode series of key/value pairs. # # value - The Hash value to encode. # # Returns an encoded representation of the Hash. def encode_options(value) opts_raw = value.reduce("") do |encoded, (key, value)| value_str = value == true ? "" : encode_string(value) encoded + encode_string(key) + encode_string(value_str) end encode_string(opts_raw) end # Read a uint64 from the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the decoded uint64 as an Integer and the # Integer number of bytes read. def decode_uint64(raw, offset=0) if raw.bytesize < offset + 8 raise DecodeError, "data too short" end uint64 = raw.byteslice(offset, 8).unpack("Q>").first [uint64, 8] end # Encoding an integer as a uint64. # # value - The Integer value to encode. # # Returns an encoded representation of the value. def encode_uint64(value) [value].pack("Q>") end # Read a uint32 from the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the decoded uint32 as an Integer and the # Integer number of bytes read. def decode_uint32(raw, offset=0) if raw.bytesize < offset + 4 raise DecodeError, "data too short" end uint32 = raw.byteslice(offset, 4).unpack("L>").first [uint32, 4] end # Encoding an integer as a uint32. # # value - The Integer value to encode. # # Returns an encoded representation of the value. def encode_uint32(value) [value].pack("L>") end # Read a uint8 from the provided raw data. # # raw - A binary String. # offset - The offset into raw at which to read (default 0). # # Returns an Array including the decoded uint8 as an Integer and the # Integer number of bytes read. def decode_uint8(raw, offset=0) if raw.bytesize < offset + 1 raise DecodeError, "data too short" end uint8 = raw.byteslice(offset, 1).unpack("C").first [uint8, 1] end # Encoding an integer as a uint8. # # value - The Integer value to encode. # # Returns an encoded representation of the value. def encode_uint8(value) [value].pack("C") end extend self end end ssh-data-1.3.0/lib/ssh_data/error.rb0000644000175100017510000000037714222500447016266 0ustar pravipravimodule SSHData Error = Class.new(StandardError) DecodeError = Class.new(Error) VerifyError = Class.new(Error) AlgorithmError = Class.new(Error) DecryptError = Class.new(Error) UnsupportedError = Class.new(Error) end ssh-data-1.3.0/lib/ssh_data/version.rb0000644000175100017510000000004714222500447016614 0ustar pravipravimodule SSHData VERSION = "1.3.0" end ssh-data-1.3.0/lib/ssh_data/private_key.rb0000644000175100017510000000431514222500447017453 0ustar pravipravimodule SSHData module PrivateKey OPENSSH_PEM_TYPE = "OPENSSH PRIVATE KEY" RSA_PEM_TYPE = "RSA PRIVATE KEY" DSA_PEM_TYPE = "DSA PRIVATE KEY" ECDSA_PEM_TYPE = "EC PRIVATE KEY" ENCRYPTED_PEM_TYPE = "ENCRYPTED PRIVATE KEY" # Parse an SSH private key. # # key - A PEM or OpenSSH encoded private key. # # Returns an Array of PrivateKey::Base subclass instances. def self.parse(key) pem_type = Encoding.pem_type(key) case pem_type when OPENSSH_PEM_TYPE parse_openssh(key) when RSA_PEM_TYPE [RSA.from_openssl(OpenSSL::PKey::RSA.new(key, ""))] when DSA_PEM_TYPE [DSA.from_openssl(OpenSSL::PKey::DSA.new(key, ""))] when ECDSA_PEM_TYPE [ECDSA.from_openssl(OpenSSL::PKey::EC.new(key, ""))] when ENCRYPTED_PEM_TYPE raise DecryptError, "cannot decode encrypted private keys" else raise AlgorithmError, "unknown PEM type: #{pem_type.inspect}" end rescue OpenSSL::PKey::PKeyError => e 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) 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) data[:private_keys].map do |priv| case priv[:algo] when PublicKey::ALGO_RSA RSA.new(**priv) when PublicKey::ALGO_DSA DSA.new(**priv) when PublicKey::ALGO_ECDSA256, PublicKey::ALGO_ECDSA384, PublicKey::ALGO_ECDSA521 ECDSA.new(**priv) when PublicKey::ALGO_ED25519 ED25519.new(**priv) else raise DecodeError, "unkown algo: #{priv[:algo].inspect}" end end end end end require "ssh_data/private_key/base" require "ssh_data/private_key/rsa" require "ssh_data/private_key/dsa" require "ssh_data/private_key/ecdsa" require "ssh_data/private_key/ed25519" ssh-data-1.3.0/lib/ssh_data/signature.rb0000644000175100017510000001025014222500447017125 0ustar pravipravi# frozen_string_literal: true module SSHData class Signature PEM_TYPE = "SSH SIGNATURE" SIGNATURE_PREAMBLE = "SSHSIG" MIN_SUPPORTED_VERSION = 1 MAX_SUPPORTED_VERSION = 1 # Spec: no SHA1 or SHA384. In practice, OpenSSH is always going to use SHA512. # Note the actual signing / verify primitive may use a different hash algorithm. # https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig#L67 SUPPORTED_HASH_ALGORITHMS = { "sha256" => OpenSSL::Digest::SHA256, "sha512" => OpenSSL::Digest::SHA512, } PERMITTED_RSA_SIGNATURE_ALGORITHMS = [ PublicKey::ALGO_RSA_SHA2_256, PublicKey::ALGO_RSA_SHA2_512, ] attr_reader :sigversion, :namespace, :signature, :reserved, :hash_algorithm # Parses a PEM armored SSH signature. # pem - A PEM encoded SSH signature. # # Returns a Signature instance. def self.parse_pem(pem) pem_type = Encoding.pem_type(pem) if pem_type != PEM_TYPE raise DecodeError, "Mismatched PEM type. Expecting '#{PEM_TYPE}', actually '#{pem_type}'." end blob = Encoding.decode_pem(pem, pem_type) self.parse_blob(blob) end def self.parse_blob(blob) data, read = Encoding.decode_openssh_signature(blob) if read != blob.bytesize raise DecodeError, "unexpected trailing data" end new(**data) end def initialize(sigversion:, publickey:, namespace:, reserved:, hash_algorithm:, signature:) if sigversion > MAX_SUPPORTED_VERSION || sigversion < MIN_SUPPORTED_VERSION raise UnsupportedError, "Signature version is not supported" end unless SUPPORTED_HASH_ALGORITHMS.has_key?(hash_algorithm) raise UnsupportedError, "Hash algorithm #{hash_algorithm} is not supported." end # Spec: empty namespaces are not permitted. # https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig#L57 raise UnsupportedError, "A namespace is required." if namespace.empty? # Spec: ignore 'reserved', don't need to validate that it is empty. @sigversion = sigversion @publickey = publickey @namespace = namespace @reserved = reserved @hash_algorithm = hash_algorithm @signature = signature end def verify(signed_data, **opts) signing_key = public_key # Unwrap the signing key if this signature was created from a certificate. key = signing_key.is_a?(Certificate) ? signing_key.public_key : signing_key digest_algorithm = SUPPORTED_HASH_ALGORITHMS[@hash_algorithm] if key.is_a?(PublicKey::RSA) sig_algo, * = Encoding.decode_signature(@signature) # Spec: If the signature is an RSA signature, the legacy 'ssh-rsa' # identifer is not permitted. # https://github.com/openssh/openssh-portable/blob/b7ffbb17e37f59249c31f1ff59d6c5d80888f689/PROTOCOL.sshsig#L72 unless PERMITTED_RSA_SIGNATURE_ALGORITHMS.include?(sig_algo) raise UnsupportedError, "RSA signature #{sig_algo} is not supported." end end message_digest = digest_algorithm.digest(signed_data) blob = SIGNATURE_PREAMBLE + Encoding.encode_string(@namespace) + Encoding.encode_string(@reserved || "") + Encoding.encode_string(@hash_algorithm) + Encoding.encode_string(message_digest) if key.class.include?(::SSHData::PublicKey::SecurityKey) key.verify(blob, @signature, **opts) else key.verify(blob, @signature) end end # Gets the public key from the signature. # If the signature was created from a certificate, this will be an # SSHData::Certificate. Otherwise, this will be a PublicKey algorithm. def public_key public_key_algorithm, _ = Encoding.decode_string(@publickey) if PublicKey::ALGOS.include?(public_key_algorithm) PublicKey.parse_rfc4253(@publickey) elsif Certificate::ALGOS.include?(public_key_algorithm) Certificate.parse_rfc4253(@publickey) else raise UnsupportedError, "Public key algorithm #{public_key_algorithm} is not supported." end end end end ssh-data-1.3.0/lib/ssh_data/certificate.rb0000644000175100017510000002024214222500447017410 0ustar pravipravirequire "securerandom" require "ipaddr" module SSHData class Certificate # Special values for valid_before and valid_after. BEGINNING_OF_TIME = Time.at(0) END_OF_TIME = Time.at((2**64)-1) # Integer certificate types TYPE_USER = 1 TYPE_HOST = 2 # Certificate algorithm identifiers ALGO_RSA = "ssh-rsa-cert-v01@openssh.com" ALGO_DSA = "ssh-dss-cert-v01@openssh.com" ALGO_ECDSA256 = "ecdsa-sha2-nistp256-cert-v01@openssh.com" ALGO_ECDSA384 = "ecdsa-sha2-nistp384-cert-v01@openssh.com" ALGO_ECDSA521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com" ALGO_ED25519 = "ssh-ed25519-cert-v01@openssh.com" ALGO_SKECDSA256 = "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com" ALGO_SKED25519 = "sk-ssh-ed25519-cert-v01@openssh.com" ALGOS = [ ALGO_RSA, ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521, ALGO_ED25519, ALGO_SKECDSA256, ALGO_SKED25519 ] CRITICAL_OPTION_FORCE_COMMAND = "force-command" CRITICAL_OPTION_SOURCE_ADDRESS = "source-address" 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 OpenSSH certificate in authorized_keys format (see sshd(8) manual # page). # # 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_openssh(cert, unsafe_no_verify: false) algo, raw, _ = SSHData.key_parts(cert) parsed = parse_rfc4253(raw, unsafe_no_verify: unsafe_no_verify) if parsed.algo != algo raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}" end parsed end # Deprecated singleton_class.send(:alias_method, :parse, :parse_openssh) # Parse an RFC 4253 binary SSH certificate. # # 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_rfc4253(raw, unsafe_no_verify: false) data, read = Encoding.decode_certificate(raw) if read != raw.bytesize raise DecodeError, "unexpected trailing data" end # Parse data into better types, where possible. public_key = PublicKey.from_data(data.delete(:public_key)) ca_key = PublicKey.from_data(data.delete(:signature_key)) new(**data.merge(public_key: public_key, ca_key: ca_key)).tap do |cert| raise VerifyError unless unsafe_no_verify || cert.verify end end # Intialize a new Certificate instance. # # algo: - The certificate's String algorithm id (one of ALGO_RSA, # ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521, # or ALGO_ED25519) # nonce: - The certificate's String nonce field. # public_key: - The certificate's public key as an PublicKey::Base # subclass instance. # serial: - The certificate's Integer serial field. # 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 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 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. # signature: - The certificate's String signature field. # # Returns nothing. def initialize(public_key:, key_id:, algo: nil, nonce: nil, serial: 0, type: TYPE_USER, valid_principals: [], valid_after: BEGINNING_OF_TIME, valid_before: END_OF_TIME, critical_options: {}, extensions: {}, reserved: "", ca_key: nil, signature: "") @algo = algo || Encoding::CERT_ALGO_BY_PUBLIC_KEY_ALGO[public_key.algo] @nonce = nonce || SecureRandom.random_bytes(32) @public_key = public_key @serial = serial @type = type @key_id = key_id @valid_principals = valid_principals @valid_after = valid_after @valid_before = valid_before @critical_options = critical_options @extensions = extensions @reserved = reserved @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 # Sign this certificate with a private key. # # private_key - An SSHData::PrivateKey::Base subclass instance. # algo: - Optionally specify the signature algorithm to use. # # Returns nothing. def sign(private_key, algo: nil) @ca_key = private_key.public_key @signature = private_key.sign(signed_data, algo: algo) end # Verify the certificate's signature. # # Returns boolean. def verify ca_key.verify(signed_data, signature) end # The force-command critical option, if present. # # Returns a String or nil. def force_command case value = critical_options[CRITICAL_OPTION_FORCE_COMMAND] when String, NilClass value else raise DecodeError, "bad force-request" end end # The source-address critical option, if present. # # Returns an Array of IPAddr instances or nil. def source_address return @source_address if defined?(@source_address) value = critical_options[CRITICAL_OPTION_SOURCE_ADDRESS] @source_address = case value when String value.split(",").map do |str_addr| begin IPAddr.new(str_addr.strip) rescue IPAddr::InvalidAddressError => e raise DecodeError, "bad source-address: #{e.message}" end end when NilClass nil else raise DecodeError, "bad source-address" end end # Check if the given IP address is allowed for use with this certificate. # # address - A String IP address. # # Returns boolean. def allowed_source_address?(address) return true if source_address.nil? parsed_addr = IPAddr.new(address) source_address.any? { |a| a.include?(parsed_addr) } rescue IPAddr::InvalidAddressError return false end private # The portion of the certificate over which the signature is calculated. # # Returns a binary String. def signed_data siglen = self.signature.bytesize + 4 rfc4253.byteslice(0...-siglen) end # 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 ssh-data-1.3.0/lib/ssh_data/public_key/0000755000175100017510000000000014222500447016727 5ustar pravipravissh-data-1.3.0/lib/ssh_data/public_key/security_key.rb0000644000175100017510000000230414222500447021772 0ustar pravipravimodule SSHData module PublicKey module SecurityKey # Defaults to match OpenSSH, user presence is required by verification is not. DEFAULT_SK_VERIFY_OPTS = { user_presence_required: true, user_verification_required: false } SK_FLAG_USER_PRESENCE = 0b001 SK_FLAG_USER_VERIFICATION = 0b100 def build_signing_blob(application, signed_data, signature) read = 0 sig_algo, raw_sig, signature_read = Encoding.decode_signature(signature) read += signature_read sk_flags, sk_flags_read = Encoding.decode_uint8(signature, read) read += sk_flags_read counter, counter_read = Encoding.decode_uint32(signature, read) read += counter_read if read != signature.bytesize raise DecodeError, "unexpected trailing data" end application_hash = OpenSSL::Digest::SHA256.digest(application) message_hash = OpenSSL::Digest::SHA256.digest(signed_data) blob = application_hash + Encoding.encode_uint8(sk_flags) + Encoding.encode_uint32(counter) + message_hash [sig_algo, raw_sig, sk_flags, blob] end end end end ssh-data-1.3.0/lib/ssh_data/public_key/rsa.rb0000644000175100017510000000400114222500447020034 0ustar pravipravimodule SSHData module PublicKey class RSA < Base attr_reader :e, :n, :openssl ALGO_DIGESTS = { ALGO_RSA => OpenSSL::Digest::SHA1, ALGO_RSA_SHA2_256 => OpenSSL::Digest::SHA256, ALGO_RSA_SHA2_512 => OpenSSL::Digest::SHA512 } def initialize(algo:, e:, n:) unless algo == ALGO_RSA raise DecodeError, "bad algorithm: #{algo.inspect}" end @algo = algo @e = e @n = n @openssl = OpenSSL::PKey::RSA.new(asn1.to_der) super(algo: algo) end # Verify an SSH signature. # # signed_data - The String message that the signature was calculated over. # signature - The binarty String signature with SSH encoding. # # Returns boolean. def verify(signed_data, signature) sig_algo, raw_sig, _ = Encoding.decode_signature(signature) digest = ALGO_DIGESTS[sig_algo] if digest.nil? raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}" end openssl.verify(digest.new, raw_sig, signed_data) end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 Encoding.encode_fields( [:string, algo], [:mpint, e], [:mpint, n] ) end # Is this public key equal to another public key? # # other - Another SSHData::PublicKey::Base instance to compare with. # # Returns boolean. def ==(other) super && other.e == e && other.n == n end private def asn1 OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::ObjectId.new("rsaEncryption"), OpenSSL::ASN1::Null.new(nil), ]), OpenSSL::ASN1::BitString.new(OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Integer.new(n), OpenSSL::ASN1::Integer.new(e), ]).to_der), ]) end end end end ssh-data-1.3.0/lib/ssh_data/public_key/sked25519.rb0000644000175100017510000000343214222500447020612 0ustar pravipravimodule SSHData module PublicKey class SKED25519 < ED25519 include SecurityKey attr_reader :application def initialize(algo:, pk:, application:) @application = application super(algo: algo, pk: pk) end def self.algorithm_identifier ALGO_SKED25519 end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 Encoding.encode_fields( [:string, algo], [:string, pk], [:string, application], ) end def verify(signed_data, signature, **opts) self.class.ed25519_gem_required! opts = DEFAULT_SK_VERIFY_OPTS.merge(opts) unknown_opts = opts.keys - DEFAULT_SK_VERIFY_OPTS.keys raise UnsupportedError, "Verification options #{unknown_opts.inspect} are not supported." unless unknown_opts.empty? sig_algo, raw_sig, sk_flags, blob = build_signing_blob(application, signed_data, signature) if sig_algo != self.class.algorithm_identifier raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}" end result = begin ed25519_key.verify(raw_sig, blob) rescue Ed25519::VerifyError false end # We don't know that the flags are correct until after we've validated the signature # which embeds the flags, so always verify the signature first. return false if opts[:user_presence_required] && (sk_flags & SK_FLAG_USER_PRESENCE != SK_FLAG_USER_PRESENCE) return false if opts[:user_verification_required] && (sk_flags & SK_FLAG_USER_VERIFICATION != SK_FLAG_USER_VERIFICATION) result end def ==(other) super && other.application == application end end end end ssh-data-1.3.0/lib/ssh_data/public_key/ed25519.rb0000644000175100017510000000401614222500447020253 0ustar pravipravimodule SSHData module PublicKey class ED25519 < Base attr_reader :pk, :ed25519_key # ed25519 isn't a hard requirement for using this Gem. We only do actual # validation with the key if the ed25519 Gem has been loaded. def self.enabled? Object.const_defined?(:Ed25519) end # Assert that the ed25519 gem has been loaded. # # Returns nothing, raises AlgorithmError. def self.ed25519_gem_required! raise AlgorithmError, "the ed25519 gem is not loaded" unless enabled? end def self.algorithm_identifier ALGO_ED25519 end def initialize(algo:, pk:) unless algo == self.class.algorithm_identifier raise DecodeError, "bad algorithm: #{algo.inspect}" end @pk = pk if self.class.enabled? @ed25519_key = Ed25519::VerifyKey.new(pk) end super(algo: algo) end # Verify an SSH signature. # # signed_data - The String message that the signature was calculated over. # signature - The binarty String signature with SSH encoding. # # Returns boolean. def verify(signed_data, signature) self.class.ed25519_gem_required! sig_algo, raw_sig, _ = Encoding.decode_signature(signature) if sig_algo != self.class.algorithm_identifier raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}" end begin ed25519_key.verify(raw_sig, signed_data) rescue Ed25519::VerifyError false end end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 Encoding.encode_fields( [:string, algo], [:string, pk], ) end # Is this public key equal to another public key? # # other - Another SSHData::PublicKey::Base instance to compare with. # # Returns boolean. def ==(other) super && other.pk == pk end end end end ssh-data-1.3.0/lib/ssh_data/public_key/ecdsa.rb0000644000175100017510000001075214222500447020340 0ustar pravipravimodule SSHData module PublicKey class ECDSA < Base attr_reader :curve, :public_key_bytes, :openssl NISTP256 = "nistp256" NISTP384 = "nistp384" NISTP521 = "nistp521" OPENSSL_CURVE_NAME_FOR_CURVE = { NISTP256 => "prime256v1", NISTP384 => "secp384r1", NISTP521 => "secp521r1", } CURVE_FOR_OPENSSL_CURVE_NAME = { "prime256v1" => NISTP256, "secp384r1" => NISTP384, "secp521r1" => NISTP521, } DIGEST_FOR_CURVE = { NISTP256 => OpenSSL::Digest::SHA256, NISTP384 => OpenSSL::Digest::SHA384, NISTP521 => OpenSSL::Digest::SHA512, } # Convert an SSH encoded ECDSA signature to DER encoding for verification with # OpenSSL. # # sig - A binary String signature from an SSH packet. # # Returns a binary String signature, as expected by OpenSSL. def self.openssl_signature(sig) r, rlen = Encoding.decode_mpint(sig, 0) s, slen = Encoding.decode_mpint(sig, rlen) if rlen + slen != sig.bytesize raise DecodeError, "unexpected trailing data" end OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s) ]).to_der end # Convert an DER encoded ECDSA signature, as generated by OpenSSL to SSH # encoding. # # sig - A binary String signature, as generated by OpenSSL. # # Returns a binary String signature, as found in an SSH packet. def self.ssh_signature(sig) a1 = OpenSSL::ASN1.decode(sig) if a1.tag_class != :UNIVERSAL || a1.tag != OpenSSL::ASN1::SEQUENCE || a1.value.count != 2 raise DecodeError, "bad asn1 signature" end r, s = a1.value if r.tag_class != :UNIVERSAL || r.tag != OpenSSL::ASN1::INTEGER || s.tag_class != :UNIVERSAL || s.tag != OpenSSL::ASN1::INTEGER raise DecodeError, "bad asn1 signature" end [Encoding.encode_mpint(r.value), Encoding.encode_mpint(s.value)].join end def self.check_algorithm!(algo, curve) unless [ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521].include?(algo) raise DecodeError, "bad algorithm: #{algo.inspect}" end unless algo == "ecdsa-sha2-#{curve}" raise DecodeError, "bad curve: #{curve.inspect}" end end def initialize(algo:, curve:, public_key:) self.class.check_algorithm!(algo, curve) @curve = curve @public_key_bytes = public_key @openssl = begin OpenSSL::PKey::EC.new(asn1.to_der) rescue ArgumentError raise DecodeError, "bad key data" end super(algo: algo) end # Verify an SSH signature. # # signed_data - The String message that the signature was calculated over. # signature - The binarty String signature with SSH encoding. # # Returns boolean. def verify(signed_data, signature) sig_algo, ssh_sig, _ = Encoding.decode_signature(signature) if sig_algo != "ecdsa-sha2-#{curve}" raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}" end openssl_sig = self.class.openssl_signature(ssh_sig) digest = DIGEST_FOR_CURVE[curve] openssl.verify(digest.new, openssl_sig, signed_data) end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 Encoding.encode_fields( [:string, algo], [:string, curve], [:string, public_key_bytes], ) end # Is this public key equal to another public key? # # other - Another SSHData::PublicKey::Base instance to compare with. # # Returns boolean. def ==(other) super && other.curve == curve && other.public_key_bytes == public_key_bytes end # The digest algorithm to use with this key's curve. # # Returns an OpenSSL::Digest. def digest DIGEST_FOR_CURVE[curve] end private def asn1 unless name = OPENSSL_CURVE_NAME_FOR_CURVE[curve] raise DecodeError, "unknown curve: #{curve.inspect}" end OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::ObjectId.new("id-ecPublicKey"), OpenSSL::ASN1::ObjectId.new(name), ]), OpenSSL::ASN1::BitString.new(public_key_bytes), ]) end end end end ssh-data-1.3.0/lib/ssh_data/public_key/base.rb0000644000175100017510000000366014222500447020173 0ustar pravipravimodule SSHData module PublicKey class Base attr_reader :algo def initialize(**kwargs) @algo = kwargs[:algo] end # Calculate the fingerprint of this public key. # # md5: - Bool of whether to generate an MD5 fingerprint instead of the # default SHA256. # # Returns a String fingerprint. def fingerprint(md5: false) if md5 # colon separated, hex encoded md5 digest OpenSSL::Digest::MD5.digest(rfc4253).unpack("H2" * 16).join(":") else # base64 encoded sha256 digest with b64 padding stripped Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(rfc4253))[0...-1] end end # Make an SSH signature. # # signed_data - The String message over which to calculated the signature. # # Returns a binary String signature. def sign(signed_data) raise "implement me" end # Verify an SSH signature. # # signed_data - The String message that the signature was calculated over. # signature - The binary String signature with SSH encoding. # # Returns boolean. def verify(signed_data, signature) raise "implement me" end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 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(rfc4253), comment].compact.join(" ") end # Is this public key equal to another public key? # # other - Another SSHData::PublicKey::Base instance to compare with. # # Returns boolean. def ==(other) other.class == self.class end end end end ssh-data-1.3.0/lib/ssh_data/public_key/dsa.rb0000644000175100017510000000707014222500447020027 0ustar pravipravimodule SSHData module PublicKey class DSA < Base attr_reader :p, :q, :g, :y, :openssl # Convert an SSH encoded DSA signature to DER encoding for verification with # OpenSSL. # # sig - A binary String signature from an SSH packet. # # Returns a binary String signature, as expected by OpenSSL. def self.openssl_signature(sig) if sig.bytesize != 40 raise DecodeError, "bad DSA signature size" end r = OpenSSL::BN.new(sig.byteslice(0, 20), 2) s = OpenSSL::BN.new(sig.byteslice(20, 20), 2) OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s) ]).to_der end # Convert an DER encoded DSA signature, as generated by OpenSSL to SSH # encoding. # # sig - A binary String signature, as generated by OpenSSL. # # Returns a binary String signature, as found in an SSH packet. def self.ssh_signature(sig) a1 = OpenSSL::ASN1.decode(sig) if a1.tag_class != :UNIVERSAL || a1.tag != OpenSSL::ASN1::SEQUENCE || a1.value.count != 2 raise DecodeError, "bad asn1 signature" end r, s = a1.value if r.tag_class != :UNIVERSAL || r.tag != OpenSSL::ASN1::INTEGER || s.tag_class != :UNIVERSAL || s.tag != OpenSSL::ASN1::INTEGER raise DecodeError, "bad asn1 signature" end # left pad big endian representations to 20 bytes and concatenate [ "\x00" * (20 - r.value.num_bytes), r.value.to_s(2), "\x00" * (20 - s.value.num_bytes), s.value.to_s(2) ].join end def initialize(algo:, p:, q:, g:, y:) unless algo == ALGO_DSA raise DecodeError, "bad algorithm: #{algo.inspect}" end @p = p @q = q @g = g @y = y @openssl = OpenSSL::PKey::DSA.new(asn1.to_der) super(algo: algo) end # Verify an SSH signature. # # signed_data - The String message that the signature was calculated over. # signature - The binarty String signature with SSH encoding. # # Returns boolean. def verify(signed_data, signature) sig_algo, ssh_sig, _ = Encoding.decode_signature(signature) if sig_algo != ALGO_DSA raise DecodeError, "bad signature algorithm: #{sig_algo.inspect}" end openssl_sig = self.class.openssl_signature(ssh_sig) openssl.verify(OpenSSL::Digest::SHA1.new, openssl_sig, signed_data) end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 Encoding.encode_fields( [:string, algo], [:mpint, p], [:mpint, q], [:mpint, g], [:mpint, y], ) end # Is this public key equal to another public key? # # other - Another SSHData::PublicKey::Base instance to compare with. # # Returns boolean. def ==(other) super && other.p == p && other.q == q && other.g == g && other.y == y end private def asn1 OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::ObjectId.new("DSA"), OpenSSL::ASN1::Sequence.new([ OpenSSL::ASN1::Integer.new(p), OpenSSL::ASN1::Integer.new(q), OpenSSL::ASN1::Integer.new(g), ]), ]), OpenSSL::ASN1::BitString.new(OpenSSL::ASN1::Integer.new(y).to_der), ]) end end end end ssh-data-1.3.0/lib/ssh_data/public_key/skecdsa.rb0000644000175100017510000000407514222500447020677 0ustar pravipravimodule SSHData module PublicKey class SKECDSA < ECDSA include SecurityKey attr_reader :application OPENSSL_CURVE_NAME_FOR_CURVE = { NISTP256 => "prime256v1", } def self.check_algorithm!(algo, curve) unless algo == ALGO_SKECDSA256 raise DecodeError, "bad algorithm: #{algo.inspect}" end unless algo == "sk-ecdsa-sha2-#{curve}@openssh.com" raise DecodeError, "bad curve: #{curve.inspect}" end end def initialize(algo:, curve:, public_key:, application:) @application = application super(algo: algo, curve: curve, public_key: public_key) end # RFC4253 binary encoding of the public key. # # Returns a binary String. def rfc4253 Encoding.encode_fields( [:string, algo], [:string, curve], [:string, public_key_bytes], [:string, application], ) end def verify(signed_data, signature, **opts) opts = DEFAULT_SK_VERIFY_OPTS.merge(opts) unknown_opts = opts.keys - DEFAULT_SK_VERIFY_OPTS.keys raise UnsupportedError, "Verification options #{unknown_opts.inspect} are not supported." unless unknown_opts.empty? sig_algo, raw_sig, sk_flags, blob = build_signing_blob(application, signed_data, signature) self.class.check_algorithm!(sig_algo, curve) openssl_sig = self.class.openssl_signature(raw_sig) digest = DIGEST_FOR_CURVE[curve] result = openssl.verify(digest.new, openssl_sig, blob) # We don't know that the flags are correct until after we've validated the signature # which embeds the flags, so always verify the signature first. return false if opts[:user_presence_required] && (sk_flags & SK_FLAG_USER_PRESENCE != SK_FLAG_USER_PRESENCE) return false if opts[:user_verification_required] && (sk_flags & SK_FLAG_USER_VERIFICATION != SK_FLAG_USER_VERIFICATION) result end def ==(other) super && other.application == application end end end end ssh-data-1.3.0/lib/ssh_data/public_key.rb0000644000175100017510000000473714222500447017267 0ustar pravipravimodule SSHData module PublicKey # Public key algorithm identifiers ALGO_RSA = "ssh-rsa" ALGO_DSA = "ssh-dss" ALGO_ECDSA256 = "ecdsa-sha2-nistp256" ALGO_ECDSA384 = "ecdsa-sha2-nistp384" ALGO_ECDSA521 = "ecdsa-sha2-nistp521" ALGO_ED25519 = "ssh-ed25519" ALGO_SKED25519 = "sk-ssh-ed25519@openssh.com" ALGO_SKECDSA256 = "sk-ecdsa-sha2-nistp256@openssh.com" # RSA SHA2 *signature* algorithms used with ALGO_RSA keys. # https://tools.ietf.org/html/draft-rsa-dsa-sha2-256-02 ALGO_RSA_SHA2_256 = "rsa-sha2-256" ALGO_RSA_SHA2_512 = "rsa-sha2-512" ALGOS = [ ALGO_RSA, ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521, ALGO_ED25519, ALGO_SKECDSA256, ALGO_SKED25519 ] # Parse an OpenSSH public key in authorized_keys format (see sshd(8) manual # page). # # key - An OpenSSH formatted public key, including algo, base64 encoded key # and optional comment. # # Returns a PublicKey::Base subclass instance. def self.parse_openssh(key) algo, raw, _ = SSHData.key_parts(key) parsed = parse_rfc4253(raw) if parsed.algo != algo raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}" end parsed end # Deprecated singleton_class.send(:alias_method, :parse, :parse_openssh) # Parse an RFC 4253 binary SSH public key. # # key - A RFC 4253 binary public key String. # # Returns a PublicKey::Base subclass instance. def self.parse_rfc4253(raw) data, read = Encoding.decode_public_key(raw) if read != raw.bytesize raise DecodeError, "unexpected trailing data" end from_data(data) end def self.from_data(data) case data[:algo] when ALGO_RSA RSA.new(**data) when ALGO_DSA DSA.new(**data) when ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521 ECDSA.new(**data) when ALGO_ED25519 ED25519.new(**data) when ALGO_SKED25519 SKED25519.new(**data) when ALGO_SKECDSA256 SKECDSA.new(**data) else raise DecodeError, "unkown algo: #{data[:algo].inspect}" end end end end require "ssh_data/public_key/base" require "ssh_data/public_key/security_key" require "ssh_data/public_key/rsa" require "ssh_data/public_key/dsa" require "ssh_data/public_key/ecdsa" require "ssh_data/public_key/ed25519" require "ssh_data/public_key/sked25519" require "ssh_data/public_key/skecdsa" ssh-data-1.3.0/ssh_data.gemspec0000644000175100017510000000451314222500447015403 0ustar pravipravi######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: ssh_data 1.3.0 ruby lib Gem::Specification.new do |s| s.name = "ssh_data".freeze s.version = "1.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["mastahyeti".freeze] s.date = "2022-01-21" s.email = "opensource+ssh_data@github.com".freeze s.files = ["./LICENSE.md".freeze, "./lib/ssh_data.rb".freeze, "./lib/ssh_data/certificate.rb".freeze, "./lib/ssh_data/encoding.rb".freeze, "./lib/ssh_data/error.rb".freeze, "./lib/ssh_data/private_key.rb".freeze, "./lib/ssh_data/private_key/base.rb".freeze, "./lib/ssh_data/private_key/dsa.rb".freeze, "./lib/ssh_data/private_key/ecdsa.rb".freeze, "./lib/ssh_data/private_key/ed25519.rb".freeze, "./lib/ssh_data/private_key/rsa.rb".freeze, "./lib/ssh_data/public_key.rb".freeze, "./lib/ssh_data/public_key/base.rb".freeze, "./lib/ssh_data/public_key/dsa.rb".freeze, "./lib/ssh_data/public_key/ecdsa.rb".freeze, "./lib/ssh_data/public_key/ed25519.rb".freeze, "./lib/ssh_data/public_key/rsa.rb".freeze, "./lib/ssh_data/public_key/security_key.rb".freeze, "./lib/ssh_data/public_key/skecdsa.rb".freeze, "./lib/ssh_data/public_key/sked25519.rb".freeze, "./lib/ssh_data/signature.rb".freeze, "./lib/ssh_data/version.rb".freeze] s.homepage = "https://github.com/github/ssh_data".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.3".freeze) s.rubygems_version = "3.3.5".freeze s.summary = "Library for parsing SSH certificates".freeze if s.respond_to? :specification_version then s.specification_version = 4 end if s.respond_to? :add_runtime_dependency then s.add_development_dependency(%q.freeze, ["~> 1.2"]) s.add_development_dependency(%q.freeze, ["~> 0.14"]) s.add_development_dependency(%q.freeze, ["~> 3.10"]) s.add_development_dependency(%q.freeze, ["~> 3.10"]) else s.add_dependency(%q.freeze, ["~> 1.2"]) s.add_dependency(%q.freeze, ["~> 0.14"]) s.add_dependency(%q.freeze, ["~> 3.10"]) s.add_dependency(%q.freeze, ["~> 3.10"]) end end