summaryrefslogtreecommitdiff
path: root/lib/gitlab/ssh_public_key.rb
blob: 2680b2456f8863f0e50c1c6276e1e936364205fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
require 'net/ssh'

module Gitlab
  class SSHPublicKey
    include Gitlab::Popen

    TYPES = %w[rsa dsa ecdsa].freeze

    Technology = Struct.new(:name, :allowed_sizes)

    Technologies = [
      Technology.new('rsa',   [1024, 2048, 3072, 4096]),
      Technology.new('dsa',   [1024, 2048, 3072]),
      Technology.new('ecdsa', [256, 384, 521])
    ].freeze

    def self.technology_names
      Technologies.map(&:name)
    end

    def self.technology(name)
      Technologies.find { |ssh_key_technology| ssh_key_technology.name == name }
    end
    private_class_method :technology

    def self.allowed_sizes(name)
      technology(name).allowed_sizes
    end

    def self.allowed_type?(type)
      technology_names.include?(type.to_s)
    end

    def initialize(key_text)
      @key_text = key_text
    end

    def valid?
      type.present?
    end

    def type
      return @type if defined?(@type)

      @type =
        case key
        when OpenSSL::PKey::EC
          :ecdsa
        when OpenSSL::PKey::RSA
          :rsa
        when OpenSSL::PKey::DSA
          :dsa
        end
    end

    def size
      return @size if defined?(@size)

      @size =
        case type
        when :ecdsa
          key.public_key.to_bn.num_bits / 2
        when :rsa
          key.n.num_bits
        when :dsa
          1024
        end
    end

    def fingerprint
      cmd_status = 0
      cmd_output = ''

      Tempfile.open('gitlab_key_file') do |file|
        file.puts key_text
        file.rewind

        cmd = []
        cmd.push('ssh-keygen')
        cmd.push('-E', 'md5') if explicit_fingerprint_algorithm?
        cmd.push('-lf', file.path)

        cmd_output, cmd_status = popen(cmd, '/tmp')
      end

      return nil unless cmd_status.zero?

      # 16 hex bytes separated by ':', optionally starting with "MD5:"
      fingerprint_matches = cmd_output.match(/(MD5:)?(?<fingerprint>(\h{2}:){15}\h{2})/)
      return nil unless fingerprint_matches

      fingerprint_matches[:fingerprint]
    end

    private

    attr_accessor :key_text

    def key
      return @key if defined?(@key)

      @key = begin
        Net::SSH::KeyFactory.load_data_public_key(key_text)
      rescue StandardError, NotImplementedError
        nil
      end
    end

    def explicit_fingerprint_algorithm?
      # OpenSSH 6.8 introduces a new default output format for fingerprints.
      # Check the version and decide which command to use.

      version_output, version_status = popen(%w[ssh -V])
      return false unless version_status.zero?

      version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/)
      return false unless version_matches

      version_info = Gitlab::VersionInfo.new(version_matches[:major].to_i, version_matches[:minor].to_i)

      required_version_info = Gitlab::VersionInfo.new(6, 8)

      version_info >= required_version_info
    end
  end
end