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,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
|