sigstore 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/CODEOWNERS +6 -0
  4. data/LICENSE +201 -0
  5. data/README.md +26 -0
  6. data/data/_store/prod/root.json +165 -0
  7. data/data/_store/prod/trusted_root.json +114 -0
  8. data/data/_store/staging/root.json +107 -0
  9. data/data/_store/staging/trusted_root.json +87 -0
  10. data/lib/sigstore/error.rb +43 -0
  11. data/lib/sigstore/internal/json.rb +53 -0
  12. data/lib/sigstore/internal/key.rb +183 -0
  13. data/lib/sigstore/internal/keyring.rb +42 -0
  14. data/lib/sigstore/internal/merkle.rb +117 -0
  15. data/lib/sigstore/internal/set.rb +42 -0
  16. data/lib/sigstore/internal/util.rb +52 -0
  17. data/lib/sigstore/internal/x509.rb +460 -0
  18. data/lib/sigstore/models.rb +272 -0
  19. data/lib/sigstore/oidc.rb +149 -0
  20. data/lib/sigstore/policy.rb +104 -0
  21. data/lib/sigstore/rekor/checkpoint.rb +114 -0
  22. data/lib/sigstore/rekor/client.rb +136 -0
  23. data/lib/sigstore/signer.rb +280 -0
  24. data/lib/sigstore/trusted_root.rb +116 -0
  25. data/lib/sigstore/tuf/config.rb +46 -0
  26. data/lib/sigstore/tuf/error.rb +49 -0
  27. data/lib/sigstore/tuf/file.rb +96 -0
  28. data/lib/sigstore/tuf/keys.rb +42 -0
  29. data/lib/sigstore/tuf/roles.rb +106 -0
  30. data/lib/sigstore/tuf/root.rb +53 -0
  31. data/lib/sigstore/tuf/snapshot.rb +45 -0
  32. data/lib/sigstore/tuf/targets.rb +84 -0
  33. data/lib/sigstore/tuf/timestamp.rb +39 -0
  34. data/lib/sigstore/tuf/trusted_metadata_set.rb +193 -0
  35. data/lib/sigstore/tuf/updater.rb +267 -0
  36. data/lib/sigstore/tuf.rb +158 -0
  37. data/lib/sigstore/verifier.rb +492 -0
  38. data/lib/sigstore/version.rb +19 -0
  39. data/lib/sigstore.rb +44 -0
  40. metadata +128 -0
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative "error"
18
+
19
+ module Sigstore::TUF
20
+ module BaseFile
21
+ def self.included(base)
22
+ base.extend(ClassMethods)
23
+ super
24
+ end
25
+
26
+ module ClassMethods
27
+ def verify_hashes(data, expected_hashed)
28
+ expected_hashed.each do |algorithm, expected_hash|
29
+ actual_hash = Digest(algorithm.upcase).hexdigest(data)
30
+ unless actual_hash == expected_hash
31
+ raise Error::LengthOrHashMismatch,
32
+ "observed hash #{actual_hash} does not match expected hash #{expected_hash}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def verify_length(data, expected_length)
38
+ actual_length = data.bytesize
39
+ return if actual_length == expected_length
40
+
41
+ raise Error::LengthOrHashMismatch,
42
+ "Observed length #{actual_length} does not match expected length #{expected_length}"
43
+ end
44
+
45
+ def validate_hashes(hashes)
46
+ raise ArgumentError, "hashes must be non-empty" if hashes.empty?
47
+
48
+ hashes.each do |algorithm, hash|
49
+ raise TypeError, "hashes items must be strings" unless algorithm.is_a?(String) && hash.is_a?(String)
50
+ end
51
+ end
52
+
53
+ def validate_length(length)
54
+ return unless length.negative?
55
+
56
+ raise ArgumentError, "length must be a non-negative integer, got #{length.inspect}"
57
+ end
58
+ end
59
+ end
60
+
61
+ module MetaFile
62
+ def self.included(base)
63
+ base.include(BaseFile)
64
+ base.extend(ClassMethods)
65
+ super
66
+ end
67
+
68
+ def initialize(version: 1, length: nil, hashes: nil, unrecognized_fields: {})
69
+ @version = version
70
+ @length = length
71
+ @hashes = hashes
72
+ @unrecognized_fields = unrecognized_fields
73
+
74
+ raise ArgumentError, "Metafile version must be positive, got #{@version}" if @version <= 0
75
+
76
+ self.class.validate_length(@length) unless @length.nil?
77
+ self.class.validate_hashes(@hashes) unless @hashes.nil?
78
+ end
79
+
80
+ def verify_length_and_hashes(data)
81
+ self.class.verify_length(data, @length) if @length
82
+ self.class.verify_hashes(data, @hashes) if @hashes
83
+ end
84
+
85
+ module ClassMethods
86
+ def from_hash(meta_dict)
87
+ version = meta_dict.fetch("version") { raise KeyError, "version is required, given #{meta_dict.inspect}" }
88
+ length = meta_dict.fetch("length", nil)
89
+ hashes = meta_dict.fetch("hashes", nil)
90
+
91
+ new(version:, length:, hashes:,
92
+ unrecognized_fields: meta_dict.slice(*(meta_dict.keys - %w[version length hashes])))
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Sigstore::TUF
18
+ class Keys
19
+ include Enumerable
20
+
21
+ def initialize(keys)
22
+ @keys = keys.to_h do |key_id, key_data|
23
+ key_type = key_data.fetch("keytype")
24
+ scheme = key_data.fetch("scheme")
25
+ keyval = key_data.fetch("keyval")
26
+ public_key_data = keyval.fetch("public")
27
+
28
+ key = Sigstore::Internal::Key.read(key_type, scheme, public_key_data, key_id:)
29
+
30
+ [key_id, key]
31
+ end
32
+ end
33
+
34
+ def fetch(key_id)
35
+ @keys.fetch(key_id)
36
+ end
37
+
38
+ def each(&)
39
+ @keys.each(&)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Sigstore::TUF
18
+ class Roles
19
+ include Enumerable
20
+
21
+ def initialize(data, keys)
22
+ @roles =
23
+ case data
24
+ when Hash # root roles
25
+ data.to_h do |role_name, role_data|
26
+ role_data = role_data.merge("name" => role_name, "paths" => nil)
27
+ role = Role.new(role_data, keys)
28
+ [role.name, role]
29
+ end
30
+ when Array # targets roles
31
+ data.to_h do |role_data|
32
+ role = Role.new(role_data, keys)
33
+ [role.name, role]
34
+ end
35
+ else
36
+ raise ArgumentError, "Unexpected data: #{data.inspect}"
37
+ end
38
+ end
39
+
40
+ def each(&)
41
+ @roles.each(&)
42
+ end
43
+
44
+ def verify_delegate(type, bytes, signatures)
45
+ role = fetch(type)
46
+ role.verify_delegate(type, bytes, signatures)
47
+ end
48
+
49
+ def fetch(name)
50
+ @roles.fetch(name)
51
+ end
52
+
53
+ def for_target(target_path)
54
+ select do |_, role|
55
+ # TODO: this needs to be tested
56
+ role.paths.any? { |path| File.fnmatch?(path, target_path, File::FNM_PATHNAME) }
57
+ end.to_h
58
+ end
59
+ end
60
+
61
+ class Role
62
+ include Sigstore::Loggable
63
+
64
+ attr_reader :keys, :name, :paths, :threshold
65
+
66
+ def initialize(data, keys)
67
+ @name = data.fetch("name")
68
+ @paths = data.fetch("paths")
69
+ @threshold = data.fetch("threshold")
70
+ @keys = data.fetch("keyids").to_h { |key_id| [key_id, keys.fetch(key_id)] }
71
+ @terminating = data.fetch("terminating", false)
72
+ end
73
+
74
+ def terminating?
75
+ @terminating
76
+ end
77
+
78
+ def verify_delegate(type, bytes, signatures)
79
+ if (duplicate_keys = signatures.map { |sig| sig.fetch("keyid") }.tally.select { |_, count| count > 1 }).any?
80
+ raise Error::DuplicateKeys, "Duplicate keys found in signatures: #{duplicate_keys.inspect}"
81
+ end
82
+
83
+ count = signatures.count do |signature|
84
+ key_id = signature.fetch("keyid")
85
+ unless @keys.include?(key_id)
86
+ logger.warn "Unknown key_id=#{key_id.inspect} in signatures for #{type}"
87
+ next
88
+ end
89
+
90
+ key = @keys.fetch(key_id)
91
+ signature_bytes = [signature.fetch("sig")].pack("H*")
92
+ verified = key.verify("sha256", signature_bytes, bytes)
93
+
94
+ logger.debug do
95
+ "key_id=#{key_id.inspect} type=#{type} verified=#{verified}"
96
+ end
97
+ verified
98
+ end
99
+
100
+ return unless count < @threshold
101
+
102
+ raise Error::TooFewSignatures,
103
+ "Not enough signatures: found #{count} out of threshold=#{@threshold} for #{type}"
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "time"
18
+
19
+ require_relative "keys"
20
+ require_relative "roles"
21
+ require_relative "../internal/key"
22
+
23
+ module Sigstore::TUF
24
+ class Root
25
+ include Sigstore::Loggable
26
+
27
+ TYPE = "root"
28
+ attr_reader :version, :consistent_snapshot, :expires
29
+
30
+ def initialize(data)
31
+ type = data.fetch("_type")
32
+ raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE
33
+
34
+ @spec_version = data.fetch("spec_version") { raise Error::InvalidData, "root missing spec_version" }
35
+ @consistent_snapshot = data.fetch("consistent_snapshot") do
36
+ raise Error::InvalidData, "root missing consistent_snapshot"
37
+ end
38
+ @version = data.fetch("version") { raise Error::InvalidData, "root missing version" }
39
+ @expires = Time.iso8601(data.fetch("expires") { raise Error::InvalidData, "root missing expires" })
40
+ keys = Keys.new data.fetch("keys")
41
+ @roles = Roles.new data.fetch("roles"), keys
42
+ @unrecognized_fields = data.fetch("unrecognized_fields", {})
43
+ end
44
+
45
+ def verify_delegate(type, bytes, signatures)
46
+ @roles.verify_delegate(type, bytes, signatures)
47
+ end
48
+
49
+ def expired?(reference_time)
50
+ @expires < reference_time
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative "file"
18
+
19
+ module Sigstore::TUF
20
+ # The class for the Snapshot role
21
+ class Snapshot
22
+ TYPE = "snapshot"
23
+
24
+ attr_reader :version, :meta
25
+
26
+ def initialize(data)
27
+ type = data.fetch("_type")
28
+ raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE
29
+
30
+ @version = data.fetch("version")
31
+ @expires = Time.iso8601 data.fetch("expires")
32
+ @meta = data.fetch("meta").transform_values { Meta.from_hash(_1) }
33
+ end
34
+
35
+ def expired?(reference_time)
36
+ @expires < reference_time
37
+ end
38
+
39
+ class Meta
40
+ include MetaFile
41
+
42
+ attr_reader :version
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative "file"
18
+ require_relative "keys"
19
+ require_relative "roles"
20
+
21
+ module Sigstore::TUF
22
+ class Targets
23
+ TYPE = "targets"
24
+
25
+ attr_reader :version, :targets, :delegations
26
+
27
+ def initialize(data)
28
+ type = data.fetch("_type")
29
+ raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE
30
+
31
+ @version = data.fetch("version")
32
+ @expires = Time.iso8601 data.fetch("expires")
33
+ @targets = data.fetch("targets").to_h { |k, v| [k, Target.new(v, k)] }
34
+ @delegations = Delegations.new(data.fetch("delegations", {}))
35
+
36
+ @unrecognized_fields = data.fetch("unrecognized_fields", {})
37
+ end
38
+
39
+ def expired?(reference_time)
40
+ @expires < reference_time
41
+ end
42
+
43
+ def verify_delegate(type, bytes, signatures)
44
+ role = @delegations.fetch(type)
45
+ role.verify_delegate(type, bytes, signatures)
46
+ end
47
+
48
+ class Target
49
+ attr_reader :path, :hashes
50
+
51
+ include BaseFile
52
+
53
+ def initialize(data, path)
54
+ @path = path
55
+ @length = data.fetch("length")
56
+ @hashes = data.fetch("hashes")
57
+ end
58
+
59
+ def verify_length_and_hashes(data)
60
+ self.class.verify_length(data, @length)
61
+ self.class.verify_hashes(data, @hashes)
62
+ end
63
+ end
64
+
65
+ class Delegations
66
+ def initialize(data)
67
+ keys = Keys.new data.fetch("keys", {})
68
+ @roles = Roles.new data.fetch("roles", []), keys
69
+ end
70
+
71
+ def roles_for_target(target_path)
72
+ @roles.for_target(target_path)
73
+ end
74
+
75
+ def any?
76
+ @roles.any?
77
+ end
78
+
79
+ def fetch(name)
80
+ @roles.fetch(name)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Sigstore::TUF
18
+ class Timestamp
19
+ TYPE = "timestamp"
20
+
21
+ attr_reader :version, :spec_version, :expires, :snapshot_meta, :unrecognized_fields
22
+
23
+ def initialize(data)
24
+ type = data.fetch("_type")
25
+ raise Error::InvalidData, "Expected type to be #{TYPE}, got #{type.inspect}" unless type == TYPE
26
+
27
+ @version = data.fetch("version")
28
+ @spec_version = data.fetch("spec_version")
29
+ @expires = Time.iso8601 data.fetch("expires")
30
+ meta_dict = data.fetch("meta")
31
+ @snapshot_meta = Snapshot::Meta.from_hash(meta_dict["snapshot.json"])
32
+ @unrecognized_fields = data.fetch("unrecognized_fields", {})
33
+ end
34
+
35
+ def expired?(reference_time)
36
+ @expires < reference_time
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Sigstore Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative "error"
18
+ require_relative "root"
19
+ require_relative "../internal/json"
20
+
21
+ require "json"
22
+
23
+ module Sigstore::TUF
24
+ class TrustedMetadataSet
25
+ include Sigstore::Loggable
26
+
27
+ def initialize(root_data, envelope_type, reference_time: Time.now.utc)
28
+ @trusted_set = {}
29
+ @reference_time = reference_time
30
+ @envelope_type = envelope_type
31
+
32
+ logger.debug { "Loading trusted root" }
33
+ load_trusted_root(root_data)
34
+ end
35
+
36
+ def root
37
+ @trusted_set.fetch("root") { raise Error::InvalidData, "missing root metadata" }
38
+ end
39
+
40
+ def root=(data)
41
+ raise Error::BadUpdateOrder, "cannot update root after timestamp" if @trusted_set.key?("timestamp")
42
+
43
+ metadata, canonical_signed, signatures = load_data(Root, data, root)
44
+ metadata.verify_delegate("root", canonical_signed, signatures)
45
+ raise Error::BadVersionNumber, "root version did not increment by one" if metadata.version != root.version + 1
46
+
47
+ @trusted_set["root"] = metadata
48
+
49
+ logger.debug { "Updated root v#{metadata.version}" }
50
+ end
51
+
52
+ def snapshot
53
+ @trusted_set.fetch("snapshot")
54
+ end
55
+
56
+ def timestamp
57
+ @trusted_set.fetch("timestamp")
58
+ end
59
+
60
+ def timestamp=(data)
61
+ raise Error::BadUpdateOrder, "cannot update timestamp after snapshot" if @trusted_set.key?("snapshot")
62
+
63
+ if root.expired?(@reference_time)
64
+ raise Error::ExpiredMetadata,
65
+ "final root.json expired at #{root.expires}, is #{@reference_time}"
66
+ end
67
+
68
+ metadata, = load_data(Timestamp, data, root)
69
+
70
+ if include?(Timestamp::TYPE)
71
+ if metadata.version < timestamp.version
72
+ raise Error::BadVersionNumber,
73
+ "timestamp version less than metadata version"
74
+ end
75
+ raise Error::EqualVersionNumber if metadata.version == timestamp.version
76
+
77
+ snapshot_meta = timestamp.snapshot_meta
78
+ new_snapshot_meta = metadata.snapshot_meta
79
+ if new_snapshot_meta.version < snapshot_meta.version
80
+ raise Error::BadVersionNumber, "snapshot version did not increase"
81
+ end
82
+ end
83
+
84
+ @trusted_set["timestamp"] = metadata
85
+ check_final_timestamp
86
+ end
87
+
88
+ def snapshot=(data, trusted: false)
89
+ raise Error::BadUpdateOrder, "cannot update snapshot before timestamp" unless @trusted_set.key?("timestamp")
90
+ raise Error::BadUpdateOrder, "cannot update snapshot after targets" if @trusted_set.key?("targets")
91
+
92
+ check_final_timestamp
93
+
94
+ snapshot_meta = timestamp.snapshot_meta
95
+
96
+ snapshot_meta.verify_length_and_hashes(data) unless trusted
97
+
98
+ new_snapshot, = load_data(Snapshot, data, root)
99
+
100
+ # If an existing trusted snapshot is updated, check for rollback attack
101
+ if include?(Snapshot::TYPE)
102
+ snapshot.meta.each do |filename, file_info|
103
+ new_file_info = new_snapshot.meta[filename]
104
+ raise Error::RepositoryError, "new snapshot is missing info for #{filename}" unless new_file_info
105
+
106
+ if new_file_info.version < file_info.version
107
+ raise Error::BadVersionNumber, "expected #{filename} v#{new_file_info.version}, got v#{file_info.version}"
108
+ end
109
+ end
110
+ end
111
+
112
+ @trusted_set["snapshot"] = new_snapshot
113
+ logger.debug { "Updated snapshot v#{new_snapshot.version}" }
114
+ check_final_snapshot
115
+ end
116
+
117
+ def include?(type)
118
+ @trusted_set.key?(type)
119
+ end
120
+
121
+ def [](role)
122
+ @trusted_set.fetch(role)
123
+ end
124
+
125
+ def update_delegated_targets(data, role, parent_role)
126
+ raise Error::BadUpdateOrder, "cannot update targets before snapshot" unless @trusted_set.key?("snapshot")
127
+
128
+ check_final_snapshot
129
+
130
+ delegator = @trusted_set[parent_role]
131
+ logger.debug { "Updating #{role} delegated by #{parent_role.inspect} to #{delegator.class}" }
132
+ raise Error::BadUpdateOrder, "cannot load targets before delegator" unless delegator
133
+
134
+ meta = snapshot.meta["#{role}.json"]
135
+ raise Error::RepositoryError, "no metadata for role #{role} in snapshot" unless meta
136
+
137
+ meta.verify_length_and_hashes(data)
138
+
139
+ new_delegate, = load_data(Targets, data, delegator, role)
140
+ version = new_delegate.version
141
+ raise Error::BadVersionNumber, "expected #{role} v#{meta.version}, got v#{version}" if version != meta.version
142
+
143
+ raise Error::ExpiredMetadata, "new #{role} is expired" if new_delegate.expired?(@reference_time)
144
+
145
+ @trusted_set[role] = new_delegate
146
+ logger.debug { "Updated #{role} v#{version}" }
147
+ new_delegate
148
+ end
149
+
150
+ private
151
+
152
+ def load_trusted_root(data)
153
+ root, canonical_signed, signatures = load_data(Root, data, nil)
154
+ # verify the new root is signed by itself
155
+ root.verify_delegate("root", canonical_signed, signatures)
156
+
157
+ @trusted_set["root"] = root
158
+ end
159
+
160
+ def load_data(type, data, delegator, role_name = nil)
161
+ metadata = JSON.parse(data)
162
+ signed = metadata.fetch("signed")
163
+ unless signed["_type"] == type::TYPE
164
+ raise Error::InvalidData,
165
+ "Expected type to be #{type::TYPE}, got #{signed["_type"].inspect}"
166
+ end
167
+
168
+ signatures = metadata.fetch("signatures")
169
+ metadata = type.new(signed)
170
+ canonical_signed = Sigstore::Internal::JSON.canonical_generate(signed)
171
+ delegator&.verify_delegate(role_name || type::TYPE, canonical_signed, signatures)
172
+ [metadata, canonical_signed, signatures]
173
+ rescue JSON::ParserError => e
174
+ raise Error::InvalidData, "Failed to parse #{type}: #{e.message}"
175
+ end
176
+
177
+ def check_final_timestamp
178
+ return unless timestamp.expired?(@reference_time)
179
+
180
+ raise Error::ExpiredMetadata,
181
+ "final timestamp.json is expired (expired at #{timestamp.expires} vs reference time #{@reference_time})"
182
+ end
183
+
184
+ def check_final_snapshot
185
+ raise Error::ExpiredMetadata, "final snapshot.json is expired" if snapshot.expired?(@reference_time)
186
+
187
+ snapshot_meta = timestamp.snapshot_meta
188
+ return if snapshot.version == snapshot_meta.version
189
+
190
+ raise Error::BadVersionNumber, "expected snapshot version #{snapshot_meta.version}, got #{snapshot.version}"
191
+ end
192
+ end
193
+ end