require 'securerandom'

module Net
  module SSH
    module Authentication
      # Class for representing an SSH certificate.
      #
      # http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.10&content-type=text/plain
      class Certificate
        attr_accessor :nonce
        attr_accessor :key
        attr_accessor :serial
        attr_accessor :type
        attr_accessor :key_id
        attr_accessor :valid_principals
        attr_accessor :valid_after
        attr_accessor :valid_before
        attr_accessor :critical_options
        attr_accessor :extensions
        attr_accessor :reserved
        attr_accessor :signature_key
        attr_accessor :signature

        # Read a certificate blob associated with a key of the given type.
        def self.read_certblob(buffer, type)
          cert = Certificate.new
          cert.nonce = buffer.read_string
          cert.key = buffer.read_keyblob(type)
          cert.serial = buffer.read_int64
          cert.type = type_symbol(buffer.read_long)
          cert.key_id = buffer.read_string
          cert.valid_principals = buffer.read_buffer.read_all(&:read_string)
          cert.valid_after = Time.at(buffer.read_int64)

          cert.valid_before = if RUBY_PLATFORM == "java"
                                # 0x20c49ba5e353f7 = 0x7fffffffffffffff/1000, the largest value possible for JRuby
                                # JRuby Time.at multiplies the arg by 1000, and then stores it in a signed long.
                                # 0x20c49ba2d52500 = 292278993-01-01 00:00:00 +0000
                                # JRuby 9.1 does not accept the year 292278994 because of edge cases (https://github.com/JodaOrg/joda-time/issues/190)
                                Time.at([0x20c49ba2d52500, buffer.read_int64].min)
                              else
                                Time.at(buffer.read_int64)
                              end

          cert.critical_options = read_options(buffer)
          cert.extensions = read_options(buffer)
          cert.reserved = buffer.read_string
          cert.signature_key = buffer.read_buffer.read_key
          cert.signature = buffer.read_string
          cert
        end

        def ssh_type
          key.ssh_type + "-cert-v01@openssh.com"
        end

        def ssh_signature_type
          key.ssh_type
        end

        # Serializes the certificate (and key).
        def to_blob
          Buffer.from(
            :raw, to_blob_without_signature,
            :string, signature
          ).to_s
        end

        def ssh_do_sign(data, sig_alg = nil)
          key.ssh_do_sign(data, sig_alg)
        end

        def ssh_do_verify(sig, data, options = {})
          key.ssh_do_verify(sig, data, options)
        end

        def to_pem
          key.to_pem
        end

        def fingerprint
          key.fingerprint
        end

        # Signs the certificate with key.
        def sign!(key, sign_nonce = nil)
          # ssh-keygen uses 32 bytes of nonce.
          self.nonce = sign_nonce || SecureRandom.random_bytes(32)
          self.signature_key = key
          self.signature = Net::SSH::Buffer.from(
            :string, key.ssh_signature_type,
            :mstring, key.ssh_do_sign(to_blob_without_signature)
          ).to_s
          self
        end

        def sign(key, sign_nonce = nil)
          cert = clone
          cert.sign!(key, sign_nonce)
        end

        # Checks whether the certificate's signature was signed by signature key.
        def signature_valid?
          buffer = Buffer.new(signature)
          sig_format = buffer.read_string
          signature_key.ssh_do_verify(buffer.read_string, to_blob_without_signature, host_key: sig_format)
        end

        def self.read_options(buffer)
          names = []
          options = buffer.read_buffer.read_all do |b|
            name = b.read_string
            names << name
            data = b.read_string
            data = Buffer.new(data).read_string unless data.empty?
            [name, data]
          end

          raise ArgumentError, "option/extension names must be in sorted order" if names.sort != names

          Hash[options]
        end
        private_class_method :read_options

        def self.type_symbol(type)
          types = { 1 => :user, 2 => :host }
          raise ArgumentError("unsupported type: #{type}") unless types.include?(type)

          types.fetch(type)
        end
        private_class_method :type_symbol

        private

        def type_value(type)
          types = { user: 1, host: 2 }
          raise ArgumentError("unsupported type: #{type}") unless types.include?(type)

          types.fetch(type)
        end

        def ssh_time(t)
          # Times in certificates are represented as a uint64.
          [[t.to_i, 0].max, 2 << 64 - 1].min
        end

        def to_blob_without_signature
          Buffer.from(
            :string, ssh_type,
            :string, nonce,
            :raw, key_without_type,
            :int64, serial,
            :long, type_value(type),
            :string, key_id,
            :string, valid_principals.inject(Buffer.new) { |acc, elem| acc.write_string(elem) }.to_s,
            :int64, ssh_time(valid_after),
            :int64, ssh_time(valid_before),
            :string, options_to_blob(critical_options),
            :string, options_to_blob(extensions),
            :string, reserved,
            :string, signature_key.to_blob
          ).to_s
        end

        def key_without_type
          # key.to_blob gives us e.g. "ssh-rsa,<key>" but we just want "<key>".
          tmp = Buffer.new(key.to_blob)
          tmp.read_string # skip the underlying key type
          tmp.read
        end

        def options_to_blob(options)
          options.keys.sort.inject(Buffer.new) do |b, name|
            b.write_string(name)
            data = options.fetch(name)
            data = Buffer.from(:string, data).to_s unless data.empty?
            b.write_string(data)
          end.to_s
        end
      end
    end
  end
end
