diff --git a/CHANGELOG.md b/CHANGELOG.md index 955f538b..661757ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,9 @@ All versions prior to 0.9.0 are untracked. * SigningConfig now has methods that return actual clients (like `RekorClient`) instead of just URLs. The returned clients are also filtered according to SigningConfig contents. [#1407](https://github.com/sigstore/sigstore-python/pull/1407) + * The ClientTrustConfig class has been moved from the private _internal package to a public + module (sigstore.models). This change formally adds the class to the project's public API, + making it available for use in other projects. [#1496](https://github.com/sigstore/sigstore-python/pull/1496) * `--trust-config` now requires a file with SigningConfig v0.2, and is able to fully configure the used Sigstore instance [#1358]/(https://github.com/sigstore/sigstore-python/pull/1358) * By default (when `--trust-config` is not used) the whole trust configuration now diff --git a/sigstore/_cli.py b/sigstore/_cli.py index d63cbd1d..8b5685f0 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -38,7 +38,6 @@ from sigstore._internal.fulcio.client import ExpiredCertificate from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient -from sigstore._internal.trust import ClientTrustConfig from sigstore._utils import sha256_digest from sigstore.dsse import StatementBuilder, Subject from sigstore.dsse._predicate import ( @@ -48,7 +47,7 @@ ) from sigstore.errors import Error, VerificationError from sigstore.hashes import Hashed -from sigstore.models import Bundle, InvalidBundle +from sigstore.models import Bundle, ClientTrustConfig, InvalidBundle from sigstore.oidc import ( ExpiredIdentity, IdentityToken, diff --git a/sigstore/_internal/trust.py b/sigstore/_internal/trust.py index e4da68de..3fbe080a 100644 --- a/sigstore/_internal/trust.py +++ b/sigstore/_internal/trust.py @@ -40,17 +40,14 @@ from sigstore._internal.fulcio.client import FulcioClient from sigstore._internal.rekor import RekorLogSubmitter -from sigstore._internal.rekor.client import RekorClient -from sigstore._internal.rekor.client_v2 import RekorV2Client from sigstore._internal.timestamp import TimestampAuthorityClient -from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater from sigstore._utils import ( KeyID, PublicKey, key_id, load_der_public_key, ) -from sigstore.errors import Error, MetadataError, TUFError, VerificationError +from sigstore.errors import Error, MetadataError, VerificationError # Versions supported by this client REKOR_VERSIONS = [1, 2] @@ -407,6 +404,9 @@ def get_tlogs(self) -> list[RekorLogSubmitter]: """ Returns the rekor transparency log clients to sign with. """ + from sigstore._internal.rekor.client import RekorClient + from sigstore._internal.rekor.client_v2 import RekorV2Client + result: list[RekorLogSubmitter] = [] for tlog in self._tlogs: if tlog.major_api_version == 1: @@ -547,102 +547,3 @@ def get_timestamp_authorities(self) -> list[CertificateAuthority]: for cert_chain in self._inner.timestamp_authorities ] return certificate_authorities - - -class ClientTrustConfig: - """ - Represents a Sigstore client's trust configuration, including a root of trust. - """ - - class ClientTrustConfigType(str, Enum): - """ - Known Sigstore client trust config media types. - """ - - CONFIG_0_1 = "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json" - - def __str__(self) -> str: - """Returns the variant's string value.""" - return self.value - - @classmethod - def from_json(cls, raw: str) -> ClientTrustConfig: - """ - Deserialize the given client trust config. - """ - inner = trustroot_v1.ClientTrustConfig.from_json(raw) - return cls(inner) - - @classmethod - def production( - cls, - offline: bool = False, - ) -> ClientTrustConfig: - """Create new trust config from Sigstore production TUF repository. - - If `offline`, will use data in local TUF cache. Otherwise will - update the data from remote TUF repository. - """ - return cls.from_tuf(DEFAULT_TUF_URL, offline) - - @classmethod - def staging( - cls, - offline: bool = False, - ) -> ClientTrustConfig: - """Create new trust config from Sigstore staging TUF repository. - - If `offline`, will use data in local TUF cache. Otherwise will - update the data from remote TUF repository. - """ - return cls.from_tuf(STAGING_TUF_URL, offline) - - @classmethod - def from_tuf( - cls, - url: str, - offline: bool = False, - ) -> ClientTrustConfig: - """Create a new trust config from a TUF repository. - - If `offline`, will use data in local TUF cache. Otherwise will - update the trust config from remote TUF repository. - """ - updater = TrustUpdater(url, offline) - - tr_path = updater.get_trusted_root_path() - inner_tr = trustroot_v1.TrustedRoot.from_json(Path(tr_path).read_bytes()) - - try: - sc_path = updater.get_signing_config_path() - inner_sc = trustroot_v1.SigningConfig.from_json(Path(sc_path).read_bytes()) - except TUFError as e: - raise e - - return cls( - trustroot_v1.ClientTrustConfig( - media_type=ClientTrustConfig.ClientTrustConfigType.CONFIG_0_1.value, - trusted_root=inner_tr, - signing_config=inner_sc, - ) - ) - - def __init__(self, inner: trustroot_v1.ClientTrustConfig) -> None: - """ - @api private - """ - self._inner = inner - - @property - def trusted_root(self) -> TrustedRoot: - """ - Return the interior root of trust, as a `TrustedRoot`. - """ - return TrustedRoot(self._inner.trusted_root) - - @property - def signing_config(self) -> SigningConfig: - """ - Return the interior root of trust, as a `SigningConfig`. - """ - return SigningConfig(self._inner.signing_config) diff --git a/sigstore/models.py b/sigstore/models.py index 71b1c8bf..545e73fb 100644 --- a/sigstore/models.py +++ b/sigstore/models.py @@ -58,6 +58,13 @@ if typing.TYPE_CHECKING: from sigstore._internal.trust import RekorKeyring +from pathlib import Path + +from sigstore_models.trustroot import v1 as trustroot_v1 + +from sigstore._internal.trust import SigningConfig, TrustedRoot +from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater +from sigstore.errors import TUFError _logger = logging.getLogger(__name__) @@ -593,3 +600,102 @@ def _from_parts( ) return cls(inner) + + +class ClientTrustConfig: + """ + Represents a Sigstore client's trust configuration, including a root of trust. + """ + + class ClientTrustConfigType(str, Enum): + """ + Known Sigstore client trust config media types. + """ + + CONFIG_0_1 = "application/vnd.dev.sigstore.clienttrustconfig.v0.1+json" + + def __str__(self) -> str: + """Returns the variant's string value.""" + return self.value + + @classmethod + def from_json(cls, raw: str) -> ClientTrustConfig: + """ + Deserialize the given client trust config. + """ + inner = trustroot_v1.ClientTrustConfig.from_json(raw) + return cls(inner) + + @classmethod + def production( + cls, + offline: bool = False, + ) -> ClientTrustConfig: + """Create new trust config from Sigstore production TUF repository. + + If `offline`, will use data in local TUF cache. Otherwise will + update the data from remote TUF repository. + """ + return cls.from_tuf(DEFAULT_TUF_URL, offline) + + @classmethod + def staging( + cls, + offline: bool = False, + ) -> ClientTrustConfig: + """Create new trust config from Sigstore staging TUF repository. + + If `offline`, will use data in local TUF cache. Otherwise will + update the data from remote TUF repository. + """ + return cls.from_tuf(STAGING_TUF_URL, offline) + + @classmethod + def from_tuf( + cls, + url: str, + offline: bool = False, + ) -> ClientTrustConfig: + """Create a new trust config from a TUF repository. + + If `offline`, will use data in local TUF cache. Otherwise will + update the trust config from remote TUF repository. + """ + updater = TrustUpdater(url, offline) + + tr_path = updater.get_trusted_root_path() + inner_tr = trustroot_v1.TrustedRoot.from_json(Path(tr_path).read_bytes()) + + try: + sc_path = updater.get_signing_config_path() + inner_sc = trustroot_v1.SigningConfig.from_json(Path(sc_path).read_bytes()) + except TUFError as e: + raise e + + return cls( + trustroot_v1.ClientTrustConfig( + media_type=ClientTrustConfig.ClientTrustConfigType.CONFIG_0_1.value, + trusted_root=inner_tr, + signing_config=inner_sc, + ) + ) + + def __init__(self, inner: trustroot_v1.ClientTrustConfig) -> None: + """ + @api private + """ + self._inner = inner + + @property + def trusted_root(self) -> TrustedRoot: + """ + Return the interior root of trust, as a `TrustedRoot`. + """ + return TrustedRoot(self._inner.trusted_root) + + @property + def signing_config(self) -> SigningConfig: + """ + Return the interior root of trust, as a `SigningConfig`. + """ + return SigningConfig(self._inner.signing_config) diff --git a/sigstore/sign.py b/sigstore/sign.py index 0aa62333..5436336b 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -59,9 +59,9 @@ from sigstore._internal.rekor import EntryRequestBody, RekorLogSubmitter from sigstore._internal.sct import verify_sct from sigstore._internal.timestamp import TimestampAuthorityClient, TimestampError -from sigstore._internal.trust import ClientTrustConfig, KeyringPurpose, TrustedRoot +from sigstore._internal.trust import KeyringPurpose, TrustedRoot from sigstore._utils import sha256_digest -from sigstore.models import Bundle +from sigstore.models import Bundle, ClientTrustConfig from sigstore.oidc import ExpiredIdentity, IdentityToken _logger = logging.getLogger(__name__) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 52533514..e832fd62 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -49,11 +49,11 @@ verify_sct, ) from sigstore._internal.timestamp import TimestampSource, TimestampVerificationResult -from sigstore._internal.trust import ClientTrustConfig, KeyringPurpose, TrustedRoot +from sigstore._internal.trust import KeyringPurpose, TrustedRoot from sigstore._utils import base64_encode_pem_cert, sha256_digest from sigstore.errors import VerificationError from sigstore.hashes import Hashed -from sigstore.models import Bundle +from sigstore.models import Bundle, ClientTrustConfig from sigstore.verify.policy import VerificationPolicy _logger = logging.getLogger(__name__) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 43dcd002..f5ddc587 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -38,9 +38,8 @@ from sigstore._internal import tuf from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient -from sigstore._internal.trust import ClientTrustConfig from sigstore._utils import sha256_digest -from sigstore.models import Bundle +from sigstore.models import Bundle, ClientTrustConfig from sigstore.oidc import IdentityToken from sigstore.sign import SigningContext from sigstore.verify.verifier import Verifier diff --git a/test/unit/internal/test_trust.py b/test/unit/internal/test_trust.py index 26b7278e..2d493f8d 100644 --- a/test/unit/internal/test_trust.py +++ b/test/unit/internal/test_trust.py @@ -32,7 +32,6 @@ from sigstore._internal.timestamp import TimestampAuthorityClient from sigstore._internal.trust import ( CertificateAuthority, - ClientTrustConfig, KeyringPurpose, SigningConfig, TrustedRoot, @@ -40,6 +39,7 @@ ) from sigstore._utils import load_pem_public_key from sigstore.errors import Error +from sigstore.models import ClientTrustConfig # Test data for TestSigningcconfig _service_v1_op1 = Service(url="url1", major_api_version=1, operator="op1") diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 006c571b..423661df 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -21,10 +21,10 @@ import sigstore.oidc from sigstore._internal.timestamp import TimestampAuthorityClient -from sigstore._internal.trust import ClientTrustConfig from sigstore.dsse import StatementBuilder, Subject from sigstore.errors import VerificationError from sigstore.hashes import Hashed +from sigstore.models import ClientTrustConfig from sigstore.sign import SigningContext from sigstore.verify.policy import UnsafeNoOp