Skip to content

Commit

Permalink
Remove re-implementation of sigstore proto types (#94)
Browse files Browse the repository at this point in the history
* Begin removing wrappers around sigstore proto types

Signed-off-by: Samuel Giddins <[email protected]>

* Remove verification materials

Signed-off-by: Samuel Giddins <[email protected]>

* Implement more signing verification

Signed-off-by: Samuel Giddins <[email protected]>

* Address 2 comments

Signed-off-by: Samuel Giddins <[email protected]>

* Relax SAN check

Signed-off-by: Samuel Giddins <[email protected]>

---------

Signed-off-by: Samuel Giddins <[email protected]>
  • Loading branch information
segiddins authored Sep 17, 2024
1 parent 63851ac commit 3f017dd
Show file tree
Hide file tree
Showing 20 changed files with 745 additions and 829 deletions.
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require "rubocop/rake_task"

RuboCop::RakeTask.new

task default: %i[test conformance conformance_staging conformance_tuf rubocop]
task default: %i[test conformance_staging conformance conformance_tuf rubocop]

require "openssl"
# Checks for https://github.com/ruby/openssl/pull/770
Expand Down
35 changes: 5 additions & 30 deletions lib/rubygems/commands/sigstore_sign_bundle_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,17 @@ def initialize
super("sigstore-sign", "Sign an artifact with sigstore",
rekor_url: Sigstore::Rekor::Client::DEFAULT_REKOR_URL)

add_option("--staging") do |_, options|
options[:trusted_root] = Sigstore::TrustedRoot.staging
end

add_option("--identity-token TOKEN", "id token") do |token, options|
options[:identity_token] = token
end

add_option("--bundle PATH") do |bundle, options|
options[:bundle] = bundle
end

add_option("--staging") do |_, options|
options[:trusted_root] = Sigstore::TrustedRoot.staging
end

add_option("--trusted-root ROOT", "path to the trusted root certificate") do |root, options|
options[:trusted_root] = Sigstore::TrustedRoot.from_file(root)
end

# add_option("--rekor-url URL", "URL of the Rekor server") do |url, options|
# options[:rekor_url] = url
# end

# add_option("--[no-]offline", "Do not fetch the latest timestamp from the Rekor server") do |offline, options|
# options[:offline] = offline
# end
end

def execute
Expand All @@ -62,24 +50,11 @@ def execute
options[:trusted_root] ||= Sigstore::TrustedRoot.production

contents = File.binread(options[:args].first)
sig, _, result = Sigstore::Signer.new(
bundle = Sigstore::Signer.new(
jwt: options[:identity_token],
trusted_root: options[:trusted_root]
).sign(contents)

bundle = Sigstore::Bundle::V1::Bundle.new
bundle.media_type = "application/vnd.dev.sigstore.bundle.v0.3+json"
bundle.verification_material = result
# for a 0.3 bundle
bundle.verification_material.certificate =
bundle.verification_material.x509_certificate_chain.certificates.first
bundle.message_signature = Sigstore::Common::V1::MessageSignature.new.tap do |ms|
ms.message_digest = Sigstore::Common::V1::HashOutput.new
ms.message_digest.algorithm = Sigstore::Common::V1::HashAlgorithm::SHA2_256
ms.message_digest.digest = OpenSSL::Digest("SHA256").digest(contents)
ms.signature = sig
end

File.binwrite(options[:bundle], bundle.to_json)
end
end
Expand Down
27 changes: 8 additions & 19 deletions lib/rubygems/commands/sigstore_sign_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def initialize
super("sigstore-sign", "Sign an artifact with sigstore",
rekor_url: Sigstore::Rekor::Client::DEFAULT_REKOR_URL)

add_option("--staging") do |_, options|
options[:trusted_root] = Sigstore::TrustedRoot.staging
end

add_option("--identity-token TOKEN", "id token") do |token, options|
options[:identity_token] = token
end
Expand All @@ -45,35 +49,20 @@ def initialize
"--certificate-chain option is not passed.") do |key, options|
options[:certificate] = key
end

add_option("--staging") do |_, options|
options[:trusted_root] = Sigstore::TrustedRoot.staging
end

add_option("--trusted-root ROOT", "path to the trusted root certificate") do |root, options|
options[:trusted_root] = Sigstore::TrustedRoot.from_file(root)
end

# add_option("--rekor-url URL", "URL of the Rekor server") do |url, options|
# options[:rekor_url] = url
# end

# add_option("--[no-]offline", "Do not fetch the latest timestamp from the Rekor server") do |offline, options|
# options[:offline] = offline
# end
end

def execute
raise Gem::CommandLineError, "must provide one artifact to sign" if options[:args].size != 1

options[:trusted_root] ||= Sigstore::TrustedRoot.production

sig, leaf, = Sigstore::Signer.new(
bundle = Sigstore::Signer.new(
jwt: options[:identity_token],
trusted_root: options[:trusted_root]
).sign(File.binread(options[:args].first))
File.binwrite(options[:signature], [sig].pack("m0"))
File.binwrite(options[:certificate], leaf.to_pem)

File.binwrite(options[:signature], [bundle.message_signature.signature].pack("m0"))
File.binwrite(options[:certificate], bundle.verification_material.certificate.raw_bytes)
end
end
end
Expand Down
66 changes: 20 additions & 46 deletions lib/rubygems/commands/sigstore_verify_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def initialize
require "sigstore"
require "sigstore/rekor/client"
require "sigstore/trusted_root"
require "sigstore/models"

super("sigstore-verify", "Display the contents of the installed gems",
rekor_url: Sigstore::Rekor::Client::DEFAULT_REKOR_URL)
Expand All @@ -43,10 +44,6 @@ def initialize
options[:certificate] = key
end

add_option("--rekor-url URL", "URL of the Rekor server") do |url, options|
options[:rekor_url] = url
end

add_option("--[no-]offline", "Do not fetch the latest timestamp from the Rekor server") do |offline, options|
options[:offline] = offline
end
Expand All @@ -63,8 +60,8 @@ def execute
issuer: options[:certificate_oidc_issuer]
)

verified = files_with_materials.all? do |file, materials|
result = verifier.verify(materials: materials, policy: policy)
verified = files_with_materials.all? do |file, input|
result = verifier.verify(input: input, policy: policy, offline: options[:offline])

if result.verified?
say "OK: #{file}"
Expand Down Expand Up @@ -97,25 +94,6 @@ def collect_verification_state
verifier = Sigstore::Verifier.staging(trust_root: options[:trusted_root])
elsif options[:rekor_url] == Sigstore::Rekor::Client::DEFAULT_REKOR_URL
verifier = Sigstore::Verifier.production(trust_root: options[:trusted_root])
else
unless options[:certificate_chain]
raise Gem::CommandLineError,
"Custom Rekor URL used without --certificate-chain"
end

cert_chain = load_pem_x509_certificates(File.read(options[:certificate_chain]))

# TODO: rekor_root_pubkey

verifier = Sigstore::Verifier.new(
rekor_client: Sigstore::Rekor::Client.new(
url: options[:rekor_url],
rekor_keyring: Sigstore::Internal::Keyring.new(keys: trust_root.rekor_keys),
ct_keyring: Sigstore::Internal::Keyring.new(keys: trusted_root.ctfe_keys)
),
fulcio_cert_chain: cert_chain,
timestamp_authorities: trusted_root.timestamp_authorities
)
end

all_materials = []
Expand Down Expand Up @@ -148,31 +126,27 @@ def collect_verification_state
end

input_map.each do |file, inputs|
rekor_entry = nil
# TODO: replace verification materials with Sigstore::Verification::V1::Input
materials = File.open(file, "rb") do |input|
if inputs[:bundle]
bundle_bytes = Gem.read_binary(inputs[:bundle])
bundle = Sigstore::Bundle::V1::Bundle.decode_json(bundle_bytes, registry: Sigstore::REGISTRY)

Sigstore::VerificationMaterials.from_bundle(input: input, bundle: bundle,
offline: options[:offline])
else
cert_pem = Gem.read_binary(inputs[:cert])
b64_sig = Gem.read_binary(inputs[:sig])
signature = b64_sig.unpack1("m")

Sigstore::VerificationMaterials.new(
input: input,
cert_pem: cert_pem,
signature: signature, rekor_entry: rekor_entry,
offline: options[:offline]
)
end
artifact = Sigstore::Verification::V1::Artifact.new
artifact.artifact = File.binread(file)

verification_input = Sigstore::Verification::V1::Input.new
verification_input.artifact = artifact

if inputs[:bundle]
bundle_bytes = Gem.read_binary(inputs[:bundle])
verification_input.bundle = Sigstore::Bundle::V1::Bundle.decode_json(bundle_bytes,
registry: Sigstore::REGISTRY)
else
cert_pem = Gem.read_binary(inputs[:cert])
b64_sig = Gem.read_binary(inputs[:sig])
signature = b64_sig.unpack1("m")

verification_input.bundle = Sigstore::SBundle.for_cert_bytes_and_signature(cert_pem, signature)
end

say "Verifying #{file}..."
all_materials << [file, materials]
all_materials << [file, Sigstore::VerificationInput.new(verification_input)]
end

[verifier, all_materials]
Expand Down
2 changes: 2 additions & 0 deletions lib/sigstore/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ class NoSignature < Error; end
class InvalidKey < Error; end

class Signing < Error; end
class InvalidIdentityToken < Error; end

class MissingRekorEntry < Error; end
class InvalidRekorEntry < Error; end
class FailedRekorLookup < Error; end
class FailedRekorPost < Error; end

class Unimplemented < Error; end

Expand Down
6 changes: 3 additions & 3 deletions lib/sigstore/internal/merkle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ def self.verify_merkle_inclusion(entry)
inclusion_proof = entry.inclusion_proof
raise MissingInclusionProofError, "Rekor entry has no inclusion proof" unless inclusion_proof

leaf_hash = hash_leaf(Util.base64_decode(entry.body))
leaf_hash = hash_leaf(entry.canonicalized_body)
verify_inclusion(inclusion_proof.log_index, inclusion_proof.tree_size,
inclusion_proof.hashes.map { |h| Util.hex_decode(h) },
Util.hex_decode(inclusion_proof.root_hash), leaf_hash)
inclusion_proof.hashes,
inclusion_proof.root_hash, leaf_hash)
end

def self.verify_inclusion(index, tree_size, proof, root, leaf_hash)
Expand Down
19 changes: 14 additions & 5 deletions lib/sigstore/internal/set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,24 @@
module Sigstore
module Internal
module SET
def self.verify_set(client:, entry:)
def self.verify_set(keyring:, entry:)
raise "invalid log entry: no inclusion promise" unless entry.inclusion_promise

signed_entry_timestamp = entry.inclusion_promise.unpack1("m0")
signed_entry_timestamp = entry.inclusion_promise.signed_entry_timestamp
log_id = Util.hex_encode(entry.log_id.key_id)

client.rekor_keyring.verify(
key_id: entry.log_id,
# https://www.rfc-editor.org/rfc/rfc8785
canonical_entry = ::JSON.dump({
body: [entry.canonicalized_body].pack("m0"),
integratedTime: entry.integrated_time,
logID: log_id,
logIndex: entry.log_index
})

keyring.verify(
key_id: log_id,
signature: signed_entry_timestamp,
data: entry.encode_canonical
data: canonical_entry
)
end
end
Expand Down
29 changes: 25 additions & 4 deletions lib/sigstore/internal/x509.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,17 @@ def tbs_certificate_der

def extension(cls)
openssl.extensions.each do |ext|
return cls.new(ext) if ext.oid == cls.oid || ext.oid == cls.oid.short_name
return cls.new(ext) if ext.oid == cls.oid || ext.oid == cls.oid.short_name || ext.oid == cls.oid.oid
end
nil
end

def_delegators :openssl, :version, :not_after, :not_before, :to_pem, :to_der,
:public_key, :to_text
:public_key, :to_text, :subject, :hash

def ==(other)
openssl == other.openssl
end

def leaf?
return false if ca?
Expand Down Expand Up @@ -154,9 +158,9 @@ def initialize(extension)
value = shift_value([OpenSSL::ASN1.decode(extension.to_der)], OpenSSL::ASN1::Sequence)
@oid = value.shift

unless @extension.is_a?(OpenSSL::X509::Extension) && @oid == self.class.oid
unless @extension.is_a?(OpenSSL::X509::Extension) && @oid.oid == self.class.oid.oid
raise ArgumentError,
"Invalid extension: #{@extension.inspect} is not a #{@oid.inspect}" \
"Invalid extension: #{@extension.inspect} is not a #{@oid.inspect} " \
"(#{self.class} / #{self.class.oid.inspect})"
end

Expand All @@ -168,6 +172,8 @@ def initialize(extension)
raise ArgumentError, "Invalid extension: extra fields left in #{self}: #{value}" unless value.empty?

parse_value(OpenSSL::ASN1.decode(contents))
rescue OpenSSL::ASN1::ASN1Error => e
raise ArgumentError, "Invalid extension: #{e.message} for #{self.class.oid}\n#{extension.inspect}"
end

def critical?
Expand Down Expand Up @@ -428,6 +434,21 @@ def unpack_sct_list(string)
list
end
end

class FulcioIssuer < Extension
self.oid = OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.57264.1.8")

attr_reader :issuer

def parse_value(value)
unless value.is_a?(OpenSSL::ASN1::UTF8String)
raise ArgumentError,
"Invalid Fulcio issuer: #{value.inspect}"
end

@issuer = value.value
end
end
end
end
end
Expand Down
Loading

0 comments on commit 3f017dd

Please sign in to comment.