146 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			146 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
module Gitlab
 | 
						|
  class SSHPublicKey
 | 
						|
    include Gitlab::Utils::StrongMemoize
 | 
						|
 | 
						|
    Technology = Struct.new(:name, :key_class, :supported_sizes, :supported_algorithms)
 | 
						|
 | 
						|
    # See https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT for the list of
 | 
						|
    # supported algorithms.
 | 
						|
    TECHNOLOGIES = [
 | 
						|
      Technology.new(:rsa, SSHData::PublicKey::RSA, [1024, 2048, 3072, 4096], %w[ssh-rsa]),
 | 
						|
      Technology.new(:dsa, SSHData::PublicKey::DSA, [1024, 2048, 3072], %w[ssh-dss]),
 | 
						|
      Technology.new(:ecdsa, SSHData::PublicKey::ECDSA, [256, 384, 521], %w[ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521]),
 | 
						|
      Technology.new(:ed25519, SSHData::PublicKey::ED25519, [256], %w[ssh-ed25519]),
 | 
						|
      Technology.new(:ecdsa_sk, SSHData::PublicKey::SKECDSA, [256], %w[sk-ecdsa-sha2-nistp256@openssh.com]),
 | 
						|
      Technology.new(:ed25519_sk, SSHData::PublicKey::SKED25519, [256], %w[sk-ssh-ed25519@openssh.com])
 | 
						|
    ].freeze
 | 
						|
 | 
						|
    def self.technologies
 | 
						|
      if Gitlab::FIPS.enabled?
 | 
						|
        Gitlab::FIPS::SSH_KEY_TECHNOLOGIES
 | 
						|
      else
 | 
						|
        TECHNOLOGIES
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def self.technology(name)
 | 
						|
      technologies.find { |tech| tech.name.to_s == name.to_s }
 | 
						|
    end
 | 
						|
 | 
						|
    def self.technology_for_key(key)
 | 
						|
      technologies.find { |tech| key.instance_of?(tech.key_class) }
 | 
						|
    end
 | 
						|
 | 
						|
    def self.supported_types
 | 
						|
      technologies.map(&:name)
 | 
						|
    end
 | 
						|
 | 
						|
    def self.supported_sizes(name)
 | 
						|
      technology(name).supported_sizes
 | 
						|
    end
 | 
						|
 | 
						|
    def self.supported_algorithms
 | 
						|
      technologies.flat_map { |tech| tech.supported_algorithms }
 | 
						|
    end
 | 
						|
 | 
						|
    def self.supported_algorithms_for_name(name)
 | 
						|
      technology(name).supported_algorithms
 | 
						|
    end
 | 
						|
 | 
						|
    def self.sanitize(key_content)
 | 
						|
      ssh_type, *parts = key_content.strip.split
 | 
						|
 | 
						|
      return key_content if parts.empty?
 | 
						|
 | 
						|
      parts.each_with_object(+"#{ssh_type} ").with_index do |(part, content), index|
 | 
						|
        content << part
 | 
						|
 | 
						|
        if self.new(content).valid?
 | 
						|
          break [content, parts[index + 1]].compact.join(' ') # Add the comment part if present
 | 
						|
        elsif parts.size == index + 1 # return original content if we've reached the last element
 | 
						|
          break key_content
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    attr_reader :key_text, :key
 | 
						|
 | 
						|
    def initialize(key_text)
 | 
						|
      @key_text = key_text
 | 
						|
 | 
						|
      # We need to strip options to parse key with options or in known_hosts
 | 
						|
      # format. See https://man.openbsd.org/sshd#AUTHORIZED_KEYS_FILE_FORMAT
 | 
						|
      # and https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT
 | 
						|
      key_text_without_options = @key_text.to_s.match(/(\A|\s)(#{self.class.supported_algorithms.join('|')}).*/).to_s
 | 
						|
 | 
						|
      @key =
 | 
						|
        begin
 | 
						|
          SSHData::PublicKey.parse_openssh(key_text_without_options)
 | 
						|
        rescue SSHData::DecodeError
 | 
						|
        end
 | 
						|
    end
 | 
						|
 | 
						|
    def valid?
 | 
						|
      key.present?
 | 
						|
    end
 | 
						|
 | 
						|
    def type
 | 
						|
      technology.name if valid?
 | 
						|
    end
 | 
						|
 | 
						|
    def fingerprint
 | 
						|
      key.fingerprint(md5: true) if valid?
 | 
						|
    end
 | 
						|
 | 
						|
    def fingerprint_sha256
 | 
						|
      'SHA256:' + key.fingerprint(md5: false) if valid?
 | 
						|
    end
 | 
						|
 | 
						|
    def bits
 | 
						|
      return unless valid?
 | 
						|
 | 
						|
      case type
 | 
						|
      when :rsa
 | 
						|
        key.n.num_bits
 | 
						|
      when :dsa
 | 
						|
        key.p.num_bits
 | 
						|
      when :ecdsa
 | 
						|
        key.openssl.group.order.num_bits
 | 
						|
      when :ed25519
 | 
						|
        256
 | 
						|
      when :ecdsa_sk
 | 
						|
        256
 | 
						|
      when :ed25519_sk
 | 
						|
        256
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def banned?
 | 
						|
      return false unless valid?
 | 
						|
 | 
						|
      banned_ssh_keys.fetch(type.to_s, []).include?(fingerprint_sha256)
 | 
						|
    end
 | 
						|
 | 
						|
    private
 | 
						|
 | 
						|
    def banned_ssh_keys
 | 
						|
      path = Rails.root.join('config/security/banned_ssh_keys.yml')
 | 
						|
      config = YAML.load_file(path) if File.exist?(path)
 | 
						|
 | 
						|
      config || {}
 | 
						|
    end
 | 
						|
    strong_memoize_attr :banned_ssh_keys
 | 
						|
 | 
						|
    def technology
 | 
						|
      @technology ||=
 | 
						|
        self.class.technology_for_key(key) || raise_unsupported_key_type_error
 | 
						|
    end
 | 
						|
 | 
						|
    def raise_unsupported_key_type_error
 | 
						|
      raise("Unsupported key type: #{key.class}")
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |