# frozen_string_literal: true

require_relative "helper"

unless Gem::HAVE_OPENSSL
  warn "Skipping Gem::Security::Policy tests.  openssl not found."
end

class TestGemSecurityPolicy < Gem::TestCase
  ALTERNATE_KEY    = load_key "alternate"
  INVALID_KEY      = load_key "invalid"
  CHILD_KEY        = load_key "child"
  GRANDCHILD_KEY   = load_key "grandchild"
  INVALIDCHILD_KEY = load_key "invalidchild"

  ALTERNATE_CERT      = load_cert "alternate"
  CA_CERT             = load_cert "ca"
  CHILD_CERT          = load_cert "child"
  EXPIRED_CERT        = load_cert "expired"
  FUTURE_CERT         = load_cert "future"
  GRANDCHILD_CERT     = load_cert "grandchild"
  INVALIDCHILD_CERT   = load_cert "invalidchild"
  INVALID_ISSUER_CERT = load_cert "invalid_issuer"
  INVALID_SIGNER_CERT = load_cert "invalid_signer"
  WRONG_KEY_CERT      = load_cert "wrong_key"

  def setup
    super

    @spec = quick_gem "a" do |s|
      s.description = "π"
      s.files = %w[lib/code.rb]
    end

    @digest = OpenSSL::Digest.new Gem::Security::DIGEST_NAME
    @trust_dir = Gem::Security.trust_dir.dir # HACK: use the object

    @no        = Gem::Security::NoSecurity
    @almost_no = Gem::Security::AlmostNoSecurity
    @low       = Gem::Security::LowSecurity
    @medium    = Gem::Security::MediumSecurity
    @high      = Gem::Security::HighSecurity

    @chain = Gem::Security::Policy.new(
      "Chain",
      verify_data: true,
      verify_signer: true,
      verify_chain: true,
      verify_root: false,
      only_trusted: false,
      only_signed: false
    )

    @root = Gem::Security::Policy.new(
      "Root",
      verify_data: true,
      verify_signer: true,
      verify_chain: true,
      verify_root: true,
      only_trusted: false,
      only_signed: false
    )
  end

  def test_check_data
    data = digest "hello"

    signature = sign data

    assert @almost_no.check_data(PUBLIC_KEY, @digest, signature, data)
  end

  def test_check_data_invalid
    data = digest "hello"

    signature = sign data

    invalid = digest "hello!"

    e = assert_raise Gem::Security::Exception do
      @almost_no.check_data PUBLIC_KEY, @digest, signature, invalid
    end

    assert_equal "invalid signature", e.message
  end

  def test_check_chain
    chain = [PUBLIC_CERT, CHILD_CERT, GRANDCHILD_CERT]

    assert @chain.check_chain chain, Time.now
  end

  def test_check_chain_empty_chain
    e = assert_raise Gem::Security::Exception do
      @chain.check_chain [], Time.now
    end

    assert_equal "empty signing chain", e.message
  end

  def test_check_chain_invalid
    chain = [PUBLIC_CERT, CHILD_CERT, INVALIDCHILD_CERT]

    e = assert_raise Gem::Security::Exception do
      @chain.check_chain chain, Time.now
    end

    assert_equal "invalid signing chain: " \
                 "certificate #{INVALIDCHILD_CERT.subject} " \
                 "was not issued by #{CHILD_CERT.subject}", e.message
  end

  def test_check_chain_no_chain
    e = assert_raise Gem::Security::Exception do
      @chain.check_chain nil, Time.now
    end

    assert_equal "missing signing chain", e.message
  end

  def test_check_cert
    assert @low.check_cert(PUBLIC_CERT, nil, Time.now)
  end

  def test_check_cert_expired
    e = assert_raise Gem::Security::Exception do
      @low.check_cert EXPIRED_CERT, nil, Time.now
    end

    assert_equal "certificate #{EXPIRED_CERT.subject} " \
                 "not valid after #{EXPIRED_CERT.not_after}",
                 e.message
  end

  def test_check_cert_future
    e = assert_raise Gem::Security::Exception do
      @low.check_cert FUTURE_CERT, nil, Time.now
    end

    assert_equal "certificate #{FUTURE_CERT.subject} " \
                 "not valid before #{FUTURE_CERT.not_before}",
                 e.message
  end

  def test_check_cert_invalid_issuer
    e = assert_raise Gem::Security::Exception do
      @low.check_cert INVALID_ISSUER_CERT, PUBLIC_CERT, Time.now
    end

    assert_equal "certificate #{INVALID_ISSUER_CERT.subject} " \
                 "was not issued by #{PUBLIC_CERT.subject}",
                 e.message
  end

  def test_check_cert_issuer
    assert @low.check_cert(CHILD_CERT, PUBLIC_CERT, Time.now)
  end

  def test_check_cert_no_signer
    e = assert_raise Gem::Security::Exception do
      @high.check_cert(nil, nil, Time.now)
    end

    assert_equal "missing signing certificate", e.message
  end

  def test_check_key
    assert @almost_no.check_key(PUBLIC_CERT, PRIVATE_KEY)
  end

  def test_check_key_no_signer
    assert @almost_no.check_key(nil, nil)

    e = assert_raise Gem::Security::Exception do
      @high.check_key(nil, nil)
    end

    assert_equal "missing key or signature", e.message
  end

  def test_check_key_wrong_key
    e = assert_raise Gem::Security::Exception do
      @almost_no.check_key(PUBLIC_CERT, ALTERNATE_KEY)
    end

    assert_equal "certificate #{PUBLIC_CERT.subject} " \
                 "does not match the signing key", e.message
  end

  def test_check_root
    chain = [PUBLIC_CERT, CHILD_CERT, INVALIDCHILD_CERT]

    assert @chain.check_root chain, Time.now
  end

  def test_check_root_empty_chain
    e = assert_raise Gem::Security::Exception do
      @chain.check_root [], Time.now
    end

    assert_equal "missing root certificate", e.message
  end

  def test_check_root_invalid_signer
    chain = [INVALID_SIGNER_CERT]

    e = assert_raise Gem::Security::Exception do
      @chain.check_root chain, Time.now
    end

    assert_equal "certificate #{INVALID_SIGNER_CERT.subject} " \
                 "was not issued by #{INVALID_SIGNER_CERT.issuer}",
                 e.message
  end

  def test_check_root_not_self_signed
    chain = [INVALID_ISSUER_CERT]

    e = assert_raise Gem::Security::Exception do
      @chain.check_root chain, Time.now
    end

    assert_equal "root certificate #{INVALID_ISSUER_CERT.subject} " \
                 "is not self-signed (issuer #{INVALID_ISSUER_CERT.issuer})",
                 e.message
  end

  def test_check_root_no_chain
    e = assert_raise Gem::Security::Exception do
      @chain.check_root nil, Time.now
    end

    assert_equal "missing signing chain", e.message
  end

  def test_check_trust
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    assert @high.check_trust [PUBLIC_CERT], @digest, @trust_dir
  end

  def test_check_trust_child
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    assert @high.check_trust [PUBLIC_CERT, CHILD_CERT], @digest, @trust_dir
  end

  def test_check_trust_empty_chain
    e = assert_raise Gem::Security::Exception do
      @chain.check_trust [], @digest, @trust_dir
    end

    assert_equal "missing root certificate", e.message
  end

  def test_check_trust_mismatch
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    e = assert_raise Gem::Security::Exception do
      @high.check_trust [WRONG_KEY_CERT], @digest, @trust_dir
    end

    assert_equal "trusted root certificate #{PUBLIC_CERT.subject} checksum " \
                 "does not match signing root certificate checksum", e.message
  end

  def test_check_trust_no_chain
    e = assert_raise Gem::Security::Exception do
      @chain.check_trust nil, @digest, @trust_dir
    end

    assert_equal "missing signing chain", e.message
  end

  def test_check_trust_no_trust
    e = assert_raise Gem::Security::Exception do
      @high.check_trust [PUBLIC_CERT], @digest, @trust_dir
    end

    assert_equal "root cert #{PUBLIC_CERT.subject} is not trusted", e.message
  end

  def test_check_trust_no_trust_child
    e = assert_raise Gem::Security::Exception do
      @high.check_trust [PUBLIC_CERT, CHILD_CERT], @digest, @trust_dir
    end

    assert_equal "root cert #{PUBLIC_CERT.subject} is not trusted " \
                 "(root of signing cert #{CHILD_CERT.subject})", e.message
  end

  def test_subject
    assert_equal "email:nobody@example", @no.subject(PUBLIC_CERT)
    assert_equal "/C=JP/ST=Tokyo/O=RubyGemsTest/CN=CA", @no.subject(CA_CERT)
  end

  def test_verify
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    assert @almost_no.verify [PUBLIC_CERT], nil, *dummy_signatures
  end

  def test_verify_chain_signatures
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    assert @high.verify [PUBLIC_CERT], nil, *dummy_signatures
  end

  def test_verify_chain_key
    @almost_no.verify [PUBLIC_CERT], PRIVATE_KEY, *dummy_signatures
  end

  def test_verify_no_digests
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    _, signatures = dummy_signatures

    e = assert_raise Gem::Security::Exception do
      @almost_no.verify [PUBLIC_CERT], nil, {}, signatures
    end

    assert_equal "no digests provided (probable bug)", e.message
  end

  def test_verify_no_digests_no_security
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    _, signatures = dummy_signatures

    e = assert_raise Gem::Security::Exception do
      @no.verify [PUBLIC_CERT], nil, {}, signatures
    end

    assert_equal "missing digest for 0", e.message
  end

  def test_verify_no_signatures
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    digests, = dummy_signatures

    use_ui @ui do
      @no.verify [PUBLIC_CERT], nil, digests, {}, "some_gem"
    end

    assert_match "WARNING:  some_gem is not signed\n", @ui.error

    assert_raise Gem::Security::Exception do
      @high.verify [PUBLIC_CERT], nil, digests, {}
    end
  end

  def test_verify_no_signatures_no_digests
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    use_ui @ui do
      @no.verify [PUBLIC_CERT], nil, {}, {}, "some_gem"
    end

    assert_empty @ui.output
    assert_empty @ui.error
  end

  def test_verify_not_enough_signatures
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    digests, signatures = dummy_signatures

    data = digest "goodbye"

    signatures[1] = PRIVATE_KEY.sign @digest.new, data.digest

    e = assert_raise Gem::Security::Exception do
      @almost_no.verify [PUBLIC_CERT], nil, digests, signatures
    end

    assert_equal "missing digest for 1", e.message
  end

  def test_verify_no_trust
    digests, signatures = dummy_signatures

    use_ui @ui do
      @low.verify [PUBLIC_CERT], nil, digests, signatures, "some_gem"
    end

    assert_equal "WARNING:  email:nobody@example is not trusted for some_gem\n",
                 @ui.error

    assert_raise Gem::Security::Exception do
      @medium.verify [PUBLIC_CERT], nil, digests, signatures
    end
  end

  def test_verify_wrong_digest_type
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    data = OpenSSL::Digest.new("SHA512")
    data << "hello"

    digests    = { "SHA512" => { 0 => data } }
    signature  = PRIVATE_KEY.sign "sha512", data.digest
    signatures = { 0 => signature }

    e = assert_raise Gem::Security::Exception do
      @almost_no.verify [PUBLIC_CERT], nil, digests, signatures
    end

    assert_equal "no digests provided (probable bug)", e.message
  end

  def test_verify_signatures_chain
    @spec.cert_chain = [PUBLIC_CERT, CHILD_CERT]

    assert @chain.verify_signatures @spec, *dummy_signatures(CHILD_KEY)
  end

  def test_verify_signatures_data
    @spec.cert_chain = [PUBLIC_CERT]

    @almost_no.verify_signatures @spec, *dummy_signatures
  end

  def test_verify_signatures_root
    @spec.cert_chain = [PUBLIC_CERT, CHILD_CERT]

    assert @root.verify_signatures @spec, *dummy_signatures(CHILD_KEY)
  end

  def test_verify_signatures_signer
    @spec.cert_chain = [PUBLIC_CERT]

    assert @low.verify_signatures @spec, *dummy_signatures
  end

  def test_verify_signatures_trust
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    @spec.cert_chain = [PUBLIC_CERT]

    assert @high.verify_signatures @spec, *dummy_signatures
  end

  def test_verify_signatures
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    @spec.cert_chain = [PUBLIC_CERT.to_s]

    metadata_gz = Gem::Util.gzip @spec.to_yaml

    package = Gem::Package.new "nonexistent.gem"
    package.checksums[Gem::Security::DIGEST_NAME] = {}

    s = StringIO.new metadata_gz
    def s.full_name
      "metadata.gz"
    end

    digests = package.digest s
    metadata_gz_digest = digests[Gem::Security::DIGEST_NAME]["metadata.gz"]

    signatures = {}
    signatures["metadata.gz"] =
      PRIVATE_KEY.sign @digest.new, metadata_gz_digest.digest

    assert @high.verify_signatures @spec, digests, signatures
  end

  def test_verify_signatures_missing
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    @spec.cert_chain = [PUBLIC_CERT.to_s]

    metadata_gz = Gem::Util.gzip @spec.to_yaml

    package = Gem::Package.new "nonexistent.gem"
    package.checksums[Gem::Security::DIGEST_NAME] = {}

    s = StringIO.new metadata_gz
    def s.full_name
      "metadata.gz"
    end

    digests = package.digest s
    digests[Gem::Security::DIGEST_NAME]["data.tar.gz"] = @digest.hexdigest "hello"

    metadata_gz_digest = digests[Gem::Security::DIGEST_NAME]["metadata.gz"]

    signatures = {}
    signatures["metadata.gz"] =
      PRIVATE_KEY.sign @digest.new, metadata_gz_digest.digest

    e = assert_raise Gem::Security::Exception do
      @high.verify_signatures @spec, digests, signatures
    end

    assert_equal "missing signature for data.tar.gz", e.message
  end

  def test_verify_signatures_none
    Gem::Security.trust_dir.trust_cert PUBLIC_CERT

    @spec.cert_chain = [PUBLIC_CERT.to_s]

    metadata_gz = Gem::Util.gzip @spec.to_yaml

    package = Gem::Package.new "nonexistent.gem"
    package.checksums[Gem::Security::DIGEST_NAME] = {}

    s = StringIO.new metadata_gz
    def s.full_name
      "metadata.gz"
    end

    digests = package.digest s
    digests[Gem::Security::DIGEST_NAME]["data.tar.gz"] = @digest.hexdigest "hello"

    assert_raise Gem::Security::Exception do
      @high.verify_signatures @spec, digests, {}
    end
  end

  def digest(data)
    digester = @digest.new
    digester << data
    digester
  end

  def sign(data, key = PRIVATE_KEY)
    key.sign @digest.new, data.digest
  end

  def dummy_signatures(key = PRIVATE_KEY)
    data = digest "hello"

    digests    = { Gem::Security::DIGEST_NAME => { 0 => data } }
    signatures = { 0 => sign(data, key) }

    [digests, signatures]
  end
end if Gem::HAVE_OPENSSL
