Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/model_signing/_signing/signing.py: 38%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

92 statements  

1# Copyright 2024 The Sigstore Authors 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""Machinery for signing and verification of ML models. 

16 

17The serialization API produces a manifest representation of the models, and we 

18use that to implement integrity checking of models in different computational 

19patterns. This means that all manifests need to be kept only in memory. 

20 

21Since we need to support multiple signing methods (e.g., Sigstore, key, 

22certificate, etc.) , we provide a `Signer` abstract class with a single `sign` 

23method that takes a signing payload and converts it to a signature in the 

24supported format. 

25 

26Finally, every signature needs to be verified. We pair every `Signer` subclass 

27with a `Verifier` which takes a signature, verify the authenticity of the 

28payload and then expand that to a manifest. 

29 

30Regarding the data formats, for signing, we need to convert the manifest to the 

31signing payload. We only support manifests serialized to in-toto formats 

32described by https://github.com/in-toto/attestation/tree/main/spec/v1. The 

33envelope format is DSSE, as described in 

34https://github.com/secure-systems-lab/dsse. The signature is in a Sigstore 

35bundle format over the DSSE payload. The format is described at 

36https://docs.sigstore.dev/about/bundle/. 

37""" 

38 

39import abc 

40import json 

41import pathlib 

42import sys 

43from typing import Any 

44 

45from in_toto_attestation.v1 import statement 

46 

47from model_signing import manifest 

48from model_signing._hashing import hashing 

49from model_signing._hashing import memory 

50 

51 

52if sys.version_info >= (3, 11): 

53 from typing import Self 

54else: 

55 from typing_extensions import Self 

56 

57 

58# The expected in-toto payload type for the signature. 

59_IN_TOTO_JSON_PAYLOAD_TYPE: str = "application/vnd.in-toto+json" 

60 

61 

62# The expected in-toto statement type for the signature. 

63_IN_TOTO_STATEMENT_TYPE: str = "https://in-toto.io/Statement/v1" 

64 

65 

66# The expected model signature predicate type. 

67_PREDICATE_TYPE: str = "https://model_signing/signature/v1.0" 

68 

69 

70# The expected model signature predicate type for v0.2 (compat) 

71_PREDICATE_TYPE_COMPAT: str = "https://model_signing/Digests/v0.1" 

72 

73 

74def dsse_payload_to_manifest(dsse_payload: dict[str, Any]) -> manifest.Manifest: 

75 """Builds a manifest from the DSSE payload read from a signature. 

76 

77 The payload here is a dictionary that represents the payload part of the 

78 DSSE envelope contained in the Sigstore bundle. 

79 

80 Args: 

81 dsse_payload: The in-toto DSSE envelope to convert to manifest. 

82 

83 Returns: 

84 A manifest representing the signed model. 

85 

86 Raises: 

87 ValueError: The payload cannot be deserialized to a manifest. 

88 """ 

89 obtained_predicate_type = dsse_payload["predicateType"] 

90 if obtained_predicate_type != _PREDICATE_TYPE: 

91 if obtained_predicate_type == _PREDICATE_TYPE_COMPAT: 

92 return dsse_payload_to_manifest_compat(dsse_payload) 

93 raise ValueError( 

94 f"Predicate type mismatch, expected {_PREDICATE_TYPE}, " 

95 f"got {obtained_predicate_type}" 

96 ) 

97 

98 subjects = dsse_payload["subject"] 

99 if len(subjects) != 1: 

100 raise ValueError(f"Expected only one subject, got {subjects}") 

101 

102 model_name = subjects[0]["name"] 

103 expected_digest = subjects[0]["digest"]["sha256"] 

104 

105 predicate = dsse_payload["predicate"] 

106 resources = predicate["resources"] 

107 if not resources: 

108 raise ValueError("Bundle contains no resources") 

109 

110 serialization_args = predicate["serialization"] 

111 serialization = manifest.SerializationType.from_args(serialization_args) 

112 

113 hasher = memory.SHA256() 

114 items = [] 

115 for resource in resources: 

116 name = resource["name"] 

117 algorithm = resource["algorithm"] 

118 digest_value = resource["digest"] 

119 digest = hashing.Digest(algorithm, bytes.fromhex(digest_value)) 

120 hasher.update(digest.digest_value) 

121 items.append(serialization.new_item(name, digest)) 

122 

123 obtained_digest = hasher.compute().digest_hex 

124 if obtained_digest != expected_digest: 

125 raise ValueError( 

126 f"Manifest is inconsistent. Root digest is {expected_digest}, " 

127 f"but the included resources hash to {obtained_digest}" 

128 ) 

129 

130 return manifest.Manifest(model_name, items, serialization) 

131 

132 

133def dsse_payload_to_manifest_compat( 

134 dsse_payload: dict[str, Any], 

135) -> manifest.Manifest: 

136 """Builds a manifest from the DSSE payload read from a signature. 

137 

138 This is the same as `dsse_payload_to_manifest` but using a DSSE payload as 

139 defined at v0.2 release. This was experimental but got used in production 

140 before v1.0 so we need to patch support to it while verifiers migrate to the 

141 forward compatible format defined by v1.0. 

142 

143 Args: 

144 dsse_payload: The in-toto DSSE envelope to convert to manifest. 

145 

146 Returns: 

147 A manifest representing the signed model. 

148 

149 Raises: 

150 ValueError: The payload cannot be deserialized to a manifest. 

151 """ 

152 # Model name is not defined, use a constant. 

153 model_name = "compat-undefined-not-present" 

154 

155 # Serialization format is not present, build a fake one. 

156 serialization = manifest.SerializationType.from_args( 

157 {"method": "files", "hash_type": "sha256", "allow_symlinks": "false"} 

158 ) 

159 

160 # The only field with actual content is the subject. 

161 items = [] 

162 for subject in dsse_payload["subject"]: 

163 name = subject["name"] 

164 algorithm = "sha256" # hardcoded, the only supported one 

165 digest_value = subject["digest"][algorithm] 

166 digest = hashing.Digest(algorithm, bytes.fromhex(digest_value)) 

167 items.append(serialization.new_item(name, digest)) 

168 

169 # There is no verification that the manifest is missing items at this point. 

170 return manifest.Manifest(model_name, items, serialization) 

171 

172 

173class Payload: 

174 """In-toto payload used to represent a model for signing. 

175 

176 This payload represents all the object (files, shards, etc.) of the model 

177 paired with their hashes. It can be seen as a serialization of a manifest. 

178 The hashes are all recorded under the predicate, given that for the subject 

179 we are limited on what hashes we can use 

180 (https://github.com/sigstore/sigstore-python/issues/1018). Each hash follows 

181 the format of a ResourceDescriptor: is an object containing a name for the 

182 object, the hashing algorithm, and the digest value. These are recorded in 

183 the predicate, as part of the `"resources"` list. 

184 

185 The subject is a name for the model (taken from the model's directory) and a 

186 global digest over all the computed digests. This is SHA256 computed over 

187 all the digests, in the order they show up in the predicate (we canonicalize 

188 this to be in alphabetical order). This digest can be used to refer to the 

189 model from other metadata documents without having to carry the entire set 

190 of resource descriptors around. 

191 

192 To ensure backwards compatibility, the predicate contains a 

193 `"serialization"` section which describes the method used to serialize a 

194 model to the manifest used to generate this payload. The section includes a 

195 method name and a list of all relevant values needed to recompute the 

196 serialization. 

197 

198 Future extensions to the model signature (e.g., incorporating model cards, 

199 etc.) can be added as part of the predicate. For v1.0 of the predicate the 

200 only supported fields in the predicate are `"serialization"` and 

201 `"resources"`. Any other field should be ignored by verifiers adhering to 

202 v1.0 version. 

203 

204 Example: 

205 ```json 

206 { 

207 "_type": "https://in-toto.io/Statement/v1", 

208 "subject": [ 

209 { 

210 "name": "sample_model", 

211 "digest": { 

212 "sha256": "143cc6..." 

213 } 

214 } 

215 ], 

216 "predicateType": "https://model_signing/signature/v1.0", 

217 "predicate": { 

218 "serialization": { 

219 "method": "files", 

220 "hash_type": "sha256", 

221 "allow_symlinks": true 

222 "ignore_paths": [ 

223 "model.sig", 

224 ".git", 

225 ".gitattributes", 

226 ".github", 

227 ".gitignore" 

228 ], 

229 }, 

230 "resources": [ 

231 { 

232 "algorithm": "sha256", 

233 "digest": "fdd892...", 

234 "name": "d0/f00" 

235 }, 

236 { 

237 "algorithm": "sha256", 

238 "digest": "e16940...", 

239 "name": "d0/f01" 

240 }, 

241 { 

242 "algorithm": "sha256", 

243 "digest": "407822...", 

244 "name": "d0/f02" 

245 }, 

246 ... 

247 { 

248 "algorithm": "sha256", 

249 "digest": "912bcf...", 

250 "name": "f3" 

251 } 

252 ] 

253 } 

254 } 

255 ``` 

256 """ 

257 

258 def __init__(self, manifest: manifest.Manifest): 

259 """Builds an instance of this in-toto payload. 

260 

261 Args: 

262 manifest: the manifest to convert to signing payload. 

263 """ 

264 hasher = memory.SHA256() 

265 resources = [] 

266 for descriptor in manifest.resource_descriptors(): 

267 hasher.update(descriptor.digest.digest_value) 

268 resources.append( 

269 { 

270 "name": descriptor.identifier, 

271 "algorithm": descriptor.digest.algorithm, 

272 "digest": descriptor.digest.digest_hex, 

273 } 

274 ) 

275 

276 root_digest = {"sha256": hasher.compute().digest_hex} 

277 subject = statement.ResourceDescriptor( 

278 name=manifest.model_name, digest=root_digest 

279 ).pb 

280 

281 predicate = { 

282 "serialization": manifest.serialization_type, 

283 "resources": resources, 

284 # other properties can go here 

285 } 

286 

287 self.statement = statement.Statement( 

288 subjects=[subject], 

289 predicate_type=_PREDICATE_TYPE, 

290 predicate=predicate, 

291 ) 

292 

293 

294class Signature(metaclass=abc.ABCMeta): 

295 """Signature class, wrapping a sigstore bundle. 

296 

297 We only support sigstore bundle signature formats, but we need to have two 

298 separate classes for this given the need to support traditional signing as 

299 well as Sigstore one. One class wraps around `sigstore_models.Bundle` and 

300 the other around the bundle as defined by `sigstore_protobuf_specs`. 

301 """ 

302 

303 @abc.abstractmethod 

304 def write(self, path: pathlib.Path) -> None: 

305 """Writes the signature to disk, to the given path. 

306 

307 Args: 

308 path: The path to write the signature to. 

309 """ 

310 

311 @abc.abstractmethod 

312 def to_bytes(self) -> bytes: 

313 """Serializes the signature to bytes. 

314 

315 Returns the same Sigstore bundle content that `write` persists to 

316 disk, encoded as UTF-8 JSON. This lets callers in serverless or 

317 pipeline contexts obtain the signature in memory without touching the 

318 filesystem. 

319 

320 Returns: 

321 The serialized signature, as UTF-8 encoded bytes. 

322 """ 

323 

324 @classmethod 

325 @abc.abstractmethod 

326 def read(cls, path: pathlib.Path) -> Self: 

327 """Reads the signature from disk. 

328 

329 Does not perform any signature verification, except what is needed to 

330 parse the signature file. 

331 

332 Args: 

333 path: The path to read the signature from. 

334 

335 Returns: 

336 An instance of the class which can be passed to a `Verifier` for 

337 signature and integrity verification. 

338 """ 

339 

340 

341class Signer(metaclass=abc.ABCMeta): 

342 """Generic signer. 

343 

344 Each signer may implement its own mechanism for managing the key material. 

345 """ 

346 

347 @abc.abstractmethod 

348 def sign(self, payload: Payload) -> Signature: 

349 """Signs the provided signing payload. 

350 

351 Args: 

352 payload: The `Payload` instance that should be signed. 

353 

354 Returns: 

355 A valid signature. 

356 """ 

357 

358 

359class Verifier(metaclass=abc.ABCMeta): 

360 """Generic signature verifier. 

361 

362 Every subclass of `Verifier` is paired with a subclass of `Signer`. This is 

363 to ensure that they support the same signature formats as well as have 

364 similar key materials. 

365 

366 If the signature is valid, the payload is expanded to a `Manifest` instance 

367 which can then be used to check the model integrity. 

368 """ 

369 

370 def verify(self, signature: Signature) -> manifest.Manifest: 

371 """Verifies the signature. 

372 

373 Args: 

374 signature: The signature to verify. 

375 

376 Returns: 

377 A `manifest.Manifest` instance that represents the model. 

378 

379 Raises: 

380 ValueError: Signature verification fails. 

381 """ 

382 payload_type, payload = self._verify_signed_content(signature) 

383 

384 if payload_type != _IN_TOTO_JSON_PAYLOAD_TYPE: 

385 raise ValueError( 

386 f"Expected DSSE payload {_IN_TOTO_JSON_PAYLOAD_TYPE}, " 

387 f"but got {payload_type}" 

388 ) 

389 

390 payload = json.loads(payload) 

391 

392 if payload["_type"] != _IN_TOTO_STATEMENT_TYPE: 

393 raise ValueError( 

394 f"Expected in-toto {_IN_TOTO_STATEMENT_TYPE} payload, " 

395 f"but got {payload['_type']}" 

396 ) 

397 

398 return dsse_payload_to_manifest(payload) 

399 

400 @abc.abstractmethod 

401 def _verify_signed_content(self, signature: Signature) -> tuple[str, bytes]: 

402 """Verifies the signed content and extract payload type and payload. 

403 

404 Subclasses only need to implement this method. 

405 

406 Args: 

407 signature: The signature to verify. 

408 

409 Returns: 

410 A tuple containing the payload type and the payload (as a JSON 

411 object loaded to a dictionary). 

412 """