sigstore.dsse
Functionality for building and manipulating in-toto Statements and DSSE envelopes.
1# Copyright 2022 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""" 16Functionality for building and manipulating in-toto Statements and DSSE envelopes. 17""" 18 19from __future__ import annotations 20 21import logging 22from typing import Any, Dict, List, Literal, Optional, Union 23 24from cryptography.exceptions import InvalidSignature 25from cryptography.hazmat.primitives import hashes 26from cryptography.hazmat.primitives.asymmetric import ec 27from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError 28from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm 29from sigstore_protobuf_specs.io.intoto import Envelope as _Envelope 30from sigstore_protobuf_specs.io.intoto import Signature 31 32from sigstore.errors import Error, VerificationError 33from sigstore.hashes import Hashed 34 35_logger = logging.getLogger(__name__) 36 37Digest = Union[ 38 Literal["sha256"], 39 Literal["sha384"], 40 Literal["sha512"], 41 Literal["sha3_256"], 42 Literal["sha3_384"], 43 Literal["sha3_512"], 44] 45""" 46NOTE: in-toto's DigestSet contains all kinds of hash algorithms that 47we intentionally do not support. This model is limited to common members of the 48SHA-2 and SHA-3 family that are at least as strong as SHA-256. 49 50See: <https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md> 51""" 52 53DigestSet = RootModel[Dict[Digest, str]] 54""" 55An internal validation model for in-toto subject digest sets. 56""" 57 58 59class Subject(BaseModel): 60 """ 61 A single in-toto statement subject. 62 """ 63 64 name: Optional[StrictStr] 65 digest: DigestSet = Field(...) 66 67 68class _Statement(BaseModel): 69 """ 70 An internal validation model for in-toto statements. 71 """ 72 73 model_config = ConfigDict(populate_by_name=True) 74 75 type_: Literal["https://in-toto.io/Statement/v1"] = Field(..., alias="_type") 76 subjects: List[Subject] = Field(..., min_length=1, alias="subject") 77 predicate_type: StrictStr = Field(..., alias="predicateType") 78 predicate: Optional[Dict[str, Any]] = Field(None, alias="predicate") 79 80 81class Statement: 82 """ 83 Represents an in-toto statement. 84 85 This type deals with opaque bytes to ensure that the encoding does not 86 change, but Statements are internally checked for conformance against 87 the JSON object layout defined in the in-toto attestation spec. 88 89 See: <https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md> 90 """ 91 92 def __init__(self, contents: bytes | _Statement) -> None: 93 """ 94 Construct a new Statement. 95 96 This takes an opaque `bytes` containing the statement; use 97 `StatementBuilder` to manually construct an in-toto statement 98 from constituent pieces. 99 """ 100 if isinstance(contents, bytes): 101 self._contents = contents 102 try: 103 self._inner = _Statement.model_validate_json(contents) 104 except ValidationError: 105 raise Error("malformed in-toto statement") 106 else: 107 self._contents = contents.model_dump_json(by_alias=True).encode() 108 self._inner = contents 109 110 def _matches_digest(self, digest: Hashed) -> bool: 111 """ 112 Returns a boolean indicating whether this in-toto Statement contains a subject 113 matching the given digest. The subject's name is **not** checked. 114 115 No digests other than SHA256 are currently supported. 116 """ 117 if digest.algorithm != HashAlgorithm.SHA2_256: 118 raise VerificationError(f"unexpected digest algorithm: {digest.algorithm}") 119 120 for sub in self._inner.subjects: 121 sub_digest = sub.digest.root.get("sha256") 122 if sub_digest is None: 123 continue 124 if sub_digest == digest.digest.hex(): 125 return True 126 127 return False 128 129 def _pae(self) -> bytes: 130 """ 131 Construct the PAE encoding for this statement. 132 """ 133 134 return _pae(Envelope._TYPE, self._contents) 135 136 137class StatementBuilder: 138 """ 139 A builder-style API for constructing in-toto Statements. 140 """ 141 142 def __init__( 143 self, 144 subjects: Optional[List[Subject]] = None, 145 predicate_type: Optional[str] = None, 146 predicate: Optional[Dict[str, Any]] = None, 147 ): 148 """ 149 Create a new `StatementBuilder`. 150 """ 151 self._subjects = subjects or [] 152 self._predicate_type = predicate_type 153 self._predicate = predicate 154 155 def subjects(self, subjects: list[Subject]) -> StatementBuilder: 156 """ 157 Configure the subjects for this builder. 158 """ 159 self._subjects = subjects 160 return self 161 162 def predicate_type(self, predicate_type: str) -> StatementBuilder: 163 """ 164 Configure the predicate type for this builder. 165 """ 166 self._predicate_type = predicate_type 167 return self 168 169 def predicate(self, predicate: dict[str, Any]) -> StatementBuilder: 170 """ 171 Configure the predicate for this builder. 172 """ 173 self._predicate = predicate 174 return self 175 176 def build(self) -> Statement: 177 """ 178 Build a `Statement` from the builder's state. 179 """ 180 try: 181 stmt = _Statement( 182 type_="https://in-toto.io/Statement/v1", 183 subjects=self._subjects, 184 predicate_type=self._predicate_type, 185 predicate=self._predicate, 186 ) 187 except ValidationError as e: 188 raise Error(f"invalid statement: {e}") 189 190 return Statement(stmt) 191 192 193class Envelope: 194 """ 195 Represents a DSSE envelope. 196 197 This class cannot be constructed directly; you must use `sign` or `from_json`. 198 199 See: <https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md> 200 """ 201 202 _TYPE = "application/vnd.in-toto+json" 203 204 def __init__(self, inner: _Envelope) -> None: 205 """ 206 @private 207 """ 208 209 self._inner = inner 210 211 @classmethod 212 def _from_json(cls, contents: bytes | str) -> Envelope: 213 """Return a DSSE envelope from the given JSON representation.""" 214 inner = _Envelope().from_json(contents) 215 return cls(inner) 216 217 def to_json(self) -> str: 218 """ 219 Return a JSON string with this DSSE envelope's contents. 220 """ 221 return self._inner.to_json() 222 223 def __eq__(self, other: object) -> bool: 224 """Equality for DSSE envelopes.""" 225 226 if not isinstance(other, Envelope): 227 return NotImplemented 228 229 return self._inner == other._inner 230 231 232def _pae(type_: str, body: bytes) -> bytes: 233 """ 234 Compute the PAE encoding for the given `type_` and `body`. 235 """ 236 237 # See: 238 # https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md 239 # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md 240 pae = f"DSSEv1 {len(type_)} {type_} ".encode() 241 pae += b" ".join([str(len(body)).encode(), body]) 242 return pae 243 244 245def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: 246 """ 247 Sign for the given in-toto `Statement`, and encapsulate the resulting 248 signature in a DSSE `Envelope`. 249 """ 250 pae = stmt._pae() 251 _logger.debug(f"DSSE PAE: {pae!r}") 252 253 signature = key.sign(pae, ec.ECDSA(hashes.SHA256())) 254 return Envelope( 255 _Envelope( 256 payload=stmt._contents, 257 payload_type=Envelope._TYPE, 258 signatures=[Signature(sig=signature)], 259 ) 260 ) 261 262 263def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes: 264 """ 265 Verify the given in-toto `Envelope`, returning the verified inner payload. 266 267 This function does **not** check the envelope's payload type. The caller 268 is responsible for performing this check. 269 """ 270 271 pae = _pae(evp._inner.payload_type, evp._inner.payload) 272 273 nsigs = len(evp._inner.signatures) 274 if nsigs != 1: 275 raise VerificationError(f"DSSE: exactly 1 signature allowed, got {nsigs}") 276 277 signature = evp._inner.signatures[0].sig 278 279 try: 280 key.verify(signature, pae, ec.ECDSA(hashes.SHA256())) 281 except InvalidSignature: 282 raise VerificationError("DSSE: invalid signature") 283 284 return evp._inner.payload
NOTE: in-toto's DigestSet contains all kinds of hash algorithms that we intentionally do not support. This model is limited to common members of the SHA-2 and SHA-3 family that are at least as strong as SHA-256.
See: https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md
An internal validation model for in-toto subject digest sets.
60class Subject(BaseModel): 61 """ 62 A single in-toto statement subject. 63 """ 64 65 name: Optional[StrictStr] 66 digest: DigestSet = Field(...)
A single in-toto statement subject.
Configuration for the model, should be a dictionary conforming to [ConfigDict
][pydantic.config.ConfigDict].
Metadata about the fields defined on the model,
mapping of field names to [FieldInfo
][pydantic.fields.FieldInfo] objects.
This replaces Model.__fields__
from Pydantic V1.
A dictionary of computed field names and their corresponding ComputedFieldInfo
objects.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- dict
- json
- parse_obj
- parse_raw
- parse_file
- from_orm
- construct
- copy
- schema
- schema_json
- validate
- update_forward_refs
82class Statement: 83 """ 84 Represents an in-toto statement. 85 86 This type deals with opaque bytes to ensure that the encoding does not 87 change, but Statements are internally checked for conformance against 88 the JSON object layout defined in the in-toto attestation spec. 89 90 See: <https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md> 91 """ 92 93 def __init__(self, contents: bytes | _Statement) -> None: 94 """ 95 Construct a new Statement. 96 97 This takes an opaque `bytes` containing the statement; use 98 `StatementBuilder` to manually construct an in-toto statement 99 from constituent pieces. 100 """ 101 if isinstance(contents, bytes): 102 self._contents = contents 103 try: 104 self._inner = _Statement.model_validate_json(contents) 105 except ValidationError: 106 raise Error("malformed in-toto statement") 107 else: 108 self._contents = contents.model_dump_json(by_alias=True).encode() 109 self._inner = contents 110 111 def _matches_digest(self, digest: Hashed) -> bool: 112 """ 113 Returns a boolean indicating whether this in-toto Statement contains a subject 114 matching the given digest. The subject's name is **not** checked. 115 116 No digests other than SHA256 are currently supported. 117 """ 118 if digest.algorithm != HashAlgorithm.SHA2_256: 119 raise VerificationError(f"unexpected digest algorithm: {digest.algorithm}") 120 121 for sub in self._inner.subjects: 122 sub_digest = sub.digest.root.get("sha256") 123 if sub_digest is None: 124 continue 125 if sub_digest == digest.digest.hex(): 126 return True 127 128 return False 129 130 def _pae(self) -> bytes: 131 """ 132 Construct the PAE encoding for this statement. 133 """ 134 135 return _pae(Envelope._TYPE, self._contents)
Represents an in-toto statement.
This type deals with opaque bytes to ensure that the encoding does not change, but Statements are internally checked for conformance against the JSON object layout defined in the in-toto attestation spec.
See: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md
93 def __init__(self, contents: bytes | _Statement) -> None: 94 """ 95 Construct a new Statement. 96 97 This takes an opaque `bytes` containing the statement; use 98 `StatementBuilder` to manually construct an in-toto statement 99 from constituent pieces. 100 """ 101 if isinstance(contents, bytes): 102 self._contents = contents 103 try: 104 self._inner = _Statement.model_validate_json(contents) 105 except ValidationError: 106 raise Error("malformed in-toto statement") 107 else: 108 self._contents = contents.model_dump_json(by_alias=True).encode() 109 self._inner = contents
Construct a new Statement.
This takes an opaque bytes
containing the statement; use
StatementBuilder
to manually construct an in-toto statement
from constituent pieces.
138class StatementBuilder: 139 """ 140 A builder-style API for constructing in-toto Statements. 141 """ 142 143 def __init__( 144 self, 145 subjects: Optional[List[Subject]] = None, 146 predicate_type: Optional[str] = None, 147 predicate: Optional[Dict[str, Any]] = None, 148 ): 149 """ 150 Create a new `StatementBuilder`. 151 """ 152 self._subjects = subjects or [] 153 self._predicate_type = predicate_type 154 self._predicate = predicate 155 156 def subjects(self, subjects: list[Subject]) -> StatementBuilder: 157 """ 158 Configure the subjects for this builder. 159 """ 160 self._subjects = subjects 161 return self 162 163 def predicate_type(self, predicate_type: str) -> StatementBuilder: 164 """ 165 Configure the predicate type for this builder. 166 """ 167 self._predicate_type = predicate_type 168 return self 169 170 def predicate(self, predicate: dict[str, Any]) -> StatementBuilder: 171 """ 172 Configure the predicate for this builder. 173 """ 174 self._predicate = predicate 175 return self 176 177 def build(self) -> Statement: 178 """ 179 Build a `Statement` from the builder's state. 180 """ 181 try: 182 stmt = _Statement( 183 type_="https://in-toto.io/Statement/v1", 184 subjects=self._subjects, 185 predicate_type=self._predicate_type, 186 predicate=self._predicate, 187 ) 188 except ValidationError as e: 189 raise Error(f"invalid statement: {e}") 190 191 return Statement(stmt)
A builder-style API for constructing in-toto Statements.
143 def __init__( 144 self, 145 subjects: Optional[List[Subject]] = None, 146 predicate_type: Optional[str] = None, 147 predicate: Optional[Dict[str, Any]] = None, 148 ): 149 """ 150 Create a new `StatementBuilder`. 151 """ 152 self._subjects = subjects or [] 153 self._predicate_type = predicate_type 154 self._predicate = predicate
Create a new StatementBuilder
.
156 def subjects(self, subjects: list[Subject]) -> StatementBuilder: 157 """ 158 Configure the subjects for this builder. 159 """ 160 self._subjects = subjects 161 return self
Configure the subjects for this builder.
163 def predicate_type(self, predicate_type: str) -> StatementBuilder: 164 """ 165 Configure the predicate type for this builder. 166 """ 167 self._predicate_type = predicate_type 168 return self
Configure the predicate type for this builder.
170 def predicate(self, predicate: dict[str, Any]) -> StatementBuilder: 171 """ 172 Configure the predicate for this builder. 173 """ 174 self._predicate = predicate 175 return self
Configure the predicate for this builder.
177 def build(self) -> Statement: 178 """ 179 Build a `Statement` from the builder's state. 180 """ 181 try: 182 stmt = _Statement( 183 type_="https://in-toto.io/Statement/v1", 184 subjects=self._subjects, 185 predicate_type=self._predicate_type, 186 predicate=self._predicate, 187 ) 188 except ValidationError as e: 189 raise Error(f"invalid statement: {e}") 190 191 return Statement(stmt)
Build a Statement
from the builder's state.
194class Envelope: 195 """ 196 Represents a DSSE envelope. 197 198 This class cannot be constructed directly; you must use `sign` or `from_json`. 199 200 See: <https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md> 201 """ 202 203 _TYPE = "application/vnd.in-toto+json" 204 205 def __init__(self, inner: _Envelope) -> None: 206 """ 207 @private 208 """ 209 210 self._inner = inner 211 212 @classmethod 213 def _from_json(cls, contents: bytes | str) -> Envelope: 214 """Return a DSSE envelope from the given JSON representation.""" 215 inner = _Envelope().from_json(contents) 216 return cls(inner) 217 218 def to_json(self) -> str: 219 """ 220 Return a JSON string with this DSSE envelope's contents. 221 """ 222 return self._inner.to_json() 223 224 def __eq__(self, other: object) -> bool: 225 """Equality for DSSE envelopes.""" 226 227 if not isinstance(other, Envelope): 228 return NotImplemented 229 230 return self._inner == other._inner
Represents a DSSE envelope.
This class cannot be constructed directly; you must use sign
or from_json
.
See: https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md