Skip to content

Commit

Permalink
start on CT verification
Browse files Browse the repository at this point in the history
  • Loading branch information
segiddins committed Mar 24, 2024
1 parent 31c15e0 commit 7b7ed49
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 29 deletions.
97 changes: 82 additions & 15 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ require "rubocop/rake_task"

RuboCop::RakeTask.new

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

desc "Run the conformance tests"
task conformance: %w[conformance:setup] do
sh({ "GHA_SIGSTORE_CONFORMANCE_XFAIL" =>
"test_verify_trust_root_with_invalid_ct_keys test_verify_dsse_bundle_with_trust_root" },
"test_verify_dsse_bundle_with_trust_root" },
File.expand_path("test/sigstore-conformance/env/bin/pytest"), "test",
"--entrypoint=#{File.join(__dir__, "bin", "conformance-entrypoint")}", "--skip-signing",
chdir: "test/sigstore-conformance")
Expand All @@ -26,29 +26,96 @@ end
desc "Run the conformance tests against staging"
task conformance_staging: %w[conformance:setup] do
sh({ "GHA_SIGSTORE_CONFORMANCE_XFAIL" =>
"test_verify_trust_root_with_invalid_ct_keys test_verify_dsse_bundle_with_trust_root" },
"test_verify_dsse_bundle_with_trust_root" },
File.expand_path("test/sigstore-conformance/env/bin/pytest"), "test",
"--entrypoint=#{File.join(__dir__, "bin", "conformance-entrypoint")}", "--skip-signing",
"--staging",
chdir: "test/sigstore-conformance")
end

desc "Run the TUF conformance tests"
task conformance_tuf: %w[tuf_conformance:setup] do
sh("test/tuf-conformance/env/bin/tuf-conformance", "bin/tuf-conformance-entrypoint")
end

namespace :conformance do
file "test/sigstore-conformance/.git/config" do
rm_rf "test/sigstore-conformance"
sh "git", "clone", "https://github.com/sigstore/sigstore-conformance", chdir: "test"
file "test/sigstore-conformance/env/pyvenv.cfg" => :sigstore_conformance do
sh "make", "dev", chdir: "test/sigstore-conformance"
end
file "test/sigstore-conformance/.git/HEAD" => %w[test/sigstore-conformance/.git/config] do
sh "git", "checkout", "main", chdir: "test/sigstore-conformance"
task setup: "test/sigstore-conformance/env/pyvenv.cfg" # rubocop:disable Rake/Desc
end

task test: %w[sigstore_conformance]

require "open3"

class GitRepo < Rake::Task
attr_accessor :path, :url, :commit

include FileUtils

def initialize(*)
super

@actions << method(:clone_repo)
@actions << method(:checkout)
end
file "test/sigstore-conformance/.git/rake-version" => %w[test/sigstore-conformance/.git/HEAD] do
sh "git", "describe", "--tags", "--always", chdir: "test/sigstore-conformance",
out: "test/sigstore-conformance/.git/rake-version"

def needed?
!correct_remote? || !correct_commit?
end
file "test/sigstore-conformance/env/pyvenv.cfg" => "test/sigstore-conformance/.git/rake-version" do
sh "make", "dev", chdir: "test/sigstore-conformance"

def correct_remote?
return false unless File.directory?(@path)

out, status = Open3.capture2(*%w[git remote get-url origin], chdir: path)
status.success? && out.strip == url
end
task setup: "test/sigstore-conformance/env/pyvenv.cfg" # rubocop:disable Rake/Desc

def correct_commit?
head, status = Open3.capture2(*%w[git rev-parse HEAD], chdir: path)
head.strip!
return true if status.success? && head == commit

desired, status = Open3.capture2(*%w[git rev-parse], "#{commit}^{commit}", chdir: path)
desired.strip!
status.success? && desired == head
end

def clone_repo(_, _)
return if correct_remote?

rm_rf path
sh "git", "clone", url, path
end

def checkout(_, _)
return if correct_commit?

sh "git", "-C", path, "switch", "--detach", commit do |ok, _|
unless ok
sh "git", "-C", path, "fetch", "origin", commit
sh "git", "-C", path, "switch", "--detach", commit
end
end
end
end

GitRepo.define_task(:sigstore_conformance).tap do |task|
task.path = "test/sigstore-conformance"
task.url = "https://github.com/sigstore/sigstore-conformance.git"
task.commit = "0a0196b"
end

task test: %w[test/sigstore-conformance/.git/rake-version]
GitRepo.define_task(:tuf_conformance).tap do |task|
task.path = "test/tuf-conformance"
task.url = "https://github.com/jku/tuf-conformance.git"
task.commit = "3072fdb346ce27210e5125b30c6626a9f6b34fc0"
end

namespace :tuf_conformance do
file "test/tuf-conformance/env/pyvenv.cfg" => :tuf_conformance do
sh "make", "dev", chdir: "test/tuf-conformance"
end
task setup: "test/tuf-conformance/env/pyvenv.cfg" # rubocop:disable Rake/Desc
end
4 changes: 4 additions & 0 deletions lib/rubygems/commands/sigstore_verify_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def initialize
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 Down
17 changes: 10 additions & 7 deletions lib/sigstore/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@ def self.from_media_type(media_type)
keyword_init: true) do
# @implements VerificationMaterials

def initialize(input:, cert_pem:, offline: false, **kwargs)
def initialize(input:, cert_pem:, **kwargs)
input_bytes = input.read
digest = OpenSSL::Digest.new("SHA256")
digest.update(input_bytes)
hashed_input = digest
certificate = OpenSSL::X509::Certificate.new(cert_pem)

raise ArgumentError, "offline verification requires a rekor entry" if offline && !rekor_entry

super(hashed_input: hashed_input, certificate: certificate, input_bytes: input_bytes, offline: offline, **kwargs)

raise ArgumentError, "offline verification requires a rekor entry" if offline && !rekor_entry?
end

def rekor_entry?
Expand Down Expand Up @@ -124,9 +124,10 @@ def find_rekor_entry(rekor_client)
def self.from_bundle(input:, bundle:, offline:)
media_type = BundleType.from_media_type(bundle.media_type)

if media_type == BundleType::BUNDLE_0_3
case media_type
when BundleType::BUNDLE_0_3
leaf_cert = OpenSSL::X509::Certificate.new(bundle.verification_material.certificate.raw_bytes)
else
when BundleType::BUNDLE_0_1, BundleType::BUNDLE_0_2
certs = bundle.verification_material.x509_certificate_chain.certificates.map do |cert|
OpenSSL::X509::Certificate.new(cert.raw_bytes)
end
Expand All @@ -138,9 +139,11 @@ def self.from_bundle(input:, bundle:, offline:)
certs.each do |cert|
raise "Root CA in chain" if cert_is_root_ca?(cert)
end
else
raise "Unsupported bundle format: #{media_type}"
end

raise "DSSE not yet supported" if bundle.dsse_envelope
raise "DSSE envelope verification not yet supported" if bundle.dsse_envelope
raise "bundle missing message signature" unless bundle.message_signature

signature = bundle.message_signature.signature
Expand All @@ -151,7 +154,7 @@ def self.from_bundle(input:, bundle:, offline:)
tlog_entry = tlog_entries.first

if media_type == BundleType::BUNDLE_0_1
raise unless tlog_entry.inclusion_promise
raise "bundle v0.1 requires an inclusion promise" unless tlog_entry.inclusion_promise
if tlog_entry.inclusion_proof && !tlog_entry.inclusion_proof.checkpoint.envelope
raise "0.1 bundle contains an inclusion proof without checkpoint"
end
Expand Down
6 changes: 2 additions & 4 deletions lib/sigstore/trusted_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ def self.from_file(path)

def rekor_keys
keys = tlog_keys(tlogs).to_a
raise "Did not find one active Rekor key" if keys.size != 1
raise "Did not find one Rekor key" if keys.size != 1

keys
end

def ctfe_keys
keys = tlog_keys(ctlogs).to_a
raise "Did not find one active CT key" if keys.size != 1
raise "Did not find any CTFE keys" if keys.empty?

keys
end
Expand All @@ -51,8 +51,6 @@ def tlog_keys(tlogs)
return enum_for(__method__, tlogs) unless block_given?

tlogs.each do |key|
next unless timerange_valid?(key.public_key.valid_for, allow_expired: false)

key_bytes = key.public_key.raw_bytes
yield key_bytes if key_bytes
end
Expand Down
53 changes: 51 additions & 2 deletions lib/sigstore/verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Sigstore
class Verifier
def initialize(rekor_client:, fulcio_cert_chain:)
@rekor_client = rekor_client
@fulcio_cert_chain = fulcio_cert_chain
@fulcio_cert_chain = fulcio_cert_chain.map { |cert| OpenSSL::X509::Certificate.new(cert) }
end

def self.production(trust_root: TrustedRoot.production)
Expand All @@ -33,7 +33,22 @@ def verify(materials:, policy:)

store_ctx = OpenSSL::X509::StoreContext.new(store, cert_ossl)

store_ctx.verify
unless store_ctx.verify
return VerificationFailure.new(
"failed to validate certification from fulcio cert chain: #{store_ctx.error_string}"
)
end

chain = store_ctx.chain || raise
chain.drop(1)

_sct = precertificate_signed_certificate_timestamps(materials.certificate)[0]
# verify_sct(
# sct,
# materials.certificate,
# chain,
# @rekor_client._ct_keyring
# )

usage_ext = materials.certificate.find_extension("keyUsage")
unless usage_ext.value == "Digital Signature"
Expand Down Expand Up @@ -77,5 +92,39 @@ def verify(materials:, policy:)

VerificationSuccess.new
end

private

def precertificate_signed_certificate_timestamps(certificate)
# this is cursed. can't always find_extension(oid) because #oid can return a string or an OID
oid = OpenSSL::X509::Extension.new("1.3.6.1.4.1.11129.2.4.2", "").oid
precert_scts_extension = certificate.find_extension(oid)
unless precert_scts_extension
raise "No PrecertificateSignedCertificateTimestamps (#{oid.inspect}) found for the certificate #{certificate.extensions.join("\n")}"
end

# TODO: parse the extension properly
# https://github.com/pierky/sct-verify/blob/master/sct-verify.py

os1 = OpenSSL::ASN1.decode(precert_scts_extension.value_der)

len = os1.value.unpack1("n")
string = os1.value[2..]
raise "os1: len=#{len} #{os1.value.inspect}" unless string && string.size == len

len = string.unpack1("n")
string = string[2..]
raise "os1: len=#{len} #{string.inspect}" unless string && string.size == len

sct_version, sct_log_id, sct_timestamp, sct_extensions_len, sct_signature_alg_hash,
sct_signature_alg_sign, sct_signature_len, sct_signature_bytes = string.unpack("Ca32QnCCna*")
raise "sct extensions not supported" unless sct_extensions_len.zero?
unless sct_signature_bytes.bytesize == sct_signature_len
raise "sct_signature_bytes: #{sct_signature_bytes.inspect} sct_signature_len: #{sct_signature_len}"
end

# TODO: parse the SCT properly
[nil]
end
end
end
1 change: 1 addition & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/tuf-conformance/
/sigstore-conformance/
/conformance_invocations
5 changes: 4 additions & 1 deletion test/sigstore/trusted_root_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ def test_production
assert_equal ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9f\n" \
"AFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RY\n" \
"tw==\n"], production.rekor_keys.map { [_1].pack("m") }
assert_equal ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEc\n" \
assert_equal ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy\n" \
"3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU\n" \
"7w==\n",
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEc\n" \
"YXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9F\n" \
"yw==\n"], production.ctfe_keys.map { [_1].pack("m") }
assert_equal "-----BEGIN CERTIFICATE-----\n" \
Expand Down

0 comments on commit 7b7ed49

Please sign in to comment.