Add base class for offline cosign commands

What does this MR do and why?

This MR adds SupplyChain::Cosign::Command in lib/supply_chain/cosign/command.rb, a base class for running one offline cosign CLI command. It is the first of three MRs for https://gitlab.com/gitlab-org/gitlab/-/work_items/603914 (local CA signing, ADR 007).

Local CA signing for private projects, self-managed and Dedicated (ADR 007) needs several offline cosign commands: import-key-pair now, trusted-root create and signing-config create in follow-up issues. Each command repeats the same work: run cosign, pass environment and stdin, check the result, map errors. This base class does that work once, so each subclass only needs to declare its subcommand and arguments.

Behaviour:

  • Subclasses define a SUBCOMMAND constant (for example %w[import-key-pair]), plus arguments and output_path. Optional hooks: validate!, env, input, result. execute runs validation, then cosign, then returns the result (the output path by default).
  • The command is built as ['cosign', *SUBCOMMAND, *arguments] and run with Gitlab::Popen.popen_with_detail as a token array, not through a shell.
  • The caller owns dir (an existing directory). No working directory is passed to Gitlab::Popen, because that would recreate dir with default permissions if the directory had been deleted.
  • Any output file left by an earlier run is removed first, so a cosign run that exits 0 without writing anything can never be mistaken for success.
  • Errors are handled in one place and raised as SupplyChain::Cosign::Command::Error (InvalidInput for bad input). Each message names the subcommand and says what went wrong: the exit status or signal (for example an OOM kill) with cosign's stderr, cosign missing from PATH, or cosign exiting successfully without writing its output. If cosign stops reading stdin early, its own exit status and stderr are reported instead of the broken pipe.

Not in this MR: there is no caller yet. The first subclass, ImportKeyPair (converts the leaf private key into a cosign-encrypted key), is the next MR, stacked on top of this one. Wiring into the provenance publisher comes in later MRs and in https://gitlab.com/gitlab-org/gitlab/-/work_items/603913.

No feature flag change is needed. Nothing calls this code yet, and the feature is behind the existing slsa_provenance_statement flag.

No database, API, UI or documentation changes.

Tests: spec/lib/supply_chain/cosign/command_spec.rb (14 examples, fast_spec_helper, with Gitlab::Popen stubbed so CI does not need cosign installed), using a shared context in spec/support/shared_contexts/lib/supply_chain/cosign/stubbed_cosign_shared_context.rb. 100% line (45/45) and branch (12/12) coverage of command.rb.

References

Screenshots or screen recordings

Not applicable. This is a backend-only change with no UI.

How to set up and validate locally

In a Rails console, with cosign installed:

  1. Define a throwaway subclass and run it:

    You would need to have cosign installed locally

    brew install cosign # if you want to test this locally
    cmd = Class.new(SupplyChain::Cosign::Command) do
      const_set(:SUBCOMMAND, %w[signing-config create].freeze)
      private
      def arguments = ['--out', output_path]
      def output_path = path('signing-config.json')
    end
    dir = Dir.mktmpdir
    path = cmd.new(dir: dir).execute
    File.read(path)

    Expect a JSON signing config.

  2. Show the error path: run the same subclass with cosign removed from PATH and expect SupplyChain::Cosign::Command::Error with a message containing "could not be run: Errno::ENOENT".

  3. Run the specs: bin/rspec spec/lib/supply_chain/cosign/command_spec.rb

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Saahil Sabu Hameed

Merge request reports

Loading
Loading