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,87 @@
1
+ {
2
+ "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
3
+ "tlogs": [
4
+ {
5
+ "baseUrl": "https://rekor.sigstage.dev",
6
+ "hashAlgorithm": "SHA2_256",
7
+ "publicKey": {
8
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==",
9
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
10
+ "validFor": {
11
+ "start": "2021-01-12T11:53:27.000Z"
12
+ }
13
+ },
14
+ "logId": {
15
+ "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY="
16
+ }
17
+ }
18
+ ],
19
+ "certificateAuthorities": [
20
+ {
21
+ "subject": {
22
+ "organization": "sigstore.dev",
23
+ "commonName": "sigstore"
24
+ },
25
+ "uri": "https://fulcio.sigstage.dev",
26
+ "certChain": {
27
+ "certificates": [
28
+ {
29
+ "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q=="
30
+ },
31
+ {
32
+ "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw="
33
+ }
34
+ ]
35
+ },
36
+ "validFor": {
37
+ "start": "2022-04-14T21:38:40.000Z"
38
+ }
39
+ }
40
+ ],
41
+ "ctlogs": [
42
+ {
43
+ "baseUrl": "https://ctfe.sigstage.dev/test",
44
+ "hashAlgorithm": "SHA2_256",
45
+ "publicKey": {
46
+ "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==",
47
+ "keyDetails": "PKCS1_RSA_PKCS1V5",
48
+ "validFor": {
49
+ "start": "2021-03-14T00:00:00.000Z",
50
+ "end": "2022-07-31T00:00:00.000Z"
51
+ }
52
+ },
53
+ "logId": {
54
+ "keyId": "G3wUKk6ZK6ffHh/FdCRUE2wVekyzHEEIpSG4savnv0w="
55
+ }
56
+ },
57
+ {
58
+ "baseUrl": "https://ctfe.sigstage.dev/2022",
59
+ "hashAlgorithm": "SHA2_256",
60
+ "publicKey": {
61
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==",
62
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
63
+ "validFor": {
64
+ "start": "2022-07-01T00:00:00.000Z",
65
+ "end": "2022-07-31T00:00:00.000Z"
66
+ }
67
+ },
68
+ "logId": {
69
+ "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA="
70
+ }
71
+ },
72
+ {
73
+ "baseUrl": "https://ctfe.sigstage.dev/2022-2",
74
+ "hashAlgorithm": "SHA2_256",
75
+ "publicKey": {
76
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==",
77
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
78
+ "validFor": {
79
+ "start": "2022-07-01T00:00:00.000Z"
80
+ }
81
+ },
82
+ "logId": {
83
+ "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno="
84
+ }
85
+ }
86
+ ]
87
+ }
@@ -0,0 +1,43 @@
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
18
+ class Error < StandardError
19
+ class InvalidSignature < Error; end
20
+ class InvalidBundle < Error; end
21
+ class InvalidCertificate < Error; end
22
+ class NoCertificate < Error; end
23
+ class NoTrustedRoot < Error; end
24
+ class NoBundle < Error; end
25
+ class NoSignature < Error; end
26
+ class InvalidKey < Error; end
27
+ class InvalidCheckpoint < Error; end
28
+ class InvalidVerificationInput < Error; end
29
+
30
+ class Signing < Error; end
31
+ class InvalidIdentityToken < Error; end
32
+
33
+ class MissingRekorEntry < Error; end
34
+ class InvalidRekorEntry < Error; end
35
+ class FailedRekorLookup < Error; end
36
+ class FailedRekorPost < Error; end
37
+
38
+ class Unimplemented < Error; end
39
+
40
+ class UnsupportedPlatform < Error; end
41
+ class UnsupportedKeyType < Error; end
42
+ end
43
+ 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
+ module Sigstore::Internal
18
+ module JSON
19
+ # Implements https://wiki.laptop.org/go/Canonical_JSON
20
+ #
21
+ # TODO: This is a naive implementation. Performance can be improved by
22
+ # serializing into a buffer instead of concatenating strings.
23
+ def self.canonical_generate(data)
24
+ case data
25
+ when NilClass
26
+ "null"
27
+ when TrueClass
28
+ "true"
29
+ when FalseClass
30
+ "false"
31
+ when Integer
32
+ data.to_s
33
+ when String
34
+ "\"#{data.gsub(/(["\\])/, '\\\\\1')}\""
35
+ when Array
36
+ contents = data.map { |v| canonical_generate(v) }.join(",")
37
+ "[#{contents}]"
38
+ when Hash
39
+ contents = data.sort_by do |k, _|
40
+ raise ArgumentError, "Non-string key in hash" unless k.is_a?(String)
41
+
42
+ k.encode("utf-16").codepoints
43
+ end
44
+ contents.map! do |k, v|
45
+ "#{canonical_generate(k)}:#{canonical_generate(v)}"
46
+ end
47
+ "{#{contents.join(",")}}"
48
+ else
49
+ raise ArgumentError, "Unsupported data type: #{data.class}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,183 @@
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 "util"
18
+
19
+ module Sigstore
20
+ module Internal
21
+ class Key
22
+ include Loggable
23
+
24
+ def self.from_key_details(key_details, key_bytes)
25
+ case key_details
26
+ when Common::V1::PublicKeyDetails::PKIX_ECDSA_P256_SHA_256
27
+ key_type = "ecdsa"
28
+ key_schema = "ecdsa-sha2-nistp256"
29
+ when Common::V1::PublicKeyDetails::PKCS1_RSA_PKCS1V5
30
+ key_type = "rsa"
31
+ key_schema = "rsa-pkcs1v15-sha256"
32
+ else
33
+ raise Error::UnsupportedKeyType, "Unsupported key type #{key_details}"
34
+ end
35
+
36
+ read(key_type, key_schema, key_bytes, key_id: OpenSSL::Digest::SHA256.hexdigest(key_bytes))
37
+ end
38
+
39
+ def self.read(key_type, schema, key_bytes, key_id: nil)
40
+ case key_type
41
+ when "ecdsa", "ecdsa-sha2-nistp256"
42
+ pkey = OpenSSL::PKey::EC.new(key_bytes)
43
+ EDCSA.new(key_type, schema, pkey, key_id:)
44
+ when "ed25519"
45
+ pkey = ED25519.pkey_from_der([key_bytes].pack("H*"))
46
+ ED25519.new(key_type, schema, pkey, key_id:)
47
+ when "rsa"
48
+ pkey = OpenSSL::PKey::RSA.new(key_bytes)
49
+ RSA.new(key_type, schema, pkey, key_id:)
50
+ else
51
+ raise ArgumentError, "Unsupported key type #{key_type}"
52
+ end.tap do |key|
53
+ if RUBY_ENGINE == "jruby" && key.to_pem != key_bytes && key.to_der != key_bytes
54
+ raise Error::UnsupportedPlatform, "Key mismatch: #{key.to_pem.inspect} != #{key_bytes.inspect}"
55
+ end
56
+ end
57
+ rescue OpenSSL::PKey::PKeyError => e
58
+ raise OpenSSL::PKey::PKeyError, "Invalid key: #{e} for #{key_type} #{schema} #{key_id}"
59
+ end
60
+
61
+ attr_reader :key_type, :schema, :key_id
62
+
63
+ def initialize(key_type, schema, key, key_id: nil)
64
+ @key_type = key_type
65
+ @key = key
66
+ @schema = schema
67
+ @key_id = key_id
68
+ end
69
+
70
+ def to_pem
71
+ @key.to_pem
72
+ end
73
+
74
+ def to_der
75
+ @key.to_der
76
+ end
77
+
78
+ def verify(algo, signature, data)
79
+ @key.verify(algo, signature, data)
80
+ rescue OpenSSL::PKey::PKeyError => e
81
+ logger.debug { "Verification failed: #{e}" }
82
+ false
83
+ end
84
+
85
+ def public_to_der
86
+ @key.public_to_der
87
+ end
88
+
89
+ class EDCSA < Key
90
+ def initialize(...)
91
+ super
92
+ unless @key_type == "ecdsa" || @key_type == "ecdsa-sha2-nistp256"
93
+ raise ArgumentError,
94
+ "key_type must be edcsa, given #{@key_type}"
95
+ end
96
+ unless @key.is_a?(OpenSSL::PKey::EC)
97
+ raise ArgumentError,
98
+ "key must be an OpenSSL::PKey::EC, is #{@key.inspect}"
99
+ end
100
+
101
+ case @schema
102
+ when "ecdsa-sha2-nistp256"
103
+ unless @key.group.curve_name == "prime256v1"
104
+ raise ArgumentError, "Expected prime256v1 curve, got #{@key.group.curve_name}"
105
+ end
106
+ else
107
+ raise ArgumentError, "Unsupported schema #{schema}"
108
+ end
109
+ end
110
+ end
111
+
112
+ class RSA < Key
113
+ def initialize(...)
114
+ super
115
+ raise ArgumentError, "key_type must be rsa, given #{@key_type}" unless @key_type == "rsa"
116
+
117
+ unless @key.is_a?(OpenSSL::PKey::RSA)
118
+ raise ArgumentError, "key must be an OpenSSL::PKey::RSA, given #{@key.inspect}"
119
+ end
120
+
121
+ case @schema
122
+ when "rsassa-pss-sha256"
123
+ raise Error::UnsupportedPlatform, "RSA-PSS verification unsupported" unless @key.respond_to?(:verify_pss)
124
+ when "rsa-pkcs1v15-sha256"
125
+ # supported
126
+ else
127
+ raise ArgumentError, "Unsupported schema #{schema}"
128
+ end
129
+ end
130
+
131
+ def verify(_algo, signature, data)
132
+ case @schema
133
+ when "rsassa-pss-sha256"
134
+ @key.verify_pss("sha256", signature, data, salt_length: :auto, mgf1_hash: "SHA256")
135
+ when "rsa-pkcs1v15-sha256"
136
+ super
137
+ else
138
+ raise ArgumentError, "Unsupported schema #{schema}"
139
+ end
140
+ end
141
+ end
142
+
143
+ class ED25519 < Key
144
+ def self.pkey_from_der(raw)
145
+ if OpenSSL::PKey.respond_to?(:new_raw_public_key)
146
+ OpenSSL::PKey.new_raw_public_key("ed25519", raw)
147
+ else
148
+ pem = <<~PEM
149
+ -----BEGIN PUBLIC KEY-----
150
+ MCowBQYDK2VwAyEA#{Internal::Util.base64_encode(raw)}
151
+ -----END PUBLIC KEY-----
152
+ PEM
153
+ OpenSSL::PKey.read(pem)
154
+ end
155
+ end
156
+
157
+ def initialize(...)
158
+ super
159
+ unless @key_type == "ed25519"
160
+ raise ArgumentError,
161
+ "key_type must be ed25519, given #{@key_type}"
162
+ end
163
+ unless @key.is_a?(OpenSSL::PKey::PKey) && @key.oid == "ED25519"
164
+ raise ArgumentError,
165
+ "key must be an OpenSSL::PKey::PKey with oid ED25519, is #{@key.inspect}"
166
+ end
167
+ raise ArgumentError, "schema must be #{schema}" unless @schema == schema
168
+
169
+ case @schema
170
+ when "ed25519"
171
+ # supported
172
+ else
173
+ raise ArgumentError, "Unsupported schema #{schema}"
174
+ end
175
+ end
176
+
177
+ def verify(_algo, signature, data)
178
+ super(nil, signature, data)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ 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
18
+ module Internal
19
+ class Keyring
20
+ def initialize(keys:)
21
+ @keyring = {}
22
+ keys.each do |key|
23
+ raise Error, "Duplicate key id #{key.key_id} in keyring" if @keyring.key?(key.key_id)
24
+
25
+ @keyring[key.key_id] = key
26
+ end
27
+ end
28
+
29
+ def verify(key_id:, signature:, data:)
30
+ key = @keyring.fetch(key_id) { raise KeyError, "key not found: #{key_id.inspect}, known: #{@keyring.keys}" }
31
+
32
+ return true if key.verify("SHA256", signature, data)
33
+
34
+ raise(Error::InvalidSignature,
35
+ "invalid signature: #{signature.inspect} over #{data.inspect} with key #{key_id.inspect}")
36
+ rescue OpenSSL::PKey::PKeyError => e
37
+ raise(Error::InvalidSignature,
38
+ "#{e}: invalid signature: #{signature.inspect} over #{data.inspect} with key #{key_id.inspect}")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,117 @@
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 "util"
18
+
19
+ module Sigstore
20
+ module Internal
21
+ module Merkle
22
+ class MissingInclusionProofError < StandardError; end
23
+ class MissingHashError < StandardError; end
24
+ class InvalidInclusionProofError < StandardError; end
25
+ class InclusionProofSizeError < InvalidInclusionProofError; end
26
+
27
+ def self.verify_merkle_inclusion(entry)
28
+ inclusion_proof = entry.inclusion_proof
29
+ raise MissingInclusionProofError, "Rekor entry has no inclusion proof" unless inclusion_proof
30
+
31
+ leaf_hash = hash_leaf(entry.canonicalized_body)
32
+ verify_inclusion(inclusion_proof.log_index, inclusion_proof.tree_size,
33
+ inclusion_proof.hashes,
34
+ inclusion_proof.root_hash, leaf_hash)
35
+ end
36
+
37
+ def self.verify_inclusion(index, tree_size, proof, root, leaf_hash)
38
+ calc_hash = root_from_inclusion_proof(index, tree_size, proof, leaf_hash)
39
+
40
+ return if calc_hash == root
41
+
42
+ raise InvalidInclusionProofError,
43
+ "Inclusion proof contains invalid root hash: " \
44
+ "expected #{root.unpack1("H*")}, calculated #{calc_hash.unpack1("H*")}"
45
+ end
46
+
47
+ def self.root_from_inclusion_proof(log_index, tree_size, proof, leaf_hash)
48
+ if log_index >= tree_size
49
+ raise InclusionProofSizeError,
50
+ "Log index #{log_index} is greater than tree size #{tree_size}"
51
+ end
52
+
53
+ if leaf_hash.bytesize != 32
54
+ raise InvalidInclusionProofError,
55
+ "Leaf hash has wrong size, expected 32 bytes, got #{leaf_hash.size}"
56
+ end
57
+
58
+ if proof.any? { |i| i.bytesize != 32 }
59
+ raise InvalidInclusionProofError,
60
+ "Proof hashes have wrong sizes, expected 32 bytes, got #{proof.inspect}"
61
+ end
62
+
63
+ inner, border = decompose_inclusion_proof(log_index, tree_size)
64
+
65
+ if proof.size != inner + border
66
+ raise InclusionProofSizeError,
67
+ "Inclusion proof has wrong size, expected #{inner + border} hashes, got #{proof.size}"
68
+ end
69
+
70
+ intermediate_result = chain_inner(
71
+ leaf_hash,
72
+ (proof[...inner] || raise(MissingHashError, "missing left hashes")),
73
+ log_index
74
+ )
75
+
76
+ chain_border_right(
77
+ intermediate_result,
78
+ proof[inner..] || raise(MissingHashError, "missing right hashes")
79
+ )
80
+ end
81
+
82
+ def self.decompose_inclusion_proof(log_index, tree_size)
83
+ inner = (log_index ^ (tree_size - 1)).bit_length
84
+ border = (log_index >> inner).to_s(2).count("1")
85
+
86
+ [inner, border]
87
+ end
88
+
89
+ def self.hash_leaf(data)
90
+ data = "\u0000#{data}".b
91
+ OpenSSL::Digest.new("SHA256").digest(data)
92
+ end
93
+
94
+ def self.chain_inner(seed, hashes, log_index)
95
+ hashes.each_with_index do |hash, i|
96
+ seed = if ((log_index >> i) & 1).zero?
97
+ hash_children(seed, hash)
98
+ else
99
+ hash_children(hash, seed)
100
+ end
101
+ end
102
+ seed
103
+ end
104
+
105
+ def self.chain_border_right(seed, hashes)
106
+ hashes.reduce(seed) do |acc, hash|
107
+ hash_children(hash, acc)
108
+ end
109
+ end
110
+
111
+ def self.hash_children(left, right)
112
+ data = "\u0001#{left}#{right}".b
113
+ OpenSSL::Digest.new("SHA256").digest(data)
114
+ end
115
+ end
116
+ end
117
+ 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
18
+ module Internal
19
+ module SET
20
+ def self.verify_set(keyring:, entry:)
21
+ raise Error, "invalid log entry: no inclusion promise" unless entry.inclusion_promise
22
+
23
+ signed_entry_timestamp = entry.inclusion_promise.signed_entry_timestamp
24
+ log_id = Util.hex_encode(entry.log_id.key_id)
25
+
26
+ # https://www.rfc-editor.org/rfc/rfc8785
27
+ canonical_entry = ::JSON.dump({
28
+ body: Internal::Util.base64_encode(entry.canonicalized_body),
29
+ integratedTime: entry.integrated_time,
30
+ logID: log_id,
31
+ logIndex: entry.log_index
32
+ })
33
+
34
+ keyring.verify(
35
+ key_id: log_id,
36
+ signature: signed_entry_timestamp,
37
+ data: canonical_entry
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
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
18
+ module Internal
19
+ module Util
20
+ module_function
21
+
22
+ def hash_algorithm_name(algorithm)
23
+ case algorithm
24
+ when Common::V1::HashAlgorithm::SHA2_256
25
+ "sha256"
26
+ when Common::V1::HashAlgorithm::SHA2_384
27
+ "sha384"
28
+ when Common::V1::HashAlgorithm::SHA2_512
29
+ "sha512"
30
+ else
31
+ raise ArgumentError, "Unrecognized hash algorithm #{algorithm}"
32
+ end
33
+ end
34
+
35
+ def hex_encode(string)
36
+ string.unpack1("H*")
37
+ end
38
+
39
+ def hex_decode(string)
40
+ [string].pack("H*")
41
+ end
42
+
43
+ def base64_encode(string)
44
+ [string].pack("m0")
45
+ end
46
+
47
+ def base64_decode(string)
48
+ string.unpack1("m0")
49
+ end
50
+ end
51
+ end
52
+ end