From 3af8dca07901f7a018b4c93cb082e920fb8c10a8 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Fri, 13 Sep 2024 21:51:09 +0200 Subject: [PATCH] Attestation CLI command improvements (#1121) --- sigstore/_cli.py | 47 ++++++++++++++++++++----------------- sigstore/dsse/_predicate.py | 14 +++++++---- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 844aa2167..61cef877b 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -16,12 +16,13 @@ import argparse import base64 +import json import logging import os import sys from dataclasses import dataclass from pathlib import Path -from typing import Dict, NoReturn, Optional, TextIO, Union +from typing import Any, Dict, NoReturn, Optional, TextIO, Union from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509 import load_pem_x509_certificate @@ -41,10 +42,7 @@ from sigstore._utils import sha256_digest from sigstore.dsse import StatementBuilder, Subject from sigstore.dsse._predicate import ( - SUPPORTED_PREDICATE_TYPES, - Predicate, - PREDICATE_TYPE_SLSA_v0_2, - PREDICATE_TYPE_SLSA_v1_0, + PredicateType, SLSAPredicateV0_2, SLSAPredicateV1_0, ) @@ -277,10 +275,10 @@ def _parser() -> argparse.ArgumentParser: dsse_options.add_argument( "--predicate-type", metavar="TYPE", - choices=SUPPORTED_PREDICATE_TYPES, - type=str, + choices=list(PredicateType), + type=PredicateType, required=True, - help=f"Specify a predicate type ({', '.join(SUPPORTED_PREDICATE_TYPES)})", + help=f"Specify a predicate type ({', '.join(list(PredicateType))})", ) oidc_options = attest.add_argument_group("OpenID Connect options") @@ -591,7 +589,7 @@ def main(args: list[str] | None = None) -> None: def _sign_common( - args: argparse.Namespace, output_map: OutputMap, predicate: Predicate | None + args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None ) -> None: """ Signing logic for both `sigstore sign` and `sigstore attest` @@ -647,13 +645,7 @@ def _sign_common( statement_builder = StatementBuilder( subjects=[subject], predicate_type=predicate_type, - # Dump by alias because while our Python models uses snake_case, - # the spec uses camelCase, which we have aliases for. - # We also exclude fields set to None, since it's how we model - # optional fields that were not set. - predicate=predicate.model_dump( - by_alias=True, exclude_none=True - ), + predicate=predicate, ) result = signer.sign_dsse(statement_builder.build()) except ExpiredIdentity as exp_identity: @@ -704,16 +696,23 @@ def _attest(args: argparse.Namespace) -> None: try: with open(predicate_path, "r") as f: - if args.predicate_type == PREDICATE_TYPE_SLSA_v0_2: - predicate: Predicate = SLSAPredicateV0_2.model_validate_json(f.read()) - elif args.predicate_type == PREDICATE_TYPE_SLSA_v1_0: - predicate = SLSAPredicateV1_0.model_validate_json(f.read()) + predicate = json.load(f) + # We do a basic sanity check using our Pydantic models to see if the + # contents of the predicate file match the specified predicate type. + # Since most of the predicate fields are optional, this only checks that + # the fields that are present and correctly spelled have the expected + # type. + if args.predicate_type == PredicateType.SLSA_v0_2: + SLSAPredicateV0_2.model_validate(predicate) + elif args.predicate_type == PredicateType.SLSA_v1_0: + SLSAPredicateV1_0.model_validate(predicate) else: _invalid_arguments( args, - f'Unsupported predicate type "{args.predicate_type}". Predicate type must be one of: {SUPPORTED_PREDICATE_TYPES}', + f'Unsupported predicate type "{args.predicate_type}". Predicate type must be one of: {list(PredicateType)}', ) - except ValidationError as e: + + except (ValidationError, json.JSONDecodeError) as e: _invalid_arguments( args, f'Unable to parse predicate of type "{args.predicate_type}": {e}' ) @@ -738,6 +737,10 @@ def _attest(args: argparse.Namespace) -> None: ) output_map[file] = SigningOutputs(bundle=bundle) + # We sign the contents of the predicate file, rather than signing the Pydantic + # model's JSON dump. This is because doing a JSON -> Model -> JSON roundtrip might + # change the original predicate if it doesn't match exactly our Pydantic model + # (e.g.: if it has extra fields). _sign_common(args, output_map=output_map, predicate=predicate) diff --git a/sigstore/dsse/_predicate.py b/sigstore/dsse/_predicate.py index 0e6289942..77d2423f0 100644 --- a/sigstore/dsse/_predicate.py +++ b/sigstore/dsse/_predicate.py @@ -16,6 +16,7 @@ Models for the predicates used in in-toto statements """ +import enum from typing import Any, Dict, List, Literal, Optional, TypeVar, Union from pydantic import ( @@ -30,10 +31,15 @@ from sigstore.dsse import Digest -PREDICATE_TYPE_SLSA_v0_2 = "https://slsa.dev/provenance/v0.2" -PREDICATE_TYPE_SLSA_v1_0 = "https://slsa.dev/provenance/v1" -SUPPORTED_PREDICATE_TYPES = [PREDICATE_TYPE_SLSA_v0_2, PREDICATE_TYPE_SLSA_v1_0] +class PredicateType(str, enum.Enum): + """ + Currently supported predicate types + """ + + SLSA_v0_2 = "https://slsa.dev/provenance/v0.2" + SLSA_v1_0 = "https://slsa.dev/provenance/v1" + # Common models SourceDigest = Union[Literal["sha1"], Literal["gitCommit"]] @@ -60,7 +66,7 @@ class _SLSAConfigBase(BaseModel): Base class used to configure the models """ - model_config = ConfigDict(alias_generator=to_camel) + model_config = ConfigDict(alias_generator=to_camel, extra="forbid") # Models for SLSA Provenance v0.2