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 +7 -0
- data/exe/sigstore-cli +5 -0
- data/lib/sigstore/cli/id_token.rb +89 -0
- data/lib/sigstore/cli.rb +269 -0
- metadata +80 -0
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,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
|
data/lib/sigstore/cli.rb
ADDED
@@ -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: []
|