sigstore-cli 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2bc6c2be40d13591ca8500483b3e698764d3fc29e8af44ea3292ab498faeea5a
4
+ data.tar.gz: ab212f10f5132aab74e2534834ea78cd3602a1bc95a74f240b56e87c493af19f
5
+ SHA512:
6
+ metadata.gz: 1c09fb5334d58c661c710d4a815e3c082c453e84834cf88dcf5e1c510d11b44b8728946416a1d62becfa4d4af2bb50280f5baf28a6c5692a9c2e1ed4cf18a9f0
7
+ data.tar.gz: f633f7c2d4e8319db67aed71cdda3d7cfc14fa8651d78ff253a1076da60d6749450eab23c6613f1014a44c84ad31a9a9d7e2fe949bbbddaf4351a6b07a355712
data/exe/sigstore-cli ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "sigstore/cli"
5
+ Sigstore::CLI.start
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Sigstore::CLI
4
+ class IdToken
5
+ include Sigstore::Loggable
6
+
7
+ class AmbientCredentialError < Sigstore::Error
8
+ end
9
+
10
+ def self.detect_credential
11
+ [
12
+ GitHub
13
+ # detect_gcp,
14
+ # detect_buildkite,
15
+ # detect_gitlab,
16
+ # detect_circleci
17
+ ].each do |detector|
18
+ credential = detector.call("sigstore")
19
+ return credential if credential
20
+ end
21
+
22
+ logger.debug { "failed to find ambient OIDC credential" }
23
+
24
+ nil
25
+ end
26
+
27
+ def self.call(audience)
28
+ new(audience).call
29
+ end
30
+
31
+ def initialize(audience)
32
+ @audience = audience
33
+ end
34
+
35
+ def call
36
+ raise NotImplementedError, "#{self.class}#call"
37
+ end
38
+
39
+ class GitHub < IdToken
40
+ class PermissionCredentialError < Sigstore::Error
41
+ end
42
+
43
+ def call
44
+ logger.debug { "looking for OIDC credentials" }
45
+ unless ENV["GITHUB_ACTIONS"]
46
+ logger.debug { "environment doesn't look like a GH action; giving up" }
47
+ return
48
+ end
49
+
50
+ req_token = ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_TOKEN", nil)
51
+ unless req_token
52
+ raise PermissionCredentialError,
53
+ "missing or insufficient OIDC token permissions, " \
54
+ "the ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable was unset"
55
+ end
56
+
57
+ req_url = ENV.fetch("ACTIONS_ID_TOKEN_REQUEST_URL", nil)
58
+ unless req_url
59
+ raise PermissionCredentialError,
60
+ "missing or insufficient OIDC token permissions, " \
61
+ "the ACTIONS_ID_TOKEN_REQUEST_URL environment variable was unset"
62
+ end
63
+ req_url = URI.parse(req_url)
64
+ req_url.query = "audience=#{URI.encode_uri_component(@audience)}"
65
+
66
+ logger.debug { "requesting OIDC token" }
67
+ resp = Net::HTTP.get_response(
68
+ req_url, { "Authorization" => "bearer #{req_token}" }
69
+ )
70
+
71
+ begin
72
+ resp.value
73
+ rescue Net::HTTPExceptions
74
+ raise AmbientCredentialError, "OIDC token request failed (code=#{resp.code}, body=#{resp.body})"
75
+ rescue Timeout::Error
76
+ raise AmbientCredentialError, "OIDC token request timed out"
77
+ end
78
+
79
+ begin
80
+ body = JSON.parse resp.body
81
+ rescue StandardError
82
+ raise AmbientCredentialError, "malformed or incomplete json"
83
+ else
84
+ body.fetch("value")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "sigstore"
5
+
6
+ module Sigstore
7
+ class CLI < Thor
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ def self.start(given_args = ARGV, config = {})
13
+ super
14
+ rescue Sigstore::Error => e
15
+ raise if config[:debug] || ENV["THOR_DEBUG"] == "1"
16
+
17
+ config[:shell].error(e.detailed_message)
18
+
19
+ exit(false)
20
+ end
21
+
22
+ class ShellWrapper
23
+ def initialize(shell)
24
+ @shell = shell
25
+ end
26
+
27
+ def close
28
+ @shell.close
29
+ end
30
+
31
+ def write(...)
32
+ @shell.say(...)
33
+ end
34
+ end
35
+
36
+ def initialize(*)
37
+ super
38
+ Sigstore.logger.reopen ShellWrapper.new(shell)
39
+ Sigstore.logger.level = options[:debug] ? Logger::DEBUG : Logger::INFO
40
+ end
41
+
42
+ package_name "sigstore-cli"
43
+
44
+ desc "verify FILE", "Verify a signature"
45
+ option :staging, type: :boolean, desc: "Use the staging trusted root"
46
+ option :signature, type: :string, desc: "Path to the signature file"
47
+ option :certificate, type: :string, desc: "Path to the public certificate"
48
+ option :certificate_identity, type: :string, desc: "The identity of the certificate"
49
+ option :certificate_oidc_issuer, type: :string, desc: "The OIDC issuer of the certificate"
50
+ option :offline, type: :boolean, desc: "Do not fetch the latest timestamp from the Rekor server"
51
+ option :bundle, type: :string, desc: "Path to the signed bundle"
52
+ option :trusted_root, type: :string, desc: "Path to the trusted root"
53
+ option :update_trusted_root, type: :boolean, desc: "Update the trusted root", default: true
54
+ exclusive :bundle, :signature
55
+ exclusive :bundle, :certificate
56
+ def verify(*files)
57
+ verifier, files_with_materials = collect_verification_state(files)
58
+ policy = Sigstore::Policy::Identity.new(
59
+ identity: options[:certificate_identity],
60
+ issuer: options[:certificate_oidc_issuer]
61
+ )
62
+
63
+ verified = files_with_materials.all? do |file, input|
64
+ result = verifier.verify(input:, policy:, offline: options[:offline])
65
+
66
+ if result.verified?
67
+ say "OK: #{file}"
68
+ true
69
+ else
70
+ say "FAIL: #{file}"
71
+ say "\t#{result.reason}"
72
+ false
73
+ end
74
+ end
75
+ exit(false) unless verified
76
+ end
77
+ map "verify-bundle" => :verify
78
+
79
+ desc "sign ARTIFACT", "Sign a file"
80
+ option :staging, type: :boolean, desc: "Use the staging trusted root"
81
+ option :identity_token, type: :string, desc: "Identity token to use for signing"
82
+ option :bundle, type: :string, desc: "Path to write the signed bundle to"
83
+ option :signature, type: :string, desc: "Path to write the signature to"
84
+ option :certificate, type: :string, desc: "Path to the public certificate"
85
+ option :trusted_root, type: :string, desc: "Path to the trusted root"
86
+ option :update_trusted_root, type: :boolean, desc: "Update the trusted root", default: true
87
+ def sign(file)
88
+ self.options = options.merge(identity_token: IdToken.detect_credential).freeze if options[:identity_token].nil?
89
+ unless options[:identity_token]
90
+ raise Error::InvalidIdentityToken,
91
+ "Failed to detect an ambient identity token, please provide one via --identity-token"
92
+ end
93
+
94
+ contents = File.binread(file)
95
+ bundle = Sigstore::Signer.new(
96
+ jwt: options[:identity_token],
97
+ trusted_root:
98
+ ).sign(contents)
99
+
100
+ File.binwrite(options[:bundle], bundle.to_json) if options[:bundle]
101
+ if options[:signature]
102
+ File.binwrite(options[:signature], Internal::Util.base64_encode(bundle.message_signature.signature))
103
+ end
104
+ File.binwrite(options[:certificate], bundle.verification_material.certificate.raw_bytes) if options[:certificate]
105
+ end
106
+ map "sign-bundle" => :sign
107
+
108
+ desc "display", "Display sigstore bundle(s)"
109
+ def display(*files)
110
+ files.each do |file|
111
+ bundle_bytes = Gem.read_binary(file)
112
+ bundle = SBundle.new Bundle::V1::Bundle.decode_json(bundle_bytes, registry: Sigstore::REGISTRY)
113
+
114
+ say "--- Bundle #{file} ---"
115
+ say "Media Type: #{bundle.media_type}"
116
+ say bundle.leaf_certificate.to_text
117
+
118
+ case bundle.content
119
+ when :message_signature
120
+ say "Signature over: #{bundle.message_signature.message_digest.algorithm} " \
121
+ "#{Internal::Util.hex_encode bundle.message_signature.message_digest.digest}"
122
+ when :dsse_envelope
123
+ say bundle.dsse_envelope.payloadType
124
+ say bundle.dsse_envelope.payload
125
+ else raise Error::InvalidBundle, "expected either message_signature or dsse_envelope"
126
+ end
127
+ end
128
+ end
129
+
130
+ class TUF < Thor
131
+ def self.exit_on_failure?
132
+ true
133
+ end
134
+
135
+ desc "download-target TARGET...", "Download a target from a TUP repo"
136
+ option :metadata_url, type: :string, desc: "URL to the metadata", required: true
137
+ option :metadata_dir, type: :string, desc: "Directory to store the metadata", required: true
138
+ option :targets_dir, type: :string, desc: "Directory to store the targets", required: true
139
+ option :cached, type: :boolean, desc: "Return cached targets only"
140
+ option :target_base_url, type: :string, desc: "Base URL for the targets"
141
+ def download_target(*targets)
142
+ trust_updater = Sigstore::TUF::TrustUpdater.new(
143
+ options[:metadata_url], false,
144
+ metadata_dir: options[:metadata_dir], targets_dir: options[:targets_dir],
145
+ target_base_url: options[:target_base_url]
146
+ )
147
+ trust_updater.refresh
148
+
149
+ targets.each do |target|
150
+ target_info = trust_updater.updater.get_targetinfo(target)
151
+ raise Sigstore::TUF::Error, "No such target: #{target}" unless target_info
152
+
153
+ path = if @cached
154
+ trust_updater.updater.find_cached_target(target_info)
155
+ else
156
+ trust_updater.updater.download_target(target_info)
157
+ end
158
+ say "Downloaded #{target} to #{path}"
159
+ end
160
+ end
161
+
162
+ desc "init ROOT", "Initialize a TUF repo"
163
+ option :metadata_dir, type: :string, desc: "Directory to store the metadata", required: true
164
+ def init(root)
165
+ FileUtils.mkdir_p(options[:metadata_dir])
166
+ FileUtils.cp(root, File.join(options[:metadata_dir], "root.json"))
167
+ end
168
+
169
+ desc "refresh", "Refresh the metadata"
170
+ option :metadata_url, type: :string, desc: "URL to the metadata", required: true
171
+ option :metadata_dir, type: :string, desc: "Directory to store the metadata", required: true
172
+ def refresh
173
+ Sigstore::TUF::TrustUpdater.new(
174
+ options[:metadata_url], false,
175
+ metadata_dir: options[:metadata_dir]
176
+ ).refresh
177
+ end
178
+ end
179
+
180
+ register TUF, "tuf", "tuf SUBCOMMAND", "TUF commands"
181
+
182
+ private
183
+
184
+ def trusted_root
185
+ return Sigstore::TrustedRoot.from_file(options[:trusted_root]) if options[:trusted_root]
186
+
187
+ if options[:staging]
188
+ Sigstore::TrustedRoot.staging(offline: !options[:update_trusted_root])
189
+ else
190
+ Sigstore::TrustedRoot.production(offline: !options[:update_trusted_root])
191
+ end
192
+ end
193
+
194
+ def collect_verification_state(files)
195
+ if (options[:certificate] || options[:signature] || options[:bundle]) && files.size > 1
196
+ raise Thor::InvocationError, "Too many files specified: #{files.inspect}"
197
+ end
198
+
199
+ if options[:bundle] && (options[:certificate] || options[:signature])
200
+ raise Thor::InvocationError, "Cannot specify both --bundle and --certificate or --signature"
201
+ end
202
+
203
+ input_map = {}
204
+
205
+ verifier = Sigstore::Verifier.for_trust_root(trust_root: trusted_root)
206
+
207
+ all_materials = []
208
+
209
+ files.each do |file|
210
+ raise Thor::InvocationError, "File not found: #{file}" unless File.exist?(file) || file.start_with?("sha256:")
211
+
212
+ sig = options[:signature]
213
+ cert = options[:certificate]
214
+ bundle = options[:bundle]
215
+
216
+ directory = File.dirname(file)
217
+
218
+ sig ||= File.join(directory, "#{file}.sig")
219
+ cert ||= File.join(directory, "#{file}.cert")
220
+ bundle = File.join(directory, "#{file}.sigstore.json") if bundle.nil?
221
+
222
+ missing = []
223
+
224
+ if options[:signature] || options[:certificate]
225
+ missing << sig unless File.exist?(sig)
226
+ missing << cert unless File.exist?(cert)
227
+ input_map[file] = { cert:, sig: }
228
+ else
229
+ missing << bundle unless File.exist?(bundle)
230
+ input_map[file] = { bundle: }
231
+ end
232
+
233
+ raise Thor::InvocationError, "Missing files: #{missing.join(", ")}" if missing.any?
234
+ end
235
+
236
+ input_map.each do |file, inputs|
237
+ artifact = Sigstore::Verification::V1::Artifact.new
238
+ case file
239
+ when /\Asha256:/
240
+ artifact.artifact_uri = file
241
+ else
242
+ artifact.artifact = File.binread(file)
243
+ end
244
+
245
+ verification_input = Sigstore::Verification::V1::Input.new
246
+ verification_input.artifact = artifact
247
+
248
+ if inputs[:bundle]
249
+ bundle_bytes = Gem.read_binary(inputs[:bundle])
250
+ verification_input.bundle = Sigstore::Bundle::V1::Bundle.decode_json(bundle_bytes,
251
+ registry: Sigstore::REGISTRY)
252
+ else
253
+ cert_pem = Gem.read_binary(inputs[:cert])
254
+ b64_sig = Gem.read_binary(inputs[:sig])
255
+ signature = b64_sig.unpack1("m")
256
+
257
+ verification_input.bundle = Sigstore::SBundle.for_cert_bytes_and_signature(cert_pem, signature).__getobj__
258
+ end
259
+
260
+ say "Verifying #{file}..."
261
+ all_materials << [file, Sigstore::VerificationInput.new(verification_input)]
262
+ end
263
+
264
+ [verifier, all_materials]
265
+ end
266
+ end
267
+ end
268
+
269
+ require "sigstore/cli/id_token"
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sigstore-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - The Sigstore Authors
8
+ - Samuel Giddins
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-11-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sigstore
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '='
19
+ - !ruby/object:Gem::Version
20
+ version: 0.2.1
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '='
26
+ - !ruby/object:Gem::Version
27
+ version: 0.2.1
28
+ - !ruby/object:Gem::Dependency
29
+ name: thor
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description:
43
+ email:
44
+ -
45
+ - segiddins@segiddins.me
46
+ executables:
47
+ - sigstore-cli
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - exe/sigstore-cli
52
+ - lib/sigstore/cli.rb
53
+ - lib/sigstore/cli/id_token.rb
54
+ homepage: https://github.com/sigstore/sigstore-ruby
55
+ licenses:
56
+ - Apache-2.0
57
+ metadata:
58
+ allowed_push_host: https://rubygems.org
59
+ homepage_uri: https://github.com/sigstore/sigstore-ruby
60
+ rubygems_mfa_required: 'true'
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 3.1.0
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 3.5.22
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: A CLI interface to the sigstore ruby client
80
+ test_files: []