diff --git a/lib/sigstore/internal/json.rb b/lib/sigstore/internal/json.rb index 914ba50..14369f6 100644 --- a/lib/sigstore/internal/json.rb +++ b/lib/sigstore/internal/json.rb @@ -17,6 +17,9 @@ module Sigstore::Internal module JSON # Implements https://wiki.laptop.org/go/Canonical_JSON + # + # TODO: This is a naive implementation. Performance can be improved by + # serializing into a buffer instead of concatenating strings. def self.canonical_generate(data) case data when NilClass @@ -34,11 +37,11 @@ def self.canonical_generate(data) "[#{contents}]" when Hash contents = data.sort_by do |k, _| + raise ArgumentError, "Non-string key in hash" unless k.is_a?(String) + k.encode("utf-16").codepoints end contents.map! do |k, v| - raise ArgumentError, "Non-string key in hash" unless k.is_a?(String) - "#{canonical_generate(k)}:#{canonical_generate(v)}" end "{#{contents.join(",")}}" diff --git a/lib/sigstore/tuf.rb b/lib/sigstore/tuf.rb index 8f78b50..53df24b 100644 --- a/lib/sigstore/tuf.rb +++ b/lib/sigstore/tuf.rb @@ -14,12 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -require_relative "tuf/config" -require_relative "tuf/trusted_metadata_set" -require_relative "tuf/root" -require_relative "tuf/snapshot" -require_relative "tuf/targets" -require_relative "tuf/timestamp" +require_relative "tuf/updater" require "tempfile" require "uri" require "net/http" @@ -100,9 +95,10 @@ def get_dirs(url) app_author = "segiddins" repo_base = encode_uri_component(url) + home = Dir.home - data_home = ENV.fetch("XDG_DATA_HOME", File.join(Dir.home, ".local", "share")) - cache_home = ENV.fetch("XDG_CACHE_HOME", File.join(Dir.home, ".cache")) + data_home = ENV.fetch("XDG_DATA_HOME", File.join(home, ".local", "share")) + cache_home = ENV.fetch("XDG_CACHE_HOME", File.join(home, ".cache")) tuf_data_dir = File.join(data_home, app_name, app_author, "tuf") tuf_cache_dir = File.join(cache_home, app_name, app_author, "tuf") @@ -132,236 +128,5 @@ def trusted_root_path path end end - - class Updater - include Loggable - - def initialize(metadata_dir:, metadata_base_url:, target_base_url:, target_dir:, fetcher:, - config: UpdaterConfig.new) - @dir = metadata_dir - @metadata_base_url = "#{metadata_base_url.to_s.chomp("/")}/" - @target_dir = target_dir - @target_base_url = target_base_url && "#{target_base_url.to_s.chomp("/")}/" - - @fetcher = fetcher - @config = config - - unless %i[metadata simple].include? @config.envelope_type - raise ArgumentError, "Unsupported envelope type: #{@config[:envelope_type].inspect}" - end - - begin - data = load_local_metadata("root") - @trusted_set = TrustedMetadataSet.new(data, "metadata") - rescue ::JSON::ParserError => e # JSON::ParseError - raise "Invalid JSON in #{File.join(@dir, "root.json")}: #{e.class} #{e}" - end - end - - def refresh - load_root - load_timestamp - load_snapshot - load_targets(Targets::TYPE, Root::TYPE) - end - - def get_targetinfo(target_path) - refresh unless @trusted_set.include? Targets::TYPE - preorder_depth_first_walk(target_path) - end - - def find_cached_target(target_info, filepath = nil) - filepath ||= generate_target_file_path(target_info) - - begin - data = File.binread(filepath) - target_info.verify_length_and_hashes(data) - filepath - rescue Errno::ENOENT, Error::LengthOrHashMismatch - nil - end - end - - def download_target(target_info, filepath = nil, target_base_url = nil) - target_base_url ||= @target_base_url - raise "No target_base_url set" unless target_base_url - - filepath ||= generate_target_file_path(target_info) - - target_filepath = target_info.path - consistent_snapshot = @trusted_set.root.consistent_snapshot - - if consistent_snapshot && @config.prefix_targets_with_hash - hashes = target_info.hashes.values - dir, sep, basename = target_filepath.rpartition("/") - target_filepath = "#{dir}#{sep}#{hashes.first}.#{basename}" - end - - full_url = URI.join(target_base_url, target_filepath) - - @fetcher.get2(full_url) do |resp| - resp.value - target_info.verify_length_and_hashes(resp.body) - - File.binwrite(filepath, resp.body) - rescue Net::HTTPClientException => e - raise "Failed to download target #{target_info.inspect} #{target_filepath.inspect} from #{full_url}: " \ - "#{e.message}" - end - logger.info { "Downloaded #{target_filepath} to #{filepath}" } - filepath - end - - private - - def load_local_metadata(role_name) - encoded_name = URI.encode_www_form_component(role_name) - - File.binread(File.join(@dir, "#{encoded_name}.json")) - end - - def load_root - lower_bound = @trusted_set.root.version + 1 - upper_bound = lower_bound + @config.max_root_rotations - - lower_bound.upto(upper_bound) do |version| - data = download_metadata("root", version) - @trusted_set.root = data - persist_metadata("root", data) - rescue Net::HTTPClientException => e - break if %w[403 404].include? e.response.code - - raise - end - end - - def load_timestamp - begin - data = load_local_metadata(Timestamp::TYPE) - rescue Errno::ENOENT => e - logger.debug "Local timestamp not valid as final: #{e.class} #{e.message}" - else - @trusted_set.timestamp = data - end - - data = download_metadata(Timestamp::TYPE, nil) - - begin - @trusted_set.timestamp = data - rescue EqualVersionNumberError - return - end - - persist_metadata(Timestamp::TYPE, data) - end - - def load_snapshot - data = load_local_metadata(Snapshot::TYPE) - @trusted_set.snapshot = data - logger.debug "Loaded snapshot from local metadata" - rescue Errno::ENOENT => e - logger.debug "Local snapshot not valid as final: #{e.class} #{e.message}" - - snapshot_meta = @trusted_set.timestamp.snapshot_meta - version = snapshot_meta.version if @trusted_set.root.consistent_snapshot - - data = download_metadata(Snapshot::TYPE, version) - @trusted_set.snapshot = data - persist_metadata(Snapshot::TYPE, data) - end - - def load_targets(role, parent_role) - return @trusted_set[role] if @trusted_set.include?(role) - - begin - data = load_local_metadata(role) - @trusted_set.update_delegated_targets(data, role, parent_role).tap do - logger.debug { "Loaded targets for #{role} from local metadata" } - end - rescue Errno::ENOENT - logger.debug { "No local targets for #{role}, fetching" } - - snapshot = @trusted_set.snapshot - metainfo = snapshot.meta.fetch("#{role}.json") - raise "No metadata for role: #{role}" unless metainfo - - version = metainfo.version if @trusted_set.root.consistent_snapshot - data = download_metadata(role, version) - delegated_targets = @trusted_set.update_delegated_targets(data, role, parent_role) - persist_metadata(role, data) - delegated_targets - end - end - - def download_metadata(role_name, version) - encoded_name = URI.encode_www_form_component(role_name) - url = if version.nil? - URI.join(@metadata_base_url, "#{encoded_name}.json") - else - URI.join(@metadata_base_url, "#{version}.#{encoded_name}.json") - end - - resp = @fetcher.get(url) - resp.value - - resp.body - end - - def persist_metadata(role_name, data) - encoded_name = URI.encode_www_form_component(role_name) - filename = File.join(@dir, "#{encoded_name}.json") - Tempfile.create("", @dir) do |f| - f.binmode - f.write(data) - f.close - - File.rename(f.path, filename) - end - end - - def preorder_depth_first_walk(target_path) - # TODO - delegations_to_visit = [[Targets::TYPE, Root::TYPE]] - visited_role_names = Set.new - - while delegations_to_visit.any? && visited_role_names.size < @config.max_delegations - role_name, parent_role = delegations_to_visit.shift - next if visited_role_names.include?(role_name) - - targets = load_targets(role_name, parent_role) - target = targets.targets.fetch(target_path, nil) - - return target if target - - visited_role_names.add(role_name) - - next unless targets.delegations.any? - - child_roles_to_visit = [] - - targets.delegations.roles_for_target(target_path).each do |child_name, terminating| - child_roles_to_visit << [child_name, role_name] - next unless terminating - - logger.debug { "Terminating delegation found for #{child_name}" } - delegations_to_visit.clear - break - end - - delegations_to_visit.concat child_roles_to_visit.reverse - end - - logger.warn { "Max delegations reached, stopping search" } if delegations_to_visit.any? - - nil - end - - def generate_target_file_path(target_info) - raise "target_dir not set" unless @target_dir - - filename = URI.encode_www_form_component(target_info.path) - File.join(@target_dir, filename) - end - end end end diff --git a/lib/sigstore/tuf/error.rb b/lib/sigstore/tuf/error.rb index 2758014..3423819 100644 --- a/lib/sigstore/tuf/error.rb +++ b/lib/sigstore/tuf/error.rb @@ -19,5 +19,9 @@ module Sigstore::TUF class Error < ::Sigstore::Error class LengthOrHashMismatch < Error; end + class ExpiredMetadata < Error; end + class EqualVersionNumber < Error; end + class BadVersionNumber < Error; end + class BadUpdateOrder < Error; end end end diff --git a/lib/sigstore/tuf/trusted_metadata_set.rb b/lib/sigstore/tuf/trusted_metadata_set.rb index ae047a5..88f6b67 100644 --- a/lib/sigstore/tuf/trusted_metadata_set.rb +++ b/lib/sigstore/tuf/trusted_metadata_set.rb @@ -14,14 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +require_relative "error" require_relative "root" require_relative "../internal/json" module Sigstore::TUF - class ExpiredMetadataError < StandardError; end - class EqualVersionNumberError < StandardError; end - class BadVersionNumberError < StandardError; end - class TrustedMetadataSet include Sigstore::Loggable @@ -30,7 +27,7 @@ def initialize(root_data, envelope_type, reference_time: Time.now.utc) @reference_time = reference_time @envelope_type = envelope_type - # debug + logger.debug { "Loading trusted root" } load_trusted_root(root_data) end @@ -39,15 +36,15 @@ def root end def root=(data) - raise "cannot update root after timestamp" if @trusted_set.key?("timestamp") + raise Error::BadUpdateOrder, "cannot update root after timestamp" if @trusted_set.key?("timestamp") - metadata, signed, signatures = load_data(Root, data, root) - metadata.verify_delegate("root", Sigstore::Internal::JSON.canonical_generate(signed), signatures) - raise "root version incr" if metadata.version != root.version + 1 + metadata, canonical_signed, signatures = load_data(Root, data, root) + metadata.verify_delegate("root", canonical_signed, signatures) + raise Error::BadVersionNumber, "root version did not increment by one" if metadata.version != root.version + 1 @trusted_set["root"] = metadata - # debug + logger.debug { "Updated root v#{metadata.version}" } end def snapshot @@ -59,18 +56,21 @@ def timestamp end def timestamp=(data) - raise "cannot update timestamp after snapshot" if @trusted_set.key?("snapshot") + raise Error::BadUpdateOrder, "cannot update timestamp after snapshot" if @trusted_set.key?("snapshot") if root.expired?(@reference_time) - raise ExpiredMetadataError, + raise Error::ExpiredMetadata, "final root.json expired at #{root.expires}, is #{@reference_time}" end metadata, = load_data(Timestamp, data, root) if include?(Timestamp::TYPE) - raise "timestamp version did not increase" if metadata.version < timestamp.version - raise EqualVersionNumberError if metadata.version == timestamp.version + if metadata.version < timestamp.version + raise Error::BadVersionNumber, + "timestamp version less than metadata version" + end + raise Error::EqualVersionNumber if metadata.version == timestamp.version snapshot_meta = timestamp.snapshot_meta new_snapshot_meta = metadata.snapshot_meta @@ -82,8 +82,8 @@ def timestamp=(data) end def snapshot=(data, trusted: false) - raise "cannot update snapshot before timestamp" unless @trusted_set.key?("timestamp") - raise "cannot update snapshot after targets" if @trusted_set.key?("targets") + raise Error::BadUpdateOrder, "cannot update snapshot before timestamp" unless @trusted_set.key?("timestamp") + raise Error::BadUpdateOrder, "cannot update snapshot after targets" if @trusted_set.key?("targets") check_final_timestamp @@ -93,7 +93,10 @@ def snapshot=(data, trusted: false) new_snapshot, = load_data(Snapshot, data, root) - raise "snapshot version incr" if include?(Snapshot::TYPE) && (new_snapshot.version < snapshot.version) + if include?(Snapshot::TYPE) && (new_snapshot.version < snapshot.version) + raise Error::BadVersionNumber, + "snapshot version decreased" + end @trusted_set["snapshot"] = new_snapshot logger.debug { "Updated snapshot v#{new_snapshot.version}" } @@ -109,12 +112,12 @@ def [](role) end def update_delegated_targets(data, role, parent_role) - raise "cannot update targets before snapshot" unless @trusted_set.key?("snapshot") + raise Error::BadUpdateOrder, "cannot update targets before snapshot" unless @trusted_set.key?("snapshot") check_final_snapshot delegator = @trusted_set.fetch(parent_role) - raise "cannot load targets before delegator" unless delegator + raise Error::BadUpdateOrder, "cannot load targets before delegator" unless delegator logger.debug { "Updating #{role} delegated by #{parent_role}" } @@ -125,9 +128,9 @@ def update_delegated_targets(data, role, parent_role) new_delegate, = load_data(Targets, data, delegator, role) version = new_delegate.version - raise "delegated targets version incr" if version != meta.version + raise Error::BadVersionNumber, "delegated targets version does not match meta version" if version != meta.version - raise "expired delegated targets" if new_delegate.expired?(@reference_time) + raise Error::ExpiredMetadata, "expired delegated targets" if new_delegate.expired?(@reference_time) @trusted_set[role] = new_delegate logger.debug { "Updated #{role} v#{version}" } @@ -137,9 +140,9 @@ def update_delegated_targets(data, role, parent_role) private def load_trusted_root(data) - root, signed, signatures = load_data(Root, data, nil) + root, canonical_signed, signatures = load_data(Root, data, nil) # verify the new root is signed by itself - root.verify_delegate("root", Sigstore::Internal::JSON.canonical_generate(signed), signatures) + root.verify_delegate("root", canonical_signed, signatures) @trusted_set["root"] = root end @@ -153,25 +156,25 @@ def load_data(type, data, delegator, role_name = nil) signatures = metadata.fetch("signatures") metadata = type.new(signed) - delegator&.verify_delegate(role_name || type::TYPE, - Sigstore::Internal::JSON.canonical_generate(signed), signatures) - [metadata, signed, signatures] + canonical_signed = Sigstore::Internal::JSON.canonical_generate(signed) + delegator&.verify_delegate(role_name || type::TYPE, canonical_signed, signatures) + [metadata, canonical_signed, signatures] end def check_final_timestamp return unless timestamp.expired?(@reference_time) - raise ExpiredMetadataError, + raise Error::ExpiredMetadata, "final timestamp.json is expired (expired at #{timestamp.expires} vs reference time #{@reference_time})" end def check_final_snapshot - raise ExpiredMetadataError, "final snapshot.json is expired" if snapshot.expired?(@reference_time) + raise Error::ExpiredMetadata, "final snapshot.json is expired" if snapshot.expired?(@reference_time) snapshot_meta = timestamp.snapshot_meta return unless snapshot.version != snapshot_meta.version - raise BadVersionNumberError, + raise Error::BadVersionNumber, "snapshot version mismatch " \ "(snapshot #{snapshot.version} != timestamp snapshot meta #{snapshot_meta.version})" end diff --git a/lib/sigstore/tuf/updater.rb b/lib/sigstore/tuf/updater.rb new file mode 100644 index 0000000..b408b36 --- /dev/null +++ b/lib/sigstore/tuf/updater.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +# Copyright 2024 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "config" +require_relative "trusted_metadata_set" +require_relative "root" +require_relative "snapshot" +require_relative "targets" +require_relative "timestamp" + +module Sigstore::TUF + class Updater + include Sigstore::Loggable + + def initialize(metadata_dir:, metadata_base_url:, target_base_url:, target_dir:, fetcher:, + config: UpdaterConfig.new) + @dir = metadata_dir + @metadata_base_url = "#{metadata_base_url.to_s.chomp("/")}/" + @target_dir = target_dir + @target_base_url = target_base_url && "#{target_base_url.to_s.chomp("/")}/" + + @fetcher = fetcher + @config = config + + unless %i[metadata simple].include? @config.envelope_type + raise ArgumentError, "Unsupported envelope type: #{@config[:envelope_type].inspect}" + end + + begin + data = load_local_metadata("root") + @trusted_set = TrustedMetadataSet.new(data, "metadata") + rescue ::JSON::ParserError => e + raise "Invalid JSON in #{File.join(@dir, "root.json")}: #{e.class} #{e}" + end + end + + def refresh + load_root + load_timestamp + load_snapshot + load_targets(Targets::TYPE, Root::TYPE) + end + + def get_targetinfo(target_path) + refresh unless @trusted_set.include? Targets::TYPE + preorder_depth_first_walk(target_path) + end + + def find_cached_target(target_info, filepath = nil) + filepath ||= generate_target_file_path(target_info) + + begin + data = File.binread(filepath) + target_info.verify_length_and_hashes(data) + filepath + rescue Errno::ENOENT, Error::LengthOrHashMismatch => e + logger.debug { "No cached target at #{filepath}: #{e.class} #{e.message}" } + nil + end + end + + def download_target(target_info, filepath = nil, target_base_url = nil) + target_base_url ||= @target_base_url + raise ArgumentError, "No target_base_url set" unless target_base_url + + filepath ||= generate_target_file_path(target_info) + + target_filepath = target_info.path + consistent_snapshot = @trusted_set.root.consistent_snapshot + + if consistent_snapshot && @config.prefix_targets_with_hash + hashes = target_info.hashes.values + dir, sep, basename = target_filepath.rpartition("/") + target_filepath = "#{dir}#{sep}#{hashes.first}.#{basename}" + end + + full_url = URI.join(target_base_url, target_filepath) + + @fetcher.get2(full_url) do |resp| + resp.value + target_info.verify_length_and_hashes(resp.body) + + # TODO: atomic write + File.binwrite(filepath, resp.body) + rescue Net::HTTPClientException => e + raise "Failed to download target #{target_info.inspect} #{target_filepath.inspect} from #{full_url}: " \ + "#{e.message}" + end + logger.info { "Downloaded #{target_filepath} to #{filepath}" } + filepath + end + + private + + def load_local_metadata(role_name) + encoded_name = URI.encode_www_form_component(role_name) + + File.binread(File.join(@dir, "#{encoded_name}.json")) + end + + def load_root + lower_bound = @trusted_set.root.version + 1 + upper_bound = lower_bound + @config.max_root_rotations + + lower_bound.upto(upper_bound) do |version| + data = download_metadata("root", version) + @trusted_set.root = data + persist_metadata("root", data) + rescue Net::HTTPClientException => e + break if %w[403 404].include? e.response.code + + raise + end + end + + def load_timestamp + begin + data = load_local_metadata(Timestamp::TYPE) + rescue Errno::ENOENT => e + logger.debug "Local timestamp not valid as final: #{e.class} #{e.message}" + else + @trusted_set.timestamp = data + end + + data = download_metadata(Timestamp::TYPE, nil) + + begin + @trusted_set.timestamp = data + rescue Error::EqualVersionNumber + logger.debug "Timestamp version did not increase" + return + end + + persist_metadata(Timestamp::TYPE, data) + end + + def load_snapshot + data = load_local_metadata(Snapshot::TYPE) + @trusted_set.snapshot = data + logger.debug "Loaded snapshot from local metadata" + rescue Errno::ENOENT => e + logger.debug "Local snapshot not valid as final: #{e.class} #{e.message}" + + snapshot_meta = @trusted_set.timestamp.snapshot_meta + version = snapshot_meta.version if @trusted_set.root.consistent_snapshot + + data = download_metadata(Snapshot::TYPE, version) + @trusted_set.snapshot = data + persist_metadata(Snapshot::TYPE, data) + end + + def load_targets(role, parent_role) + return @trusted_set[role] if @trusted_set.include?(role) + + begin + data = load_local_metadata(role) + @trusted_set.update_delegated_targets(data, role, parent_role).tap do + logger.debug { "Loaded targets for #{role} from local metadata" } + end + rescue Errno::ENOENT + logger.debug { "No local targets for #{role}, fetching" } + + snapshot = @trusted_set.snapshot + metainfo = snapshot.meta.fetch("#{role}.json") + raise "No metadata for role: #{role}" unless metainfo + + version = metainfo.version if @trusted_set.root.consistent_snapshot + data = download_metadata(role, version) + delegated_targets = @trusted_set.update_delegated_targets(data, role, parent_role) + persist_metadata(role, data) + delegated_targets + end + end + + def download_metadata(role_name, version) + encoded_name = URI.encode_www_form_component(role_name) + url = if version.nil? + URI.join(@metadata_base_url, "#{encoded_name}.json") + else + URI.join(@metadata_base_url, "#{version}.#{encoded_name}.json") + end + + logger.debug { "Downloading metadata for #{role_name} from #{url}" } + + resp = @fetcher.get(url) + resp.value + + resp.body + end + + def persist_metadata(role_name, data) + logger.debug { "Persisting metadata for #{role_name}" } + + encoded_name = URI.encode_www_form_component(role_name) + filename = File.join(@dir, "#{encoded_name}.json") + Tempfile.create("", @dir) do |f| + f.binmode + f.write(data) + f.close + + File.rename(f.path, filename) + end + end + + def preorder_depth_first_walk(target_path) + logger.debug { "Searching for target #{target_path}" } + + delegations_to_visit = [[Targets::TYPE, Root::TYPE]] + visited_role_names = Set.new + + while delegations_to_visit.any? && visited_role_names.size < @config.max_delegations + role_name, parent_role = delegations_to_visit.shift + next if visited_role_names.include?(role_name) + + targets = load_targets(role_name, parent_role) + target = targets.targets.fetch(target_path, nil) + + return target if target + + visited_role_names.add(role_name) + + next unless targets.delegations.any? + + child_roles_to_visit = [] + + targets.delegations.roles_for_target(target_path).each do |child_name, terminating| + child_roles_to_visit << [child_name, role_name] + next unless terminating + + logger.debug { "Terminating delegation found for #{child_name}" } + delegations_to_visit.clear + break + end + + delegations_to_visit.concat child_roles_to_visit.reverse + end + + logger.warn { "Max delegations reached, stopping search" } if delegations_to_visit.any? + + nil + end + + def generate_target_file_path(target_info) + raise ArgumentError, "target_dir not set" unless @target_dir + + filename = URI.encode_www_form_component(target_info.path) + File.join(@target_dir, filename) + end + end +end diff --git a/test/sigstore/internal/json_test.rb b/test/sigstore/internal/json_test.rb index aaf14fb..9cc0b6a 100644 --- a/test/sigstore/internal/json_test.rb +++ b/test/sigstore/internal/json_test.rb @@ -12,6 +12,11 @@ def test_canonical_generate end assert_equal "Unsupported data type: Float", e.message + e = assert_raise(ArgumentError) do + Sigstore::Internal::JSON.canonical_generate({ 1 => [] }) + end + assert_equal "Non-string key in hash", e.message + hash = { "empty" => "", "a" => "b", diff --git a/test/sigstore/tuf/trusted_metadata_set_test.rb b/test/sigstore/tuf/trusted_metadata_set_test.rb index c6f40e8..4e63ca3 100644 --- a/test/sigstore/tuf/trusted_metadata_set_test.rb +++ b/test/sigstore/tuf/trusted_metadata_set_test.rb @@ -77,7 +77,7 @@ def test_initialize def test_raises_when_updating_root_after_timestamp @set.timestamp = JSON.dump(@timestamp) - e = assert_raise(RuntimeError) do + e = assert_raise(Sigstore::TUF::Error::BadUpdateOrder) do @set.root = @root_data end @@ -85,7 +85,7 @@ def test_raises_when_updating_root_after_timestamp end def test_raises_when_updating_snapshot_before_timestamp - e = assert_raise(RuntimeError) do + e = assert_raise(Sigstore::TUF::Error::BadUpdateOrder) do @set.snapshot = JSON.dump(@snapshot) end @@ -95,7 +95,7 @@ def test_raises_when_updating_snapshot_before_timestamp def test_raises_when_updating_timestamp_after_snapshot @set.timestamp = JSON.dump(@timestamp) @set.snapshot = JSON.dump(@snapshot) - e = assert_raise(RuntimeError) do + e = assert_raise(Sigstore::TUF::Error::BadUpdateOrder) do @set.timestamp = JSON.dump(@timestamp) end