Skip to content

Commit

Permalink
Extract cli into a separate gem we can publish
Browse files Browse the repository at this point in the history
So other workflows can `gem exec sigstore-cli` without rewriting its functionality over and over

Signed-off-by: Samuel Giddins <segiddins@segiddins.me>
  • Loading branch information
segiddins committed Nov 18, 2024
1 parent c3c0970 commit b4baee4
Show file tree
Hide file tree
Showing 19 changed files with 230 additions and 76 deletions.
7 changes: 1 addition & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,7 @@ jobs:
run: bin/rake build
- name: Run the smoketest
run: |
# we smoke-test sigstore by installing each of the distributions
# we've built in a fresh environment and using each to sign and
# verify for itself, using the ambient OIDC identity
for dist in pkg/*; do
./bin/smoketest "${dist}"
done
./bin/smoketest pkg/*.gem
env:
WORKFLOW_NAME: ci

Expand Down
7 changes: 1 addition & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,7 @@ jobs:

- name: sign
run: |
# we smoke-test sigstore by installing each of the distributions
# we've built in a fresh environment and using each to sign and
# verify for itself, using the ambient OIDC identity
for dist in pkg/*; do
./bin/smoketest "${dist}"
done
./bin/smoketest pkg/*.gem
- name: Generate hashes for provenance
shell: bash
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

# Specify your gem's dependencies in sigstore.gemspec
gemspec
gemspec path: "cli"

gem "base64", "~> 0.2.0" # Until https://github.com/vcr/vcr/commit/5c9230b43b6a51dec78941d16bf8e2954042964c is released
gem "rake", "~> 13.2"
Expand Down
11 changes: 10 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ PATH
protobug_sigstore_protos (~> 0.1.0)
uri

PATH
remote: cli
specs:
sigstore-cli (0.1.1)
sigstore (= 0.1.1)
thor

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -100,6 +107,7 @@ DEPENDENCIES
rubocop-performance (~> 1.23)
rubocop-rake (~> 0.6.0)
sigstore!
sigstore-cli!
simplecov (~> 0.22.0)
test-unit (~> 3.0)
thor (~> 1.3)
Expand Down Expand Up @@ -140,6 +148,7 @@ CHECKSUMS
rubocop-rake (0.6.0) sha256=56b6f22189af4b33d4f4e490a555c09f1281b02f4d48c3a61f6e8fe5f401d8db
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
sigstore (0.1.1)
sigstore-cli (0.1.1)
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
simplecov-html (0.12.3) sha256=4b1aad33259ffba8b29c6876c12db70e5750cb9df829486e4c6e5da4fa0aa07b
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
Expand All @@ -152,4 +161,4 @@ CHECKSUMS
webmock (3.24.0) sha256=be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122

BUNDLED WITH
2.5.16
2.5.23
9 changes: 9 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
require "bundler/gem_tasks"
require "rake/testtask"

directory "pkg"
namespace "cli" do
Bundler::GemHelper.install_tasks(dir: "cli")
task build: "pkg" do # rubocop:disable Rake/Desc
FileUtils.cp_r FileList["cli/pkg/*"], "pkg"
end
end
task "build" => "cli:build" # rubocop:disable Rake/Desc

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.test_files = FileList["test/**/*_test.rb"]
Expand Down
3 changes: 1 addition & 2 deletions bin/conformance-entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,5 @@ ENV.update(
"XDG_CACHE_HOME" => nil
)

load File.expand_path("sigstore-ruby", __dir__)

require "sigstore/cli"
Sigstore::CLI.start(ARGV << "--no-update-trusted-root")
27 changes: 27 additions & 0 deletions bin/sigstore-cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'sigstore-cli' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

bundle_binstub = File.expand_path("bundle", __dir__)

if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end

require "rubygems"
require "bundler/setup"

load Gem.bin_path("sigstore-cli", "sigstore-cli")
68 changes: 32 additions & 36 deletions bin/smoketest
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ require "json"

include FileUtils # rubocop:disable Style/MixinUsage

dist = ARGV[0] || raise(StandardError, "Usage: #{$PROGRAM_NAME} <dist>")
raise(StandardError, "Usage: #{$PROGRAM_NAME} <dists...>") if ARGV.empty?

dists = ARGV
mkdir_p %w[smoketest-gem-home smoketest-artifacts]

at_exit { rm_rf "smoketest-gem-home" }
Expand All @@ -20,42 +22,36 @@ env = {
"BUNDLE_GEMFILE" => "smoketest-gem-home/Gemfile"
}

sh(env, "gem", "install", dist, "--no-document", exception: true)
sh(env, "gem", "install", "thor", "--no-document", exception: true)
cert_identity = "#{ENV.fetch("GITHUB_SERVER_URL")}/#{ENV.fetch("GITHUB_REPOSITORY")}" \
"/.github/workflows/#{ENV.fetch("WORKFLOW_NAME", "release")}.yml@#{ENV.fetch("GITHUB_REF")}"

sh(env, "gem", "install", *dists, "--no-document", exception: true)

File.write("smoketest-gem-home/Gemfile", <<~RUBY)
gem "sigstore"
gem "thor"
gem "sigstore-cli"
RUBY

id_token ||= Net::HTTP.get_response(
URI(ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_URL") + "&audience=#{URI.encode_uri_component("sigstore")}"),
{ "Authorization" => "bearer #{ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_TOKEN")}" },
&:value
).body.then { JSON.parse(_1).fetch("value") }

sh(env, File.expand_path("sigstore-ruby", __dir__),
"sign", dist, "--identity-token=#{id_token}",
"--signature=smoketest-artifacts/#{File.basename(dist)}.sig",
"--certificate=smoketest-artifacts/#{File.basename(dist)}.crt",
"--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json",
exception: true)

cert_identity = "#{ENV.fetch("GITHUB_SERVER_URL")}/#{ENV.fetch("GITHUB_REPOSITORY")}" \
"/.github/workflows/#{ENV.fetch("WORKFLOW_NAME", "release")}.yml@#{ENV.fetch("GITHUB_REF")}"

sh(env, File.expand_path("sigstore-ruby", __dir__),
"verify",
"--signature=smoketest-artifacts/#{File.basename(dist)}.sig",
"--certificate=smoketest-artifacts/#{File.basename(dist)}.crt",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com",
"--certificate-identity=#{cert_identity}",
dist,
exception: true)
sh(env, File.expand_path("sigstore-ruby", __dir__),
"verify",
"--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com",
"--certificate-identity=#{cert_identity}",
dist,
exception: true)
dists.each do |dist|
sh(env, File.expand_path("sigstore-cli", __dir__),
"sign", dist,
"--signature=smoketest-artifacts/#{File.basename(dist)}.sig",
"--certificate=smoketest-artifacts/#{File.basename(dist)}.crt",
"--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json",
exception: true)

sh(env, File.expand_path("sigstore-cli", __dir__),
"verify",
"--signature=smoketest-artifacts/#{File.basename(dist)}.sig",
"--certificate=smoketest-artifacts/#{File.basename(dist)}.crt",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com",
"--certificate-identity=#{cert_identity}",
dist,
exception: true)
sh(env, File.expand_path("sigstore-cli", __dir__),
"verify",
"--bundle=smoketest-artifacts/#{File.basename(dist)}.sigstore.json",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com",
"--certificate-identity=#{cert_identity}",
dist,
exception: true)
end
2 changes: 1 addition & 1 deletion bin/tuf-conformance-entrypoint
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ end
ARGV.prepend("tuf")
ARGV[2, 0] = args

load File.expand_path("sigstore-ruby", __dir__)
require "sigstore/cli"
Sigstore::CLI.start(ARGV)
2 changes: 2 additions & 0 deletions cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pkg/
*.gem
5 changes: 5 additions & 0 deletions cli/exe/sigstore-cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "sigstore/cli"
Sigstore::CLI.start
26 changes: 8 additions & 18 deletions bin/sigstore-ruby → cli/lib/sigstore/cli.rb
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup"

require "thor"
require "sigstore"
require "sigstore/error"

module Sigstore
class CLI < Thor
Expand Down Expand Up @@ -44,7 +39,7 @@ def initialize(*)
Sigstore.logger.level = options[:debug] ? Logger::DEBUG : Logger::INFO
end

package_name "sigstore-ruby"
package_name "sigstore-cli"

desc "verify FILE", "Verify a signature"
option :staging, type: :boolean, desc: "Use the staging trusted root"
Expand All @@ -59,10 +54,6 @@ def initialize(*)
exclusive :bundle, :signature
exclusive :bundle, :certificate
def verify(*files)
require "sigstore/verifier"
require "sigstore/models"
require "sigstore/policy"

verifier, files_with_materials = collect_verification_state(files)
policy = Sigstore::Policy::Identity.new(
identity: options[:certificate_identity],
Expand All @@ -87,14 +78,18 @@ def verify(*files)

desc "sign ARTIFACT", "Sign a file"
option :staging, type: :boolean, desc: "Use the staging trusted root"
option :identity_token, type: :string, desc: "Identity token to use for signing", required: true
option :identity_token, type: :string, desc: "Identity token to use for signing"
option :bundle, type: :string, desc: "Path to write the signed bundle to"
option :signature, type: :string, desc: "Path to write the signature to"
option :certificate, type: :string, desc: "Path to the public certificate"
option :trusted_root, type: :string, desc: "Path to the trusted root"
option :update_trusted_root, type: :boolean, desc: "Update the trusted root", default: true
def sign(file)
require "sigstore/signer"
options[:identity_token] ||= IdToken.detect_credential
unless options[:identity_token]
raise Error::InvalidIdentityToken,
"Failed to detect an ambient identity_token, please provide one via --identity-token"
end

contents = File.binread(file)
bundle = Sigstore::Signer.new(
Expand All @@ -112,9 +107,6 @@ def sign(file)

desc "display", "Display sigstore bundle(s)"
def display(*files)
require "sigstore/models"
require "sigstore/internal/x509"

files.each do |file|
bundle_bytes = Gem.read_binary(file)
bundle = SBundle.new Bundle::V1::Bundle.decode_json(bundle_bytes, registry: Sigstore::REGISTRY)
Expand Down Expand Up @@ -147,7 +139,6 @@ def self.exit_on_failure?
option :cached, type: :boolean, desc: "Return cached targets only"
option :target_base_url, type: :string, desc: "Base URL for the targets"
def download_target(*targets)
require "sigstore/tuf"
trust_updater = Sigstore::TUF::TrustUpdater.new(
options[:metadata_url], false,
metadata_dir: options[:metadata_dir], targets_dir: options[:targets_dir],
Expand Down Expand Up @@ -179,7 +170,6 @@ def init(root)
option :metadata_url, type: :string, desc: "URL to the metadata", required: true
option :metadata_dir, type: :string, desc: "Directory to store the metadata", required: true
def refresh
require "sigstore/tuf"
Sigstore::TUF::TrustUpdater.new(
options[:metadata_url], false,
metadata_dir: options[:metadata_dir]
Expand Down Expand Up @@ -276,4 +266,4 @@ def collect_verification_state(files)
end
end

Sigstore::CLI.start if $PROGRAM_NAME == __FILE__
require "sigstore/cli/id_token"
89 changes: 89 additions & 0 deletions cli/lib/sigstore/cli/id_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

class Sigstore::CLI
class IdToken
include Sigstore::Loggable

class AmbientCredentialError < Sigstore::Error
end

def self.detect_credential
[
GitHub
# detect_gcp,
# detect_buildkite,
# detect_gitlab,
# detect_circleci
].each do |detector|
credential = detector.call("sigstore")
return credential if credential
end

logger.debug { "failed to find ambient OIDC credential" }

nil
end

def self.call(audience)
new(audience).call
end

def initialize(audience)
@audience = audience
end

def call
raise NotImplementedError, "#{self.class}#call"
end

class GitHub < IdToken
class PermissionCredentialError < Sigstore::Error
end

def call
logger.debug { "looking for OIDC credentials" }
unless ENV["GITHUB_ACTIONS"]
logger.debug { "environment doesn't look like a GH action; giving up" }
return
end

req_token = ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_TOKEN", nil)
unless req_token
raise PermissionCredentialError,
"missing or insufficient OIDC token permissions, " \
"the ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable was unset"
end

req_url = ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_URL", nil)
unless req_url
raise PermissionCredentialError,
"missing or insufficient OIDC token permissions, " \
"the ACTIONS_ID_TOKEN_REQUEST_URL environment variable was unset"
end
req_url = URI.parse(req_url)
req_url.query = "audience=#{URI.encode_uri_component(@audience)}"

logger.debug { "requesting OIDC token" }
resp = Net::HTTP.get_request(
req_url, { "Authorization" => "bearer #{req_token}" }
)

begin
resp.value
rescue Net::HTTPExceptions
raise AmbientCredentialError, "OIDC token request failed (code=#{resp.code}, body=#{resp.body})"
rescue Timeout::Error
raise AmbientCredentialError, "OIDC token request timed out"
end

begin
body = JSON.parse resp.body
rescue StandardError
raise AmbientCredentialError, "malformed or incomplete json"
else
body.fetch("value")
end
end
end
end
end
Loading

0 comments on commit b4baee4

Please sign in to comment.