sigstore 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|