riboseinc/rspec-pgp_matchers

View on GitHub
lib/rspec/pgp_matchers/be_a_valid_pgp_signature_of.rb

Summary

Maintainability
A
0 mins
Test Coverage
# (c) Copyright 2018 Ribose Inc.
#

# A following PGP matcher calls the GPG executable in a subshell, then
# parses command output.  This is a poor pattern in general, because output
# messages may subtly change over GPG evolution.
#
# However, GPG executables do not provide any machine-readable output which
# could be used instead.  Furthermore, using RNP or GPGME from here is also
# a poor idea, because this gem is going to be tested against various
# combinations of software, in some of which that dependency may be unavailable.
#
# If output parsing will ever become a source of problems, then the preferred
# solution is to develop a minimalist validator which, if executed in
# a subshell, returns useful machine-readable output.  A validator tool running
# in a separate process may leverage GPGME, as it won't be exposed outside
# the validator.  A previous implementation of this matcher may provide some
# useful ideas.  See commit 2e2bd0da090d7d31ecacc2d1ea6bd3e13479e675.
#
# rubocop:disable Metrics/BlockLength
RSpec::Matchers.define :be_a_valid_pgp_signature_of do |text|
  # rubocop:enable Metrics/BlockLength
  include RSpec::PGPMatchers::GPGMatcherHelper

  attr_reader :err

  match do |signature_string|
    @err = verify_signature(text, signature_string)
    @err.nil?
  end

  chain :signed_by, :expected_signer

  failure_message do
    err
  end

  # Returns +nil+ if first signature is valid, or an error message otherwise.
  def verify_signature(cleartext, signature_string)
    cmd_output = run_verify(cleartext, signature_string)
    cmd_result = analyse_verify_output(*cmd_output)

    if cmd_result[:well_formed_pgp_data]
      match_constraints(**cmd_result)
    else
      msg_no_pgg_data(signature_string)
    end
  end

  def analyse_verify_output(_stdout_str, stderr_str, status)
    {
      well_formed_pgp_data: (status.exitstatus != 2),
      signature: detect_signers(stderr_str).first,
    }
  end

  def match_constraints(signature:, **_ignored)
    match_signature(signature)
  end

  def msg_mismatch(text)
    "expected given signature to be a valid Open PGP signature " +
      "of following text:\n#{text}"
  end

  def msg_no_pgg_data(file_text)
    "expected given text to be a valid Open PGP signature, " +
      "but it contains no PGP data, just:\n#{file_text}"
  end

  def msg_wrong_signer(actual_signer)
    "expected singature to be signed by #{expected_signer}, " +
      "but was actually signed by #{actual_signer}"
  end
end