diff --git a/lib/sigstore/verifier.rb b/lib/sigstore/verifier.rb index c32f6ea..701c920 100644 --- a/lib/sigstore/verifier.rb +++ b/lib/sigstore/verifier.rb @@ -173,7 +173,9 @@ def verify(input:, policy:, offline:) rescue JSON::ParserError raise Error::InvalidBundle, "invalid JSON for in-toto statement in DSSE payload" end - verify_in_toto(input, in_toto) + if (result = verify_in_toto(input, in_toto)) + return result + end else raise Sigstore::Error::Unimplemented, "unsupported DSSE payload type: #{bundle.dsse_envelope.payloadType.inspect}" @@ -216,25 +218,27 @@ def verify_dsse(dsse_envelope, public_key) end def verify_in_toto(input, in_toto_payload) - type = in_toto_payload.fetch("_type") + type = in_toto_payload["_type"] raise Error::InvalidBundle, "Expected in-toto statement, got #{type.inspect}" unless type == "https://in-toto.io/Statement/v1" - subject = in_toto_payload.fetch("subject") - raise Error::InvalidBundle, "Expected in-toto statement with subject" unless subject && subject.size == 1 - - subject = subject.first - digest = subject.fetch("digest") - raise Error::InvalidBundle, "Expected in-toto statement with digest" if !digest || digest.empty? + subjects = in_toto_payload["subject"] + raise Error::InvalidBundle, "Expected in-toto statement with subject" if !subjects || subjects.empty? + expected_algorithm = Internal::Util.hash_algorithm_name(input.hashed_input.algorithm) expected_hexdigest = Internal::Util.hex_encode(input.hashed_input.digest) - digest.each do |name, value| - next if expected_hexdigest == value - return VerificationFailure.new( - "in-toto subject does not match for #{input.hashed_input.algorithm} of #{subject.fetch("name")}: " \ - "expected #{name} to be #{value}, got #{expected_hexdigest}" + matched = subjects.map do |subject| + digest = subject["digest"] + raise Error::InvalidBundle, "Expected in-toto statement with digest" if !digest || digest.empty? + + digest[expected_algorithm] == expected_hexdigest + end.any? + + return if matched + + VerificationFailure.new( + "None of in-toto subjects matches artifact for #{expected_algorithm}: #{expected_hexdigest}" ) - end end public diff --git a/test/sigstore/verifier_test.rb b/test/sigstore/verifier_test.rb index 5a26a9b..151861f 100644 --- a/test/sigstore/verifier_test.rb +++ b/test/sigstore/verifier_test.rb @@ -4,6 +4,125 @@ require "sigstore/verifier" class Sigstore::VerifierTest < Test::Unit::TestCase + HEXDIGEST256 = "01234567" * 8 + OTHER_HEXDIGEST256 = "0" * 64 + + HashedInput = Struct.new(:hashed_input) + + def make_input(hexdigest) + digest = [hexdigest].pack("H*") + hashed_input = Sigstore::Common::V1::HashOutput.new + hashed_input.algorithm = Sigstore::Common::V1::HashAlgorithm::SHA2_256 + hashed_input.digest = digest + HashedInput.new(hashed_input) + end + + def make_payload(subjects) + { + "_type" => "https://in-toto.io/Statement/v1", + "subject" => subjects, + "predicateType" => "https://slsa.dev/provenance/v1", + "predicate" => {} + } + end + + def test_verify_in_toto_single_subject_matches + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([ + { "name" => "artifact.txt", "digest" => { "sha256" => HEXDIGEST256 } } + ]) + assert_nil verifier.send(:verify_in_toto, input, payload) + end + + def test_verify_in_toto_multiple_subjects_first_matches + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([ + { "name" => "artifact.txt", "digest" => { "sha256" => HEXDIGEST256 } }, + { "name" => "other.txt", "digest" => { "sha256" => OTHER_HEXDIGEST256 } } + ]) + assert_nil verifier.send(:verify_in_toto, input, payload) + end + + def test_verify_in_toto_multiple_subjects_second_matches + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([ + { "name" => "other.txt", "digest" => { "sha256" => OTHER_HEXDIGEST256 } }, + { "name" => "artifact.txt", "digest" => { "sha256" => HEXDIGEST256 } } + ]) + assert_nil verifier.send(:verify_in_toto, input, payload) + end + + def test_verify_in_toto_no_subject_matches + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([ + { "name" => "other.txt", "digest" => { "sha256" => OTHER_HEXDIGEST256 } } + ]) + result = verifier.send(:verify_in_toto, input, payload) + assert_kind_of Sigstore::VerificationFailure, result + end + + def test_verify_in_toto_wrong_algorithm_does_not_match + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([ + { "name" => "artifact.txt", "digest" => { "sha512_256" => HEXDIGEST256 } } + ]) + result = verifier.send(:verify_in_toto, input, payload) + assert_kind_of Sigstore::VerificationFailure, result + end + + def test_verify_in_toto_no_subjects_raises + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload(nil) + assert_raise Sigstore::Error::InvalidBundle do + verifier.send(:verify_in_toto, input, payload) + end + end + + def test_verify_in_toto_empty_subjects_raises + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([]) + assert_raise Sigstore::Error::InvalidBundle do + verifier.send(:verify_in_toto, input, payload) + end + end + + def test_verify_in_toto_no_digest_raises + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([{ "name" => "artifact.txt" }]) + assert_raise Sigstore::Error::InvalidBundle do + verifier.send(:verify_in_toto, input, payload) + end + end + + def test_verify_in_toto_empty_digest_raises + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([{ "name" => "artifact.txt", "digest" => {} }]) + assert_raise Sigstore::Error::InvalidBundle do + verifier.send(:verify_in_toto, input, payload) + end + end + + def test_verify_in_toto_wrong_type_raises + verifier = Sigstore::Verifier.allocate + input = make_input(HEXDIGEST256) + payload = make_payload([ + { "name" => "artifact.txt", "digest" => { "sha256" => HEXDIGEST256 } } + ]) + payload["_type"] = "https://in-toto.io/Statement/v0.1" + assert_raise Sigstore::Error::InvalidBundle do + verifier.send(:verify_in_toto, input, payload) + end + end + def test_pack_digitally_signed_precertificate verifier = Sigstore::Verifier.allocate [3, 255, 1024, 16_777_215].each do |precert_bytes_len|