sigstore 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/CODEOWNERS +6 -0
- data/LICENSE +201 -0
- data/README.md +26 -0
- data/data/_store/prod/root.json +165 -0
- data/data/_store/prod/trusted_root.json +114 -0
- data/data/_store/staging/root.json +107 -0
- data/data/_store/staging/trusted_root.json +87 -0
- data/lib/sigstore/error.rb +43 -0
- data/lib/sigstore/internal/json.rb +53 -0
- data/lib/sigstore/internal/key.rb +183 -0
- data/lib/sigstore/internal/keyring.rb +42 -0
- data/lib/sigstore/internal/merkle.rb +117 -0
- data/lib/sigstore/internal/set.rb +42 -0
- data/lib/sigstore/internal/util.rb +52 -0
- data/lib/sigstore/internal/x509.rb +460 -0
- data/lib/sigstore/models.rb +272 -0
- data/lib/sigstore/oidc.rb +149 -0
- data/lib/sigstore/policy.rb +104 -0
- data/lib/sigstore/rekor/checkpoint.rb +114 -0
- data/lib/sigstore/rekor/client.rb +136 -0
- data/lib/sigstore/signer.rb +280 -0
- data/lib/sigstore/trusted_root.rb +116 -0
- data/lib/sigstore/tuf/config.rb +46 -0
- data/lib/sigstore/tuf/error.rb +49 -0
- data/lib/sigstore/tuf/file.rb +96 -0
- data/lib/sigstore/tuf/keys.rb +42 -0
- data/lib/sigstore/tuf/roles.rb +106 -0
- data/lib/sigstore/tuf/root.rb +53 -0
- data/lib/sigstore/tuf/snapshot.rb +45 -0
- data/lib/sigstore/tuf/targets.rb +84 -0
- data/lib/sigstore/tuf/timestamp.rb +39 -0
- data/lib/sigstore/tuf/trusted_metadata_set.rb +193 -0
- data/lib/sigstore/tuf/updater.rb +267 -0
- data/lib/sigstore/tuf.rb +158 -0
- data/lib/sigstore/verifier.rb +492 -0
- data/lib/sigstore/version.rb +19 -0
- data/lib/sigstore.rb +44 -0
- 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
|