Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract cli into a separate gem we can publish #174

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
self.options = options.merge(identity_token: IdToken.detect_credential).freeze if options[:identity_token].nil?
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_response(
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
Loading