From a7598dfcc01108562e81544fbebd17e962fa3d63 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 10:32:43 +0200 Subject: [PATCH 01/38] tuf: Add initial TUF trust root updater TrustUpdater can be used to fetch specific trust roots: * Currently supports fetching * ctfe keys and * the rekor key * Caches target files in ~/.cache/sigstore-python/ * Stores metadata in ~/.local/share/sigstore-python/ * Expects to either * find the metadata _for the given URL_ in metadata store * (for prod and stage only) find the boostrap root.json in sigstore/_store The "API" that TrustUpdater provides is not meant to be final: it is the minimal one that should fulfill current needs. Nothing uses the TrustUpdater yet, but it's testable: >>> from sigstore._tuf import TrustUpdater, DEFAULT_TUF_URL >>> updater = TrustUpdater(DEFAULT_TUF_URL) >>> rekor_key_bytes = updater.get_rekor_key() Co-authored-by: Joshua Lock Co-authored-by: wxjdsr Signed-off-by: Jussi Kukkonen --- pyproject.toml | 1 + sigstore/_internal/tuf.py | 120 +++++++++++++++++++++++++++++ sigstore/_store/root.json | 156 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 sigstore/_internal/tuf.py create mode 100644 sigstore/_store/root.json diff --git a/pyproject.toml b/pyproject.toml index 09877e63b..9eace7e0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "pyOpenSSL >= 22.0.0", "requests", "securesystemslib", + "tuf >= 2.0.0", ] requires-python = ">=3.7" diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py new file mode 100644 index 000000000..cc27e4484 --- /dev/null +++ b/sigstore/_internal/tuf.py @@ -0,0 +1,120 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import shutil +from importlib import resources +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from urllib import parse + +from tuf.api.metadata import Metadata, Timestamp +from tuf.ngclient import Updater + +logger = logging.getLogger(__name__) + +DEFAULT_TUF_URL = "https://sigstore-tuf-root.storage.googleapis.com/" +STAGING_TUF_URL = "https://sigstore-preprod-tuf-root.storage.googleapis.com/" + + +def _get_dirs(url: str) -> Tuple[Path, Path]: + """Return metadata dir and target cache dir for URL""" + # NOTE: this is not great for windows: should maybe depend on appdirs? + # TODO: there should be URL normalization if URLs come from user + dir = parse.quote(url, safe="") + md_dir = Path.home() / ".local" / "share" / "sigstore-python" / "tuf" / dir + targets_dir = Path.home() / ".cache" / "sigstore-python" / "tuf" / dir + return md_dir, targets_dir + + +class TrustUpdater: + def __init__(self, url: str) -> None: + self._repo_url = url + self._updater = None + + self._metadata_dir, self._targets_dir = _get_dirs(url) + + # intialize metadata dir + tuf_root = self._metadata_dir / "root.json" + if not tuf_root.exists(): + if self._repo_url not in [DEFAULT_TUF_URL, STAGING_TUF_URL]: + raise Exception(f"TUF root not found in {tuf_root}") + + self._metadata_dir.mkdir(parents=True, exist_ok=True) + with resources.path("sigstore._store", "root.json") as res: + shutil.copy2(res, self._metadata_dir) + + # intialize targets cache dir + # TODO: Pre-populate with any targets we ship with sources + self._targets_dir.mkdir(parents=True, exist_ok=True) + + # this metadata refresh could be done lazily but currently that + # is not needed (if TrustUpdater is created it is always used) + self._setup() + + logger.debug("TUF metadata: %s", self._metadata_dir) + logger.debug("TUF targets cache: %s", self._targets_dir) + + @classmethod + def production(cls) -> "TrustUpdater": + return cls(DEFAULT_TUF_URL) + + @classmethod + def staging(cls) -> "TrustUpdater": + return cls(STAGING_TUF_URL) + + def _setup(self) -> None: + """Initialize and update the toplevel TUF metadata""" + self._updater = Updater( + metadata_dir=str(self._metadata_dir), + metadata_base_url=f"{self._repo_url}", + target_base_url=f"{self._repo_url}targets/", + target_dir=str(self._targets_dir), + ) + + # NOTE: we would like to avoid refresh if the toplevel metadata is valid. + # https://github.com/theupdateframework/python-tuf/issues/2225 + self._updater.refresh() + + def get_ctfe_keys(self) -> List[bytes]: + """Return the active CTFE public keys contents""" + ctfes = [] + targets = self._updater._trusted_set.targets.signed.targets + for target_info in targets.values(): + custom = target_info.unrecognized_fields["custom"]["sigstore"] + if custom["status"] == "Active" and custom["usage"] == "CTFE": + path = self._updater.find_cached_target(target_info) + if path is None: + path = self._updater.download_target(target_info) + with open(path, "rb") as f: + ctfes.append(f.read()) + + if not ctfes: + raise Exception("CTFE keys not found in TUF metadata") + + return ctfes + + def get_rekor_key(self) -> bytes: + """Return the rekor public key content""" + targets = self._updater._trusted_set.targets.signed.targets + for target, target_info in targets.items(): + custom = target_info.unrecognized_fields["custom"]["sigstore"] + if custom["status"] == "Active" and custom["usage"] == "Rekor": + path = self._updater.find_cached_target(target_info) + if path is None: + path = self._updater.download_target(target_info) + with open(path, "rb") as f: + return f.read() + + raise Exception("Rekor key not found in TUF metadata") diff --git a/sigstore/_store/root.json b/sigstore/_store/root.json new file mode 100644 index 000000000..38f80f940 --- /dev/null +++ b/sigstore/_store/root.json @@ -0,0 +1,156 @@ +{ + "signed": { + "_type": "root", + "spec_version": "1.0", + "version": 5, + "expires": "2023-04-18T18:13:43Z", + "keys": { + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n" + } + }, + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n" + } + }, + "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELrWvNt94v4R085ELeeCMxHp7PldF\n0/T1GxukUh2ODuggLGJE0pc1e8CSBf6CS91Fwo9FUOuRsjBUld+VqSyCdQ==\n-----END PUBLIC KEY-----\n" + } + }, + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n" + } + }, + "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n" + } + }, + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n" + } + }, + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEy8XKsmhBYDI8Jc0GwzBxeKax0cm5\nSTKEU65HPFunUn41sT8pi0FjM4IkHz/YUmwmLUO0Wt7lxhj6BkLIK4qYAw==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": { + "root": { + "keyids": [ + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de" + ], + "threshold": 3 + }, + "snapshot": { + "keyids": [ + "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de" + ], + "threshold": 3 + }, + "timestamp": { + "keyids": [ + "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a" + ], + "threshold": 1 + } + }, + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "sig": "3045022100fc1c2be509ce50ea917bbad1d9efe9d96c8c2ebea04af2717aa3d9c6fe617a75022012eef282a19f2d8bd4818aa333ef48a06489f49d4d34a20b8fe8fc867bb25a7a" + }, + { + "keyid": "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "sig": "30450221008a4392ae5057fc00778b651e61fea244766a4ae58db84d9f1d3810720ab0f3b702207c49e59e8031318caf02252ecea1281cecc1e5986c309a9cef61f455ecf7165d" + }, + { + "keyid": "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "sig": "3046022100da1b8dc5d53aaffbbfac98de3e23ee2d2ad3446a7bed09fac0f88bae19be2587022100b681c046afc3919097dfe794e0d819be891e2e850aade315bec06b0c4dea221b" + }, + { + "keyid": "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de", + "sig": "3046022100b534e0030e1b271133ecfbdf3ba9fbf3becb3689abea079a2150afbb63cdb7c70221008c39a718fd9495f249b4ab8788d5b9dc269f0868dbe38b272f48207359d3ded9" + }, + { + "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", + "sig": "3045022100fc1c2be509ce50ea917bbad1d9efe9d96c8c2ebea04af2717aa3d9c6fe617a75022012eef282a19f2d8bd4818aa333ef48a06489f49d4d34a20b8fe8fc867bb25a7a" + }, + { + "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", + "sig": "30450221008a4392ae5057fc00778b651e61fea244766a4ae58db84d9f1d3810720ab0f3b702207c49e59e8031318caf02252ecea1281cecc1e5986c309a9cef61f455ecf7165d" + }, + { + "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", + "sig": "3046022100da1b8dc5d53aaffbbfac98de3e23ee2d2ad3446a7bed09fac0f88bae19be2587022100b681c046afc3919097dfe794e0d819be891e2e850aade315bec06b0c4dea221b" + }, + { + "keyid": "75e867ab10e121fdef32094af634707f43ddd79c6bab8ad6c5ab9f03f4ea8c90", + "sig": "3046022100b534e0030e1b271133ecfbdf3ba9fbf3becb3689abea079a2150afbb63cdb7c70221008c39a718fd9495f249b4ab8788d5b9dc269f0868dbe38b272f48207359d3ded9" + } + ] +} \ No newline at end of file From e6d392e50b8ea52f38eb9d661c656e8a4b840b7a Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 12:56:13 +0200 Subject: [PATCH 02/38] Rekor: Refactor CTKeyring, Use TUF in prod/staging CTKeyring: * Take bytes as constructore input: this makes it easier to feed things from either CLI arguments or the TUF trust updater. * Remove tests that no longer make sense. The prod/staging contant should still be tested but TUF is now used in the same flows: Unsure how to best test this. Use TUF to find the CTFE and rekor key when using "production" or "staging". Note that "staging" is currently untested: I am not sure even the URL makes sense. Signed-off-by: Jussi Kukkonen Signed-off-by: Jussi Kukkonen --- sigstore/_cli.py | 2 +- sigstore/_internal/ctfe.py | 45 +++--------------------------- sigstore/_internal/rekor/client.py | 21 +++++++------- test/unit/internal/test_ctfe.py | 42 ++-------------------------- 4 files changed, 18 insertions(+), 92 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 1dc9562a3..2deef3c22 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -427,7 +427,7 @@ def _sign(args: argparse.Namespace) -> None: elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL: signer = Signer.production() else: - ct_keyring = CTKeyring([load_pem_public_key(args.ctfe_pem.read())]) + ct_keyring = CTKeyring([args.ctfe_pem.read()]) signer = Signer( fulcio=FulcioClient(args.fulcio_url), rekor=RekorClient( diff --git a/sigstore/_internal/ctfe.py b/sigstore/_internal/ctfe.py index a1fcfdd00..e0eaf0e32 100644 --- a/sigstore/_internal/ctfe.py +++ b/sigstore/_internal/ctfe.py @@ -25,12 +25,7 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, rsa -from sigstore._utils import ( - PublicKey, - key_id, - load_pem_public_key, - read_embedded, -) +from sigstore._utils import key_id, load_pem_public_key class CTKeyringError(Exception): @@ -58,48 +53,16 @@ class CTKeyring: This structure exists to facilitate key rotation in a CT log. """ - def __init__(self, keys: List[PublicKey] = []): + def __init__(self, keys: List[bytes] = []): """ Create a new `CTKeyring`, with `keys` as the initial set of signing keys. """ self._keyring = {} - for key in keys: + for key_bytes in keys: + key = load_pem_public_key(key_bytes) self._keyring[key_id(key)] = key - @classmethod - def staging(cls) -> CTKeyring: - """ - Returns a `CTKeyring` instance capable of verifying SCTs from - Sigstore's staging deployment. - """ - keyring = cls() - keyring._add_resource("ctfe.staging.pub") - keyring._add_resource("ctfe_2022.staging.pub") - keyring._add_resource("ctfe_2022.2.staging.pub") - - return keyring - - @classmethod - def production(cls) -> CTKeyring: - """ - Returns a `CTKeyring` instance capable of verifying SCTs from - Sigstore's production deployment. - """ - keyring = cls() - keyring._add_resource("ctfe.pub") - keyring._add_resource("ctfe_2022.pub") - - return keyring - - def _add_resource(self, name: str) -> None: - """ - Adds a key to the current keyring, as identified by its - resource name under `sigstore._store`. - """ - key_pem = read_embedded(name) - self.add(key_pem) - def add(self, key_pem: bytes) -> None: """ Adds a PEM-encoded key to the current keyring. diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 378bdd639..10b79748b 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -33,6 +33,7 @@ from securesystemslib.formats import encode_canonical from sigstore._internal.ctfe import CTKeyring +from sigstore._internal.tuf import TrustUpdater from sigstore._utils import base64_encode_pem_cert, read_embedded logger = logging.getLogger(__name__) @@ -40,12 +41,6 @@ DEFAULT_REKOR_URL = "https://rekor.sigstore.dev" STAGING_REKOR_URL = "https://rekor.sigstage.dev" -_DEFAULT_REKOR_ROOT_PUBKEY = read_embedded("rekor.pub") -_STAGING_REKOR_ROOT_PUBKEY = read_embedded("rekor.staging.pub") - -_DEFAULT_REKOR_CTFE_PUBKEY = read_embedded("ctfe.pub") -_STAGING_REKOR_CTFE_PUBKEY = read_embedded("ctfe.staging.pub") - class RekorBundle(BaseModel): """ @@ -412,13 +407,19 @@ def __del__(self) -> None: @classmethod def production(cls) -> RekorClient: - return cls( - DEFAULT_REKOR_URL, _DEFAULT_REKOR_ROOT_PUBKEY, CTKeyring.production() - ) + updater = TrustUpdater.production() + rekor_key = updater.get_rekor_key() + ctfe_keys = updater.get_ctfe_keys() + + return cls(DEFAULT_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) @classmethod def staging(cls) -> RekorClient: - return cls(STAGING_REKOR_URL, _STAGING_REKOR_ROOT_PUBKEY, CTKeyring.staging()) + updater = TrustUpdater.staging() + rekor_key = updater.get_rekor_key() + ctfe_keys = updater.get_ctfe_keys() + + return cls(STAGING_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) @property def log(self) -> RekorLog: diff --git a/test/unit/internal/test_ctfe.py b/test/unit/internal/test_ctfe.py index 031fe4a95..f68c52242 100644 --- a/test/unit/internal/test_ctfe.py +++ b/test/unit/internal/test_ctfe.py @@ -25,36 +25,10 @@ class TestCTKeyring: def test_keyring_init(self): - pubkey = pretend.stub( - public_bytes=pretend.call_recorder(lambda encoding, format: bytes(0)) - ) - ctkeyring = CTKeyring([pubkey]) + keybytes = b"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu\ndDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\n-----END PUBLIC KEY-----" + ctkeyring = CTKeyring([keybytes]) assert len(ctkeyring._keyring) == 1 - def test_keyring_cardinalities(self): - production = CTKeyring.production() - staging = CTKeyring.staging() - - assert len(production._keyring) == 2 - assert len(staging._keyring) == 3 - - def test_production_staging_both_initialize(self): - keyrings = [CTKeyring.production(), CTKeyring.staging()] - for keyring in keyrings: - assert keyring is not None - - def test_production_staging_keyrings_are_disjoint(self): - production = CTKeyring.production() - staging = CTKeyring.staging() - - production_key_ids = production._keyring.keys() - staging_key_ids = staging._keyring.keys() - - # The key IDs (and therefore keys) in the production and staging instances - # should never overlap. Overlapping would imply loading keys intended - # for the wrong instance. - assert production_key_ids.isdisjoint(staging_key_ids) - def test_verify_fail_empty_keyring(self): ctkeyring = CTKeyring() key_id = pretend.stub(hex=pretend.call_recorder(lambda: pretend.stub())) @@ -63,15 +37,3 @@ def test_verify_fail_empty_keyring(self): with pytest.raises(CTKeyringLookupError, match="no known key for key ID?"): ctkeyring.verify(key_id=key_id, signature=signature, data=data) - - def test_verify_fail_keytype(self): - psuedo_key = pretend.stub( - public_bytes=pretend.call_recorder(lambda encoding, format: bytes(0)) - ) - ctkeyring = CTKeyring([psuedo_key]) - - signature = pretend.stub() - data = pretend.stub() - - with pytest.raises(CTKeyringError, match="unsupported key type:?"): - ctkeyring.verify(key_id=key_id(psuedo_key), signature=signature, data=data) From 22a60559f1c3b9e2b516efa8fcf1403f23bc6009 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 13:37:50 +0200 Subject: [PATCH 03/38] cli: Use TUF for rekor/ctfe keys if not in args Use TUF to get CTFE/Rekor keys in the non-staging, non-production flow. As before, the assumption is that user wants production keys in this case. Refactor TrustUpdater so that it does not do network traffic if nothing is requested. Signed-off-by: Jussi Kukkonen --- sigstore/_cli.py | 40 ++++++++++++++++----------------------- sigstore/_internal/tuf.py | 10 ++++++---- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 2deef3c22..e03daa102 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -40,11 +40,10 @@ RekorClient, RekorEntry, ) +from sigstore._internal.tuf import TrustUpdater from sigstore._sign import Signer from sigstore._utils import ( SplitCertificateChainError, - load_pem_public_key, - read_embedded, split_certificate_chain, ) from sigstore._verify import ( @@ -60,22 +59,6 @@ logging.basicConfig(level=os.environ.get("SIGSTORE_LOGLEVEL", "INFO").upper()) -class _Embedded: - """ - A repr-wrapper for reading embedded resources, needed to help `argparse` - render defaults correctly. - """ - - def __init__(self, name: str) -> None: - self._name = name - - def read(self) -> bytes: - return read_embedded(self._name) - - def __repr__(self) -> str: - return f"{self._name} (embedded)" - - def _boolify_env(envvar: str) -> bool: """ An `argparse` helper for turning an environment variable into a boolean. @@ -116,7 +99,7 @@ def _add_shared_instance_options(group: argparse._ArgumentGroup) -> None: metavar="FILE", type=argparse.FileType("rb"), help="A PEM-encoded root public key for Rekor itself (conflicts with --staging)", - default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY", _Embedded("rekor.pub")), + default=os.getenv("SIGSTORE_REKOR_ROOT_PUBKEY"), ) @@ -238,7 +221,7 @@ def _parser() -> argparse.ArgumentParser: metavar="FILE", type=argparse.FileType("rb"), help="A PEM-encoded public key for the CT log (conflicts with --staging)", - default=os.getenv("SIGSTORE_CTFE", _Embedded("ctfe.pub")), + default=os.getenv("SIGSTORE_CTFE"), ) sign.add_argument( @@ -427,12 +410,21 @@ def _sign(args: argparse.Namespace) -> None: elif args.fulcio_url == DEFAULT_FULCIO_URL and args.rekor_url == DEFAULT_REKOR_URL: signer = Signer.production() else: - ct_keyring = CTKeyring([args.ctfe_pem.read()]) + # Assume "production" keys if none are given as arguments + updater = TrustUpdater.production() + if args.ctfe_pem is not None: + ctfe_keys = [args.ctfe_pem.read()] + else: + ctfe_keys = updater.get_ctfe_keys() + if args.rekor_root_pubkey is not None: + rekor_key = args.rekor_root_pubkey.read() + else: + rekor_key = updater.get_rekor_key() + + ct_keyring = CTKeyring(ctfe_keys) signer = Signer( fulcio=FulcioClient(args.fulcio_url), - rekor=RekorClient( - args.rekor_url, args.rekor_root_pubkey.read(), ct_keyring - ), + rekor=RekorClient(args.rekor_url, rekor_key, ct_keyring), ) # The order of precedence is as follows: diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index cc27e4484..eeef310ec 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -59,10 +59,6 @@ def __init__(self, url: str) -> None: # TODO: Pre-populate with any targets we ship with sources self._targets_dir.mkdir(parents=True, exist_ok=True) - # this metadata refresh could be done lazily but currently that - # is not needed (if TrustUpdater is created it is always used) - self._setup() - logger.debug("TUF metadata: %s", self._metadata_dir) logger.debug("TUF targets cache: %s", self._targets_dir) @@ -89,6 +85,9 @@ def _setup(self) -> None: def get_ctfe_keys(self) -> List[bytes]: """Return the active CTFE public keys contents""" + if not self._updater: + self._setup() + ctfes = [] targets = self._updater._trusted_set.targets.signed.targets for target_info in targets.values(): @@ -107,6 +106,9 @@ def get_ctfe_keys(self) -> List[bytes]: def get_rekor_key(self) -> bytes: """Return the rekor public key content""" + if not self._updater: + self._setup() + targets = self._updater._trusted_set.targets.signed.targets for target, target_info in targets.items(): custom = target_info.unrecognized_fields["custom"]["sigstore"] From b6325fd15b3e845efc541bf962293ed349e919d1 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 14:20:04 +0200 Subject: [PATCH 04/38] Fix linter issues in TUF related code Signed-off-by: Jussi Kukkonen --- sigstore/_cli.py | 5 +---- sigstore/_internal/rekor/client.py | 2 +- sigstore/_internal/tuf.py | 18 ++++++++++-------- test/unit/internal/test_ctfe.py | 14 +++++++------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index e03daa102..6fc3a7219 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -42,10 +42,7 @@ ) from sigstore._internal.tuf import TrustUpdater from sigstore._sign import Signer -from sigstore._utils import ( - SplitCertificateChainError, - split_certificate_chain, -) +from sigstore._utils import SplitCertificateChainError, split_certificate_chain from sigstore._verify import ( CertificateVerificationFailure, RekorEntryMissing, diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 10b79748b..09ee0c343 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -34,7 +34,7 @@ from sigstore._internal.ctfe import CTKeyring from sigstore._internal.tuf import TrustUpdater -from sigstore._utils import base64_encode_pem_cert, read_embedded +from sigstore._utils import base64_encode_pem_cert logger = logging.getLogger(__name__) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index eeef310ec..02433da3e 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -16,10 +16,9 @@ import shutil from importlib import resources from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import List, Optional, Tuple from urllib import parse -from tuf.api.metadata import Metadata, Timestamp from tuf.ngclient import Updater logger = logging.getLogger(__name__) @@ -41,7 +40,7 @@ def _get_dirs(url: str) -> Tuple[Path, Path]: class TrustUpdater: def __init__(self, url: str) -> None: self._repo_url = url - self._updater = None + self._updater: Optional[Updater] = None self._metadata_dir, self._targets_dir = _get_dirs(url) @@ -70,9 +69,9 @@ def production(cls) -> "TrustUpdater": def staging(cls) -> "TrustUpdater": return cls(STAGING_TUF_URL) - def _setup(self) -> None: + def _setup(self) -> "Updater": """Initialize and update the toplevel TUF metadata""" - self._updater = Updater( + updater = Updater( metadata_dir=str(self._metadata_dir), metadata_base_url=f"{self._repo_url}", target_base_url=f"{self._repo_url}targets/", @@ -81,14 +80,16 @@ def _setup(self) -> None: # NOTE: we would like to avoid refresh if the toplevel metadata is valid. # https://github.com/theupdateframework/python-tuf/issues/2225 - self._updater.refresh() + updater.refresh() + return updater def get_ctfe_keys(self) -> List[bytes]: """Return the active CTFE public keys contents""" if not self._updater: - self._setup() + self._updater = self._setup() ctfes = [] + assert self._updater._trusted_set.targets targets = self._updater._trusted_set.targets.signed.targets for target_info in targets.values(): custom = target_info.unrecognized_fields["custom"]["sigstore"] @@ -107,8 +108,9 @@ def get_ctfe_keys(self) -> List[bytes]: def get_rekor_key(self) -> bytes: """Return the rekor public key content""" if not self._updater: - self._setup() + self._updater = self._setup() + assert self._updater._trusted_set.targets targets = self._updater._trusted_set.targets.signed.targets for target, target_info in targets.items(): custom = target_info.unrecognized_fields["custom"]["sigstore"] diff --git a/test/unit/internal/test_ctfe.py b/test/unit/internal/test_ctfe.py index f68c52242..1b28ad5fc 100644 --- a/test/unit/internal/test_ctfe.py +++ b/test/unit/internal/test_ctfe.py @@ -15,17 +15,17 @@ import pretend import pytest -from sigstore._internal.ctfe import ( - CTKeyring, - CTKeyringError, - CTKeyringLookupError, -) -from sigstore._utils import key_id +from sigstore._internal.ctfe import CTKeyring, CTKeyringLookupError class TestCTKeyring: def test_keyring_init(self): - keybytes = b"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu\ndDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\n-----END PUBLIC KEY-----" + keybytes = ( + b"-----BEGIN PUBLIC KEY-----\n" + b"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu\n" + b"dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==\n" + b"-----END PUBLIC KEY-----" + ) ctkeyring = CTKeyring([keybytes]) assert len(ctkeyring._keyring) == 1 From c689fdb24537db1b3e666b750cb4d16f6222b9d5 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 16:45:31 +0200 Subject: [PATCH 05/38] tuf: Fetch Fulcio certificates with TUF * Assume that the active fulcio certs in the repository form a certificate chain that cryptography can ingest * Refactor RekorClient construction so that we avoid constructing multiple TrustTupdaters Signed-off-by: Jussi Kukkonen --- sigstore/_internal/rekor/client.py | 12 +----------- sigstore/_internal/tuf.py | 22 ++++++++++++++++++++++ sigstore/_sign.py | 9 +++++++-- sigstore/_verify/verifier.py | 23 +++++++---------------- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 09ee0c343..daddd92cd 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -406,21 +406,11 @@ def __del__(self) -> None: self.session.close() @classmethod - def production(cls) -> RekorClient: - updater = TrustUpdater.production() + def with_updater(cls, updater: TrustUpdater) -> RekorClient: rekor_key = updater.get_rekor_key() ctfe_keys = updater.get_ctfe_keys() - return cls(DEFAULT_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) - @classmethod - def staging(cls) -> RekorClient: - updater = TrustUpdater.staging() - rekor_key = updater.get_rekor_key() - ctfe_keys = updater.get_ctfe_keys() - - return cls(STAGING_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) - @property def log(self) -> RekorLog: return RekorLog(urljoin(self.url, "log/"), session=self.session) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 02433da3e..f098b0f27 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -122,3 +122,25 @@ def get_rekor_key(self) -> bytes: return f.read() raise Exception("Rekor key not found in TUF metadata") + + def get_fulcio_certs(self) -> List[bytes]: + """Return the active Fulcio certificate contents""" + if not self._updater: + self._updater = self._setup() + + certs = [] + assert self._updater._trusted_set.targets + targets = self._updater._trusted_set.targets.signed.targets + for target_info in targets.values(): + custom = target_info.unrecognized_fields["custom"]["sigstore"] + if custom["status"] == "Active" and custom["usage"] == "Fulcio": + path = self._updater.find_cached_target(target_info) + if path is None: + path = self._updater.download_target(target_info) + with open(path, "rb") as f: + certs.append(f.read()) + + if not certs: + raise Exception("Fulcio certificates not found in TUF metadata") + + return certs diff --git a/sigstore/_sign.py b/sigstore/_sign.py index 4360b1cf1..5a96a930a 100644 --- a/sigstore/_sign.py +++ b/sigstore/_sign.py @@ -33,6 +33,7 @@ from sigstore._internal.oidc import Identity from sigstore._internal.rekor import RekorClient, RekorEntry from sigstore._internal.sct import verify_sct +from sigstore._internal.tuf import TrustUpdater from sigstore._utils import sha256_streaming logger = logging.getLogger(__name__) @@ -61,14 +62,18 @@ def production(cls) -> Signer: """ Return a `Signer` instance configured against Sigstore's production-level services. """ - return cls(fulcio=FulcioClient.production(), rekor=RekorClient.production()) + updater = TrustUpdater.production() + rekor = RekorClient.with_updater(updater) + return cls(fulcio=FulcioClient.production(), rekor=rekor) @classmethod def staging(cls) -> Signer: """ Return a `Signer` instance configured against Sigstore's staging-level services. """ - return cls(fulcio=FulcioClient.staging(), rekor=RekorClient.staging()) + updater = TrustUpdater.staging() + rekor = RekorClient.with_updater(updater) + return cls(fulcio=FulcioClient.staging(), rekor=rekor) def sign( self, diff --git a/sigstore/_verify/verifier.py b/sigstore/_verify/verifier.py index 58b11fa29..a5089510f 100644 --- a/sigstore/_verify/verifier.py +++ b/sigstore/_verify/verifier.py @@ -45,6 +45,7 @@ ) from sigstore._internal.rekor import RekorClient from sigstore._internal.set import InvalidSetError, verify_set +from sigstore._internal.tuf import TrustUpdater from sigstore._utils import read_embedded from sigstore._verify.models import InvalidRekorEntry as InvalidRekorEntryError from sigstore._verify.models import RekorEntryMissing as RekorEntryMissingError @@ -58,12 +59,6 @@ logger = logging.getLogger(__name__) -_DEFAULT_FULCIO_ROOT_CERT = read_embedded("fulcio.crt.pem") -_DEFAULT_FULCIO_INTERMEDIATE_CERT = read_embedded("fulcio_intermediate.crt.pem") - -_STAGING_FULCIO_ROOT_CERT = read_embedded("fulcio.crt.staging.pem") -_STAGING_FULCIO_INTERMEDIATE_CERT = read_embedded("fulcio_intermediate.crt.staging.pem") - class RekorEntryMissing(VerificationFailure): """ @@ -119,12 +114,10 @@ def production(cls) -> Verifier: """ Return a `Verifier` instance configured against Sigstore's production-level services. """ + updater = TrustUpdater.production() return cls( - rekor=RekorClient.production(), - fulcio_certificate_chain=[ - _DEFAULT_FULCIO_ROOT_CERT, - _DEFAULT_FULCIO_INTERMEDIATE_CERT, - ], + rekor=RekorClient.with_updater(updater), + fulcio_certificate_chain=updater.get_fulcio_certs(), ) @classmethod @@ -132,12 +125,10 @@ def staging(cls) -> Verifier: """ Return a `Verifier` instance configured against Sigstore's staging-level services. """ + updater = TrustUpdater.staging() return cls( - rekor=RekorClient.staging(), - fulcio_certificate_chain=[ - _STAGING_FULCIO_ROOT_CERT, - _STAGING_FULCIO_INTERMEDIATE_CERT, - ], + rekor=RekorClient.with_updater(updater), + fulcio_certificate_chain=updater.get_fulcio_certs() ) def verify( From d7b75c1df29b1e45129cdceb2b166aac135a4025 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 17:25:23 +0200 Subject: [PATCH 06/38] cli: Use production rekor key by default If this is not production or staging but rekor key is not given, use production: this is what original (non-tuf) code was doing as well. Signed-off-by: Jussi Kukkonen --- sigstore/_cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 6fc3a7219..d36ebd7e9 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -549,10 +549,16 @@ def _verify(args: argparse.Namespace) -> None: except SplitCertificateChainError as error: args._parser.error(f"Failed to parse certificate chain: {error}") + if args.rekor_root_pubkey is not None: + rekor_key = args.rekor_root_pubkey.read() + else: + updater = TrustUpdater.production() + rekor_key = updater.get_rekor_key() + verifier = Verifier( rekor=RekorClient( url=args.rekor_url, - pubkey=args.rekor_root_pubkey.read(), + pubkey=rekor_key, # We don't use the CT keyring in verification so we can supply an empty keyring ct_keyring=CTKeyring(), ), From 6386af7b8ea3d0cf1c61dcad38b7b089aecee55d Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 17:23:05 +0200 Subject: [PATCH 07/38] tuf: Enable staging support https://tuf-root-staging.storage.googleapis.com/ does work as staging repository. There is still a bug somewhere as staging verify currently fails. Signed-off-by: Jussi Kukkonen --- sigstore/_internal/tuf.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index f098b0f27..30cab4bf6 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) DEFAULT_TUF_URL = "https://sigstore-tuf-root.storage.googleapis.com/" -STAGING_TUF_URL = "https://sigstore-preprod-tuf-root.storage.googleapis.com/" +STAGING_TUF_URL = "https://tuf-root-staging.storage.googleapis.com/" def _get_dirs(url: str) -> Tuple[Path, Path]: @@ -47,12 +47,16 @@ def __init__(self, url: str) -> None: # intialize metadata dir tuf_root = self._metadata_dir / "root.json" if not tuf_root.exists(): - if self._repo_url not in [DEFAULT_TUF_URL, STAGING_TUF_URL]: + if self._repo_url == DEFAULT_TUF_URL: + fname = "root.json" + elif self._repo_url == STAGING_TUF_URL: + fname = "staging-root.json" + else: raise Exception(f"TUF root not found in {tuf_root}") self._metadata_dir.mkdir(parents=True, exist_ok=True) - with resources.path("sigstore._store", "root.json") as res: - shutil.copy2(res, self._metadata_dir) + with resources.path("sigstore._store", fname) as res: + shutil.copy2(res, tuf_root) # intialize targets cache dir # TODO: Pre-populate with any targets we ship with sources From 29885ff5e14688abc08dd463bcb5ea63091a4cc7 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Tue, 20 Dec 2022 17:36:23 +0200 Subject: [PATCH 08/38] _store: Add missing staging root.json This is needed to bootstrap the TUF metadata with --staging. Signed-off-by: Jussi Kukkonen --- sigstore/_store/staging-root.json | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 sigstore/_store/staging-root.json diff --git a/sigstore/_store/staging-root.json b/sigstore/_store/staging-root.json new file mode 100644 index 000000000..cfdffc13a --- /dev/null +++ b/sigstore/_store/staging-root.json @@ -0,0 +1,87 @@ +{ + "signatures": [ + { + "keyid": "e864b064b09791888913104e7f99fec1526df8047aba7170e767534cce0b60bb", + "sig": "d867ae1e99c21ac5e44fb09413d9351fefa37147f56573dc6923651f7badbcefd7a0d5421aacd66ba9394959862930aa5f71f511edaa4dbc4c6de0aaffcd3306" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": false, + "expires": "2032-10-20T18:54:05Z", + "keys": { + "5d2da8f9ad58e2006befdc5724defb2bddca032c4c20934a48365c8af9fe91c4": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "c5319e3c1f5c89b680fb5ab7fd60f44ee0fa25a15270a667d908c7c74e1f5bd8" + }, + "scheme": "ed25519" + }, + "77ae02bf54c38218f28158551062a86f7a9320574ab6ae63e5c96a14c801efa3": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "bb15adf3924c08d23b78f093f7131c1dc5a0716f706d02b7ae46dd6756894b79" + }, + "scheme": "ed25519" + }, + "8132b9a0526173757a3341d08079e4882c1d9b084f164fc397a572690183516b": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "f77a1b58274a212cf1947d21eb61c6dbd21aee95a7a579d605d1cbdb510574a6" + }, + "scheme": "ed25519" + }, + "e864b064b09791888913104e7f99fec1526df8047aba7170e767534cce0b60bb": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "21eaa32c2a328cbcbf6a254b884eea142f09ef275c8da135989eed6105707336" + }, + "scheme": "ed25519" + } + }, + "roles": { + "root": { + "keyids": [ + "e864b064b09791888913104e7f99fec1526df8047aba7170e767534cce0b60bb" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "77ae02bf54c38218f28158551062a86f7a9320574ab6ae63e5c96a14c801efa3" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "5d2da8f9ad58e2006befdc5724defb2bddca032c4c20934a48365c8af9fe91c4" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "8132b9a0526173757a3341d08079e4882c1d9b084f164fc397a572690183516b" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file From e1712a7b4e22637e26c7a0a047562e8f1226b430 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 20 Dec 2022 14:12:26 -0500 Subject: [PATCH 09/38] verifier: blacken Signed-off-by: William Woodruff --- sigstore/_verify/verifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/_verify/verifier.py b/sigstore/_verify/verifier.py index a5089510f..dd2eb50e0 100644 --- a/sigstore/_verify/verifier.py +++ b/sigstore/_verify/verifier.py @@ -128,7 +128,7 @@ def staging(cls) -> Verifier: updater = TrustUpdater.staging() return cls( rekor=RekorClient.with_updater(updater), - fulcio_certificate_chain=updater.get_fulcio_certs() + fulcio_certificate_chain=updater.get_fulcio_certs(), ) def verify( From b62a410c0c387dcd8125693a9d5f5192b0cfc8e6 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 20 Dec 2022 14:19:28 -0500 Subject: [PATCH 10/38] pyproject, sigstore/tuf: use appdirs for local state Signed-off-by: William Woodruff --- pyproject.toml | 1 + sigstore/_internal/tuf.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9eace7e0c..c6f98c59f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Topic :: Security :: Cryptography", ] dependencies = [ + "appdirs ~= 1.4", "cryptography >= 38", "importlib_resources ~= 5.7; python_version < '3.11'", "pydantic", diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 30cab4bf6..e3ee812cb 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -19,6 +19,7 @@ from typing import List, Optional, Tuple from urllib import parse +import appdirs from tuf.ngclient import Updater logger = logging.getLogger(__name__) @@ -28,13 +29,18 @@ def _get_dirs(url: str) -> Tuple[Path, Path]: - """Return metadata dir and target cache dir for URL""" - # NOTE: this is not great for windows: should maybe depend on appdirs? - # TODO: there should be URL normalization if URLs come from user - dir = parse.quote(url, safe="") - md_dir = Path.home() / ".local" / "share" / "sigstore-python" / "tuf" / dir - targets_dir = Path.home() / ".cache" / "sigstore-python" / "tuf" / dir - return md_dir, targets_dir + """ + Given a TUF repository URL, return suitable local metadata and cache directories. + + These directories are not guaranteed to already exist. + """ + + builder = appdirs.AppDirs("sigstore-python", parse.quote(url, safe="")) + + data_dir = Path(builder.user_data_dir) + cache_dir = Path(builder.user_cache_dir) + + return (data_dir / "tuf"), (cache_dir / "tuf") class TrustUpdater: From a8d1e4e9d76db3cbcb6cd3b53b725452f4987174 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 20 Dec 2022 14:20:06 -0500 Subject: [PATCH 11/38] verifier: unused import Signed-off-by: William Woodruff --- sigstore/_verify/verifier.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sigstore/_verify/verifier.py b/sigstore/_verify/verifier.py index dd2eb50e0..6e35f3a45 100644 --- a/sigstore/_verify/verifier.py +++ b/sigstore/_verify/verifier.py @@ -46,7 +46,6 @@ from sigstore._internal.rekor import RekorClient from sigstore._internal.set import InvalidSetError, verify_set from sigstore._internal.tuf import TrustUpdater -from sigstore._utils import read_embedded from sigstore._verify.models import InvalidRekorEntry as InvalidRekorEntryError from sigstore._verify.models import RekorEntryMissing as RekorEntryMissingError from sigstore._verify.models import ( From f75c8666dd357ce14617846f9bc17347f56e0756 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 20 Dec 2022 15:08:47 -0500 Subject: [PATCH 12/38] _internal/tuf: disambiguate caches correctly Signed-off-by: William Woodruff --- sigstore/_internal/tuf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index e3ee812cb..a42eda37b 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -35,12 +35,13 @@ def _get_dirs(url: str) -> Tuple[Path, Path]: These directories are not guaranteed to already exist. """ - builder = appdirs.AppDirs("sigstore-python", parse.quote(url, safe="")) + builder = appdirs.AppDirs("sigstore-python", "sigstore") + tuf_base = parse.quote(url, safe="") data_dir = Path(builder.user_data_dir) cache_dir = Path(builder.user_cache_dir) - return (data_dir / "tuf"), (cache_dir / "tuf") + return (data_dir / tuf_base), (cache_dir / tuf_base) class TrustUpdater: From 3a8f026154b1efc03c0a526b6b413a22fa4ca5b4 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 20 Dec 2022 15:54:07 -0500 Subject: [PATCH 13/38] sign, verify, internal: refactor rekor client handling Signed-off-by: William Woodruff --- sigstore/_internal/rekor/client.py | 7 ------- sigstore/_internal/tuf.py | 13 +++++++++++++ sigstore/_sign.py | 11 ++++++++--- sigstore/_verify/verifier.py | 21 +++++++++++++++++---- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index daddd92cd..36250dc0e 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -33,7 +33,6 @@ from securesystemslib.formats import encode_canonical from sigstore._internal.ctfe import CTKeyring -from sigstore._internal.tuf import TrustUpdater from sigstore._utils import base64_encode_pem_cert logger = logging.getLogger(__name__) @@ -405,12 +404,6 @@ def __init__(self, url: str, pubkey: bytes, ct_keyring: CTKeyring) -> None: def __del__(self) -> None: self.session.close() - @classmethod - def with_updater(cls, updater: TrustUpdater) -> RekorClient: - rekor_key = updater.get_rekor_key() - ctfe_keys = updater.get_ctfe_keys() - return cls(DEFAULT_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) - @property def log(self) -> RekorLog: return RekorLog(urljoin(self.url, "log/"), session=self.session) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index a42eda37b..577c1561f 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -22,6 +22,9 @@ import appdirs from tuf.ngclient import Updater +from sigstore._internal.ctfe import CTKeyring +from sigstore._internal.rekor.client import RekorClient + logger = logging.getLogger(__name__) DEFAULT_TUF_URL = "https://sigstore-tuf-root.storage.googleapis.com/" @@ -80,6 +83,16 @@ def production(cls) -> "TrustUpdater": def staging(cls) -> "TrustUpdater": return cls(STAGING_TUF_URL) + def rekor_client(self, rekor_url: str) -> RekorClient: + """ + Create a `RekorClient` from the current updater. + """ + return RekorClient( + rekor_url, + self.get_rekor_key(), + CTKeyring(self.get_ctfe_keys()), + ) + def _setup(self) -> "Updater": """Initialize and update the toplevel TUF metadata""" updater = Updater( diff --git a/sigstore/_sign.py b/sigstore/_sign.py index 5a96a930a..3d0a76c3e 100644 --- a/sigstore/_sign.py +++ b/sigstore/_sign.py @@ -31,7 +31,12 @@ from sigstore._internal.fulcio import FulcioClient from sigstore._internal.oidc import Identity -from sigstore._internal.rekor import RekorClient, RekorEntry +from sigstore._internal.rekor.client import ( + DEFAULT_REKOR_URL, + STAGING_REKOR_URL, + RekorClient, + RekorEntry, +) from sigstore._internal.sct import verify_sct from sigstore._internal.tuf import TrustUpdater from sigstore._utils import sha256_streaming @@ -63,7 +68,7 @@ def production(cls) -> Signer: Return a `Signer` instance configured against Sigstore's production-level services. """ updater = TrustUpdater.production() - rekor = RekorClient.with_updater(updater) + rekor = updater.rekor_client(DEFAULT_REKOR_URL) return cls(fulcio=FulcioClient.production(), rekor=rekor) @classmethod @@ -72,7 +77,7 @@ def staging(cls) -> Signer: Return a `Signer` instance configured against Sigstore's staging-level services. """ updater = TrustUpdater.staging() - rekor = RekorClient.with_updater(updater) + rekor = updater.rekor_client(STAGING_REKOR_URL) return cls(fulcio=FulcioClient.staging(), rekor=rekor) def sign( diff --git a/sigstore/_verify/verifier.py b/sigstore/_verify/verifier.py index 6e35f3a45..a54853dfb 100644 --- a/sigstore/_verify/verifier.py +++ b/sigstore/_verify/verifier.py @@ -18,6 +18,7 @@ from __future__ import annotations +import base64 import datetime import logging from typing import List, cast @@ -43,7 +44,11 @@ InvalidInclusionProofError, verify_merkle_inclusion, ) -from sigstore._internal.rekor import RekorClient +from sigstore._internal.rekor.client import ( + DEFAULT_REKOR_URL, + STAGING_REKOR_URL, + RekorClient, +) from sigstore._internal.set import InvalidSetError, verify_set from sigstore._internal.tuf import TrustUpdater from sigstore._verify.models import InvalidRekorEntry as InvalidRekorEntryError @@ -66,8 +71,16 @@ class RekorEntryMissing(VerificationFailure): """ reason: str = "Rekor has no entry for the given verification materials" + signature: str + """ + The signature present during lookup failure, encoded with base64. + """ + artifact_hash: str + """ + The artifact hash present during lookup failure, encoded as a hex string. + """ class CertificateVerificationFailure(VerificationFailure): @@ -115,7 +128,7 @@ def production(cls) -> Verifier: """ updater = TrustUpdater.production() return cls( - rekor=RekorClient.with_updater(updater), + rekor=updater.rekor_client(DEFAULT_REKOR_URL), fulcio_certificate_chain=updater.get_fulcio_certs(), ) @@ -126,7 +139,7 @@ def staging(cls) -> Verifier: """ updater = TrustUpdater.staging() return cls( - rekor=RekorClient.with_updater(updater), + rekor=updater.rekor_client(STAGING_REKOR_URL), fulcio_certificate_chain=updater.get_fulcio_certs(), ) @@ -227,7 +240,7 @@ def verify( entry = materials.rekor_entry(self._rekor) except RekorEntryMissingError: return RekorEntryMissing( - signature=materials.signature, + signature=base64.b64encode(materials.signature).decode(), artifact_hash=materials.input_digest.hex(), ) except InvalidRekorEntryError: From 7d80e935ebd2ba0102346862928f3534f91012e9 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 20 Dec 2022 16:24:11 -0500 Subject: [PATCH 14/38] test/verify: fix TestVerificationMaterials test Signed-off-by: William Woodruff --- test/unit/verify/test_models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/verify/test_models.py b/test/unit/verify/test_models.py index c0cf53d04..460b095f6 100644 --- a/test/unit/verify/test_models.py +++ b/test/unit/verify/test_models.py @@ -15,7 +15,8 @@ import pretend import pytest -from sigstore._internal.rekor import RekorClient +from sigstore._internal.rekor.client import STAGING_REKOR_URL +from sigstore._internal.tuf import TrustUpdater from sigstore._verify.models import InvalidRekorEntry @@ -36,6 +37,7 @@ def test_verification_materials_retrieves_rekor_entry(self, signing_materials): materials = signing_materials("a.txt") assert materials._offline_rekor_entry is None - client = RekorClient.staging() + tuf = TrustUpdater.staging() + client = tuf.rekor_client(STAGING_REKOR_URL) entry = materials.rekor_entry(client) assert entry is not None From 1dd9c1fff1c8cbb953ea643af14a29514d42d468 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 11:21:27 +0200 Subject: [PATCH 15/38] Refactor RekorClient construction once more * Bring back RekorClient.production() and RekorClient.staging(): these are simple but make the calling code slightly clearer maybe * Add a TrustUpdater argument to those methods: if you use production/staging, you need a TrustUpdater * The TrustUpdater can not be constructed inside RekorClient as other components may need it as well. It's not perfectly elegant for the caller but it's not horrible either: updater = TrustUpdater.staging() client = RekorClient.staging(updater) This design means TrustUpdater does not know anything about the sigstore mechanisms: it just discovers and downloads files. Signed-off-by: Jussi Kukkonen --- sigstore/_internal/rekor/client.py | 15 +++++++++++++++ sigstore/_internal/tuf.py | 13 ------------- sigstore/_sign.py | 11 +++-------- sigstore/_verify/verifier.py | 10 +++------- test/unit/verify/test_models.py | 4 ++-- 5 files changed, 23 insertions(+), 30 deletions(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 36250dc0e..eb70ae8fc 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -33,6 +33,7 @@ from securesystemslib.formats import encode_canonical from sigstore._internal.ctfe import CTKeyring +from sigstore._internal.tuf import TrustUpdater from sigstore._utils import base64_encode_pem_cert logger = logging.getLogger(__name__) @@ -404,6 +405,20 @@ def __init__(self, url: str, pubkey: bytes, ct_keyring: CTKeyring) -> None: def __del__(self) -> None: self.session.close() + @classmethod + def production(cls, updater: TrustUpdater) -> RekorClient: + rekor_key = updater.get_rekor_key() + ctfe_keys = updater.get_ctfe_keys() + + return cls(DEFAULT_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) + + @classmethod + def staging(cls, updater: TrustUpdater) -> RekorClient: + rekor_key = updater.get_rekor_key() + ctfe_keys = updater.get_ctfe_keys() + + return cls(STAGING_REKOR_URL, rekor_key, CTKeyring(ctfe_keys)) + @property def log(self) -> RekorLog: return RekorLog(urljoin(self.url, "log/"), session=self.session) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 577c1561f..a42eda37b 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -22,9 +22,6 @@ import appdirs from tuf.ngclient import Updater -from sigstore._internal.ctfe import CTKeyring -from sigstore._internal.rekor.client import RekorClient - logger = logging.getLogger(__name__) DEFAULT_TUF_URL = "https://sigstore-tuf-root.storage.googleapis.com/" @@ -83,16 +80,6 @@ def production(cls) -> "TrustUpdater": def staging(cls) -> "TrustUpdater": return cls(STAGING_TUF_URL) - def rekor_client(self, rekor_url: str) -> RekorClient: - """ - Create a `RekorClient` from the current updater. - """ - return RekorClient( - rekor_url, - self.get_rekor_key(), - CTKeyring(self.get_ctfe_keys()), - ) - def _setup(self) -> "Updater": """Initialize and update the toplevel TUF metadata""" updater = Updater( diff --git a/sigstore/_sign.py b/sigstore/_sign.py index 3d0a76c3e..71e667869 100644 --- a/sigstore/_sign.py +++ b/sigstore/_sign.py @@ -31,12 +31,7 @@ from sigstore._internal.fulcio import FulcioClient from sigstore._internal.oidc import Identity -from sigstore._internal.rekor.client import ( - DEFAULT_REKOR_URL, - STAGING_REKOR_URL, - RekorClient, - RekorEntry, -) +from sigstore._internal.rekor.client import RekorClient, RekorEntry from sigstore._internal.sct import verify_sct from sigstore._internal.tuf import TrustUpdater from sigstore._utils import sha256_streaming @@ -68,7 +63,7 @@ def production(cls) -> Signer: Return a `Signer` instance configured against Sigstore's production-level services. """ updater = TrustUpdater.production() - rekor = updater.rekor_client(DEFAULT_REKOR_URL) + rekor = RekorClient.production(updater) return cls(fulcio=FulcioClient.production(), rekor=rekor) @classmethod @@ -77,7 +72,7 @@ def staging(cls) -> Signer: Return a `Signer` instance configured against Sigstore's staging-level services. """ updater = TrustUpdater.staging() - rekor = updater.rekor_client(STAGING_REKOR_URL) + rekor = RekorClient.staging(updater) return cls(fulcio=FulcioClient.staging(), rekor=rekor) def sign( diff --git a/sigstore/_verify/verifier.py b/sigstore/_verify/verifier.py index a54853dfb..85af6358d 100644 --- a/sigstore/_verify/verifier.py +++ b/sigstore/_verify/verifier.py @@ -44,11 +44,7 @@ InvalidInclusionProofError, verify_merkle_inclusion, ) -from sigstore._internal.rekor.client import ( - DEFAULT_REKOR_URL, - STAGING_REKOR_URL, - RekorClient, -) +from sigstore._internal.rekor.client import RekorClient from sigstore._internal.set import InvalidSetError, verify_set from sigstore._internal.tuf import TrustUpdater from sigstore._verify.models import InvalidRekorEntry as InvalidRekorEntryError @@ -128,7 +124,7 @@ def production(cls) -> Verifier: """ updater = TrustUpdater.production() return cls( - rekor=updater.rekor_client(DEFAULT_REKOR_URL), + rekor=RekorClient.production(updater), fulcio_certificate_chain=updater.get_fulcio_certs(), ) @@ -139,7 +135,7 @@ def staging(cls) -> Verifier: """ updater = TrustUpdater.staging() return cls( - rekor=updater.rekor_client(STAGING_REKOR_URL), + rekor=RekorClient.staging(updater), fulcio_certificate_chain=updater.get_fulcio_certs(), ) diff --git a/test/unit/verify/test_models.py b/test/unit/verify/test_models.py index 460b095f6..f51783338 100644 --- a/test/unit/verify/test_models.py +++ b/test/unit/verify/test_models.py @@ -15,7 +15,7 @@ import pretend import pytest -from sigstore._internal.rekor.client import STAGING_REKOR_URL +from sigstore._internal.rekor.client import RekorClient from sigstore._internal.tuf import TrustUpdater from sigstore._verify.models import InvalidRekorEntry @@ -38,6 +38,6 @@ def test_verification_materials_retrieves_rekor_entry(self, signing_materials): assert materials._offline_rekor_entry is None tuf = TrustUpdater.staging() - client = tuf.rekor_client(STAGING_REKOR_URL) + client = RekorClient.staging(tuf) entry = materials.rekor_entry(client) assert entry is not None From 8072f1d587cceeb2da019b47679dd78b271e62eb Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 11:51:13 +0200 Subject: [PATCH 16/38] internal: Improve tuf docstrings Signed-off-by: Jussi Kukkonen --- sigstore/_internal/tuf.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index a42eda37b..9a0cc5dce 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -45,6 +45,16 @@ def _get_dirs(url: str) -> Tuple[Path, Path]: class TrustUpdater: + """Internal trust root (certificates and keys) downloader. + + TrustUpdater discovers the currently valid certificates and keys and + securely downloads them from the remote TUF repository at 'url'. + + TrustUpdater expects to find an initial root.json in either the local + metadata directory for this URL, or (as special case for the sigstore.dev + production and staging instances) in the application resources. + """ + def __init__(self, url: str) -> None: self._repo_url = url self._updater: Optional[Updater] = None @@ -95,7 +105,10 @@ def _setup(self) -> "Updater": return updater def get_ctfe_keys(self) -> List[bytes]: - """Return the active CTFE public keys contents""" + """Return the active CTFE public keys contents. + + May download files from the remote repository. + """ if not self._updater: self._updater = self._setup() @@ -117,7 +130,10 @@ def get_ctfe_keys(self) -> List[bytes]: return ctfes def get_rekor_key(self) -> bytes: - """Return the rekor public key content""" + """Return the rekor public key content. + + May download files from the remote repository. + """ if not self._updater: self._updater = self._setup() @@ -135,7 +151,10 @@ def get_rekor_key(self) -> bytes: raise Exception("Rekor key not found in TUF metadata") def get_fulcio_certs(self) -> List[bytes]: - """Return the active Fulcio certificate contents""" + """Return the active Fulcio certificate contents. + + May download files from the remote repository. + """ if not self._updater: self._updater = self._setup() From e85d6f40c2e27871051eb6810fd674cd159099ed Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 12:00:35 +0200 Subject: [PATCH 17/38] internal: Refactor tuf No functional change, just refactor the target discovery into a single method. Signed-off-by: Jussi Kukkonen --- sigstore/_internal/tuf.py | 58 ++++++++++++--------------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 9a0cc5dce..2d74adfb6 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -104,29 +104,33 @@ def _setup(self) -> "Updater": updater.refresh() return updater - def get_ctfe_keys(self) -> List[bytes]: - """Return the active CTFE public keys contents. - - May download files from the remote repository. - """ + def _get(self, usage: str) -> List[bytes]: + """Return all active targets with given usage""" if not self._updater: self._updater = self._setup() - ctfes = [] + data = [] assert self._updater._trusted_set.targets targets = self._updater._trusted_set.targets.signed.targets for target_info in targets.values(): custom = target_info.unrecognized_fields["custom"]["sigstore"] - if custom["status"] == "Active" and custom["usage"] == "CTFE": + if custom["status"] == "Active" and custom["usage"] == usage: path = self._updater.find_cached_target(target_info) if path is None: path = self._updater.download_target(target_info) with open(path, "rb") as f: - ctfes.append(f.read()) + data.append(f.read()) + + return data + + def get_ctfe_keys(self) -> List[bytes]: + """Return the active CTFE public keys contents. + May download files from the remote repository. + """ + ctfes = self._get("CTFE") if not ctfes: raise Exception("CTFE keys not found in TUF metadata") - return ctfes def get_rekor_key(self) -> bytes: @@ -134,43 +138,17 @@ def get_rekor_key(self) -> bytes: May download files from the remote repository. """ - if not self._updater: - self._updater = self._setup() - - assert self._updater._trusted_set.targets - targets = self._updater._trusted_set.targets.signed.targets - for target, target_info in targets.items(): - custom = target_info.unrecognized_fields["custom"]["sigstore"] - if custom["status"] == "Active" and custom["usage"] == "Rekor": - path = self._updater.find_cached_target(target_info) - if path is None: - path = self._updater.download_target(target_info) - with open(path, "rb") as f: - return f.read() - - raise Exception("Rekor key not found in TUF metadata") + keys = self._get("Rekor") + if len(keys) != 1: + raise Exception("Did not find one active Rekor key in TUF metadata") + return keys[0] def get_fulcio_certs(self) -> List[bytes]: """Return the active Fulcio certificate contents. May download files from the remote repository. """ - if not self._updater: - self._updater = self._setup() - - certs = [] - assert self._updater._trusted_set.targets - targets = self._updater._trusted_set.targets.signed.targets - for target_info in targets.values(): - custom = target_info.unrecognized_fields["custom"]["sigstore"] - if custom["status"] == "Active" and custom["usage"] == "Fulcio": - path = self._updater.find_cached_target(target_info) - if path is None: - path = self._updater.download_target(target_info) - with open(path, "rb") as f: - certs.append(f.read()) - + certs = self._get("Fulcio") if not certs: raise Exception("Fulcio certificates not found in TUF metadata") - return certs From 238f191f07521129bc376455524e309c5d2f3be5 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 12:17:36 +0200 Subject: [PATCH 18/38] tests: Remove test for _store The test doesn't make a lot of sense now that the keys are not being read from the _store. Signed-off-by: Jussi Kukkonen --- sigstore/_utils.py | 8 -------- test/unit/test_store.py | 31 ------------------------------- 2 files changed, 39 deletions(-) delete mode 100644 test/unit/test_store.py diff --git a/sigstore/_utils.py b/sigstore/_utils.py index 88fda5ed7..09a12c7f9 100644 --- a/sigstore/_utils.py +++ b/sigstore/_utils.py @@ -151,11 +151,3 @@ def sha256_streaming(io: IO[bytes]) -> bytes: nbytes = io.readinto(view) # type: ignore return sha256.digest() - - -def read_embedded(name: str) -> bytes: - """ - Read a resource embedded in this distribution of sigstore-python, - returning its contents as bytes. - """ - return resources.files("sigstore._store").joinpath(name).read_bytes() diff --git a/test/unit/test_store.py b/test/unit/test_store.py deleted file mode 100644 index 494ff23c9..000000000 --- a/test/unit/test_store.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2022 The Sigstore Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from sigstore._utils import read_embedded - - -def test_store_reads_fulcio_root_cert(): - fulcio_crt = read_embedded("fulcio.crt.pem").strip() - lines = fulcio_crt.split(b"\n") - - assert lines[0].startswith(b"-----BEGIN CERTIFICATE-----") - assert lines[-1].startswith(b"-----END CERTIFICATE-----") - - -def test_store_reads_ctfe_pub(): - ctfe_pub = read_embedded("ctfe.pub").strip() - lines = ctfe_pub.split(b"\n") - - assert lines[0].startswith(b"-----BEGIN PUBLIC KEY-----") - assert lines[-1].startswith(b"-----END PUBLIC KEY-----") From 863225008adcf2ac6ad09ccc51f2aa12a46c5c68 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 12:12:13 +0200 Subject: [PATCH 19/38] _store: Remove all certificates and keys These are now made available via the _internal.tuf module. There is still a case to be made for embedding keys and certs in the wheel (to optimize the first run experience, and the experience for those who might not persist their caches, e.g. CI systems). But: * Testing this without embedded keys first likely makes sense: We get more experience and feedback on the trust update system * There should be some automated system that updates the embedded keys. Otherwise obsolete keys will be embedded and no-one notices. Signed-off-by: Jussi Kukkonen --- sigstore/_store/ctfe.pub | 4 ---- sigstore/_store/ctfe.staging.pub | 13 ------------- sigstore/_store/ctfe_2022.2.staging.pub | 4 ---- sigstore/_store/ctfe_2022.pub | 4 ---- sigstore/_store/ctfe_2022.staging.pub | 4 ---- sigstore/_store/fulcio.crt.pem | 13 ------------- sigstore/_store/fulcio.crt.staging.pem | 13 ------------- sigstore/_store/fulcio_intermediate.crt.pem | 14 -------------- .../_store/fulcio_intermediate.crt.staging.pem | 14 -------------- sigstore/_store/rekor.pub | 4 ---- sigstore/_store/rekor.staging.pub | 4 ---- 11 files changed, 91 deletions(-) delete mode 100644 sigstore/_store/ctfe.pub delete mode 100644 sigstore/_store/ctfe.staging.pub delete mode 100644 sigstore/_store/ctfe_2022.2.staging.pub delete mode 100644 sigstore/_store/ctfe_2022.pub delete mode 100644 sigstore/_store/ctfe_2022.staging.pub delete mode 100644 sigstore/_store/fulcio.crt.pem delete mode 100644 sigstore/_store/fulcio.crt.staging.pem delete mode 100644 sigstore/_store/fulcio_intermediate.crt.pem delete mode 100644 sigstore/_store/fulcio_intermediate.crt.staging.pem delete mode 100644 sigstore/_store/rekor.pub delete mode 100644 sigstore/_store/rekor.staging.pub diff --git a/sigstore/_store/ctfe.pub b/sigstore/_store/ctfe.pub deleted file mode 100644 index 75df6bbb9..000000000 --- a/sigstore/_store/ctfe.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu -dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w== ------END PUBLIC KEY----- diff --git a/sigstore/_store/ctfe.staging.pub b/sigstore/_store/ctfe.staging.pub deleted file mode 100644 index 39512c214..000000000 --- a/sigstore/_store/ctfe.staging.pub +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN RSA PUBLIC KEY----- -MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3 -slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZG -z/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT5 -3cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXX -w4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K -6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZev -opmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lI -xNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0x -igwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYU -SeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7g -joCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ== ------END RSA PUBLIC KEY----- diff --git a/sigstore/_store/ctfe_2022.2.staging.pub b/sigstore/_store/ctfe_2022.2.staging.pub deleted file mode 100644 index 0f5eb8637..000000000 --- a/sigstore/_store/ctfe_2022.2.staging.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHq -c24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ== ------END PUBLIC KEY----- diff --git a/sigstore/_store/ctfe_2022.pub b/sigstore/_store/ctfe_2022.pub deleted file mode 100644 index 32fa2ad10..000000000 --- a/sigstore/_store/ctfe_2022.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNK -AaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw== ------END PUBLIC KEY----- diff --git a/sigstore/_store/ctfe_2022.staging.pub b/sigstore/_store/ctfe_2022.staging.pub deleted file mode 100644 index 3023b8618..000000000 --- a/sigstore/_store/ctfe_2022.staging.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bY -eSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA== ------END PUBLIC KEY----- diff --git a/sigstore/_store/fulcio.crt.pem b/sigstore/_store/fulcio.crt.pem deleted file mode 100644 index 3afc46bb6..000000000 --- a/sigstore/_store/fulcio.crt.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw -KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y -MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl -LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 -XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex -X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j -YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY -wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ -KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM -WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 -TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ ------END CERTIFICATE----- \ No newline at end of file diff --git a/sigstore/_store/fulcio.crt.staging.pem b/sigstore/_store/fulcio.crt.staging.pem deleted file mode 100644 index 47a5becff..000000000 --- a/sigstore/_store/fulcio.crt.staging.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAq -MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy -MDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUu -ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9 -BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUEC -CWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNj -MGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9C -Mrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6H -j2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm -45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTr -y3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw= ------END CERTIFICATE----- diff --git a/sigstore/_store/fulcio_intermediate.crt.pem b/sigstore/_store/fulcio_intermediate.crt.pem deleted file mode 100644 index 6d1c298ba..000000000 --- a/sigstore/_store/fulcio_intermediate.crt.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw -KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y -MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl -LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C -AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 -7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS -0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB -BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp -KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI -zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR -nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP -mygUY7Ii2zbdCdliiow= ------END CERTIFICATE----- \ No newline at end of file diff --git a/sigstore/_store/fulcio_intermediate.crt.staging.pem b/sigstore/_store/fulcio_intermediate.crt.staging.pem deleted file mode 100644 index d94a2aa40..000000000 --- a/sigstore/_store/fulcio_intermediate.crt.staging.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAq -MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy -MDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUu -ZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB -BgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9Kt -NfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWI -JEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF -BQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQF -Gn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjO -PQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1O -HHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/ -KX1SBrKQu220FmVL0Q== ------END CERTIFICATE----- diff --git a/sigstore/_store/rekor.pub b/sigstore/_store/rekor.pub deleted file mode 100644 index 050ef6014..000000000 --- a/sigstore/_store/rekor.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr -kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw== ------END PUBLIC KEY----- diff --git a/sigstore/_store/rekor.staging.pub b/sigstore/_store/rekor.staging.pub deleted file mode 100644 index 4234e16c3..000000000 --- a/sigstore/_store/rekor.staging.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9 -nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg== ------END PUBLIC KEY----- From d342697a5d65dd857b196fe1f56da6da149b1938 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 13:27:06 +0200 Subject: [PATCH 20/38] tests: Add mock TUF fetcher for staging This allows running (otherwise offline) staging tests without network access. * Add a fixture that mocks tuf.ngclient fetcher: it returns files from test assets * Mark the relevant tests with mock_staging_tuf fixture * Mark test_verifier_production() as "online": there is no way to test production tuf repository offline as it expires every two weeks Signed-off-by: Jussi Kukkonen --- sigstore/_internal/tuf.py | 4 + test/unit/assets-staging-tuf/1.root.json | 87 ++++++++++++++++++ test/unit/assets-staging-tuf/ctfe.pub | 13 +++ test/unit/assets-staging-tuf/ctfe_2022.pub | 4 + test/unit/assets-staging-tuf/ctfe_2022_2.pub | 4 + test/unit/assets-staging-tuf/fulcio.crt.pem | 13 +++ .../fulcio_intermediate.crt.pem | 14 +++ test/unit/assets-staging-tuf/rekor.pub | 4 + test/unit/assets-staging-tuf/snapshot.json | 30 +++++++ test/unit/assets-staging-tuf/targets.json | 88 +++++++++++++++++++ test/unit/assets-staging-tuf/timestamp.json | 23 +++++ test/unit/conftest.py | 21 +++++ test/unit/test_sign.py | 3 +- test/unit/verify/test_verifier.py | 7 +- 14 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 test/unit/assets-staging-tuf/1.root.json create mode 100644 test/unit/assets-staging-tuf/ctfe.pub create mode 100644 test/unit/assets-staging-tuf/ctfe_2022.pub create mode 100644 test/unit/assets-staging-tuf/ctfe_2022_2.pub create mode 100644 test/unit/assets-staging-tuf/fulcio.crt.pem create mode 100644 test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem create mode 100644 test/unit/assets-staging-tuf/rekor.pub create mode 100644 test/unit/assets-staging-tuf/snapshot.json create mode 100644 test/unit/assets-staging-tuf/targets.json create mode 100644 test/unit/assets-staging-tuf/timestamp.json diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 2d74adfb6..604c65fb5 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -27,6 +27,9 @@ DEFAULT_TUF_URL = "https://sigstore-tuf-root.storage.googleapis.com/" STAGING_TUF_URL = "https://tuf-root-staging.storage.googleapis.com/" +# for tests to override +_fetcher = None + def _get_dirs(url: str) -> Tuple[Path, Path]: """ @@ -97,6 +100,7 @@ def _setup(self) -> "Updater": metadata_base_url=f"{self._repo_url}", target_base_url=f"{self._repo_url}targets/", target_dir=str(self._targets_dir), + fetcher=_fetcher, ) # NOTE: we would like to avoid refresh if the toplevel metadata is valid. diff --git a/test/unit/assets-staging-tuf/1.root.json b/test/unit/assets-staging-tuf/1.root.json new file mode 100644 index 000000000..cfdffc13a --- /dev/null +++ b/test/unit/assets-staging-tuf/1.root.json @@ -0,0 +1,87 @@ +{ + "signatures": [ + { + "keyid": "e864b064b09791888913104e7f99fec1526df8047aba7170e767534cce0b60bb", + "sig": "d867ae1e99c21ac5e44fb09413d9351fefa37147f56573dc6923651f7badbcefd7a0d5421aacd66ba9394959862930aa5f71f511edaa4dbc4c6de0aaffcd3306" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": false, + "expires": "2032-10-20T18:54:05Z", + "keys": { + "5d2da8f9ad58e2006befdc5724defb2bddca032c4c20934a48365c8af9fe91c4": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "c5319e3c1f5c89b680fb5ab7fd60f44ee0fa25a15270a667d908c7c74e1f5bd8" + }, + "scheme": "ed25519" + }, + "77ae02bf54c38218f28158551062a86f7a9320574ab6ae63e5c96a14c801efa3": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "bb15adf3924c08d23b78f093f7131c1dc5a0716f706d02b7ae46dd6756894b79" + }, + "scheme": "ed25519" + }, + "8132b9a0526173757a3341d08079e4882c1d9b084f164fc397a572690183516b": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "f77a1b58274a212cf1947d21eb61c6dbd21aee95a7a579d605d1cbdb510574a6" + }, + "scheme": "ed25519" + }, + "e864b064b09791888913104e7f99fec1526df8047aba7170e767534cce0b60bb": { + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keytype": "ed25519", + "keyval": { + "public": "21eaa32c2a328cbcbf6a254b884eea142f09ef275c8da135989eed6105707336" + }, + "scheme": "ed25519" + } + }, + "roles": { + "root": { + "keyids": [ + "e864b064b09791888913104e7f99fec1526df8047aba7170e767534cce0b60bb" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "77ae02bf54c38218f28158551062a86f7a9320574ab6ae63e5c96a14c801efa3" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "5d2da8f9ad58e2006befdc5724defb2bddca032c4c20934a48365c8af9fe91c4" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "8132b9a0526173757a3341d08079e4882c1d9b084f164fc397a572690183516b" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/test/unit/assets-staging-tuf/ctfe.pub b/test/unit/assets-staging-tuf/ctfe.pub new file mode 100644 index 000000000..39512c214 --- /dev/null +++ b/test/unit/assets-staging-tuf/ctfe.pub @@ -0,0 +1,13 @@ +-----BEGIN RSA PUBLIC KEY----- +MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3 +slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZG +z/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT5 +3cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXX +w4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K +6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZev +opmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lI +xNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0x +igwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYU +SeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7g +joCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ== +-----END RSA PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/ctfe_2022.pub b/test/unit/assets-staging-tuf/ctfe_2022.pub new file mode 100644 index 000000000..3023b8618 --- /dev/null +++ b/test/unit/assets-staging-tuf/ctfe_2022.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bY +eSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA== +-----END PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/ctfe_2022_2.pub b/test/unit/assets-staging-tuf/ctfe_2022_2.pub new file mode 100644 index 000000000..0f5eb8637 --- /dev/null +++ b/test/unit/assets-staging-tuf/ctfe_2022_2.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHq +c24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ== +-----END PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/fulcio.crt.pem b/test/unit/assets-staging-tuf/fulcio.crt.pem new file mode 100644 index 000000000..47a5becff --- /dev/null +++ b/test/unit/assets-staging-tuf/fulcio.crt.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAq +MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy +MDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUu +ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9 +BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUEC +CWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNj +MGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9C +Mrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6H +j2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm +45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTr +y3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw= +-----END CERTIFICATE----- diff --git a/test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem b/test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem new file mode 100644 index 000000000..d94a2aa40 --- /dev/null +++ b/test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAq +MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy +MDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUu +ZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB +BgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9Kt +NfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWI +JEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF +BQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQF +Gn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjO +PQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1O +HHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/ +KX1SBrKQu220FmVL0Q== +-----END CERTIFICATE----- diff --git a/test/unit/assets-staging-tuf/rekor.pub b/test/unit/assets-staging-tuf/rekor.pub new file mode 100644 index 000000000..4234e16c3 --- /dev/null +++ b/test/unit/assets-staging-tuf/rekor.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9 +nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg== +-----END PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/snapshot.json b/test/unit/assets-staging-tuf/snapshot.json new file mode 100644 index 000000000..3fbc53aad --- /dev/null +++ b/test/unit/assets-staging-tuf/snapshot.json @@ -0,0 +1,30 @@ +{ + "signatures": [ + { + "keyid": "77ae02bf54c38218f28158551062a86f7a9320574ab6ae63e5c96a14c801efa3", + "sig": "2bbce0ade009e7dd1160e9ea4e8610c8603dd53c256f4105f297b5b085ebdde292503dd3c462d413aeb6db3e56534f04c0b0b2ce7e7e21194723aa09d6676700" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2032-10-20T18:54:05Z", + "meta": { + "root.json": { + "hashes": { + "sha512": "5e437c93331d12b3e54c1f1dabd61a5c000d08a29b85e35f71f49cc52c37ca8df90fad6a598f1e597617ecf580b18191d924e9f275e78adc6aaf9c5f1ad6c49c" + }, + "length": 2482, + "version": 1 + }, + "targets.json": { + "hashes": { + "sha512": "df7ffa4f0634db1723dc5cee5a0b65438aeed2c37be5a76dd0f5bd2a01d5522f7a4bf1257aeed1c303d3b0775e2ad173434fe24fff13b2c9582a4deacf937aac" + }, + "length": 2616, + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/test/unit/assets-staging-tuf/targets.json b/test/unit/assets-staging-tuf/targets.json new file mode 100644 index 000000000..ec7f674ef --- /dev/null +++ b/test/unit/assets-staging-tuf/targets.json @@ -0,0 +1,88 @@ +{ + "signatures": [ + { + "keyid": "5d2da8f9ad58e2006befdc5724defb2bddca032c4c20934a48365c8af9fe91c4", + "sig": "f8e9b911e87e875c56511dae7fc30512305de2d5be437b78b07c532396f08750445bb423b9972e103f84ddf871c656fa3221b26f3655c57c1b47e45a190bcf07" + } + ], + "signed": { + "_type": "targets", + "expires": "2032-10-20T18:54:05Z", + "spec_version": "1.0", + "targets": { + "ctfe.pub": { + "custom": { + "sigstore": { + "status": "Active", + "usage": "CTFE" + } + }, + "hashes": { + "sha512": "b861189e48df51186a39612230fba6b02af951f7b35ad9375e8ca182d0e085d470e26d69f7cd4d7450a0f223991e8e5a4ddf8f1968caa15255de8e37035af43a" + }, + "length": 775 + }, + "ctfe_2022.pub": { + "custom": { + "sigstore": { + "status": "Active", + "usage": "CTFE" + } + }, + "hashes": { + "sha512": "ab975a75600fc366a837536d0dcba841b755552d21bb114498ff8ac9d2403f76643f5b91269bce5d124a365514719a3edee9dcc2b046cb173f51af659911fcd3" + }, + "length": 178 + }, + "ctfe_2022_2.pub": { + "custom": { + "sigstore": { + "status": "Active", + "usage": "CTFE" + } + }, + "hashes": { + "sha512": "3d035f94e1b14ac84627a28afdbed9a34861fb84239f76d73aa1a99f52262bfd95c4fa0ee71f1fd7e3bfb998d89cd5e0f0eafcff9fa7fa87c6e23484fc1e0cec" + }, + "length": 178 + }, + "fulcio.crt.pem": { + "custom": { + "sigstore": { + "status": "Active", + "usage": "Fulcio" + } + }, + "hashes": { + "sha512": "c69ae618883a0c89c282c0943a1ad0c16b0a7788f74e47a1adefc631dac48a0c4449d8c3de7455ae7d772e43c4a87e341f180b0614a46a86006969f8a7b84532" + }, + "length": 741 + }, + "fulcio_intermediate.crt.pem": { + "custom": { + "sigstore": { + "status": "Active", + "usage": "Fulcio" + } + }, + "hashes": { + "sha512": "90659875a02f73d1026055427c6d857c556e410e23748ff88aeb493227610fd2f5fbdd95ef2a21565f91438dfb3e073f50c4c9dd06f9a601b5d9b064d5cb60b4" + }, + "length": 790 + }, + "rekor.pub": { + "custom": { + "sigstore": { + "status": "Active", + "usage": "Rekor" + } + }, + "hashes": { + "sha512": "09ab08698a67354a95d3b8897d9ce7eaef05f06f5ed5f0202d79c228579858ecc5816b7e1b7cc6786abe7d6aaa758e1fcb05900cb749235186c3bf9522d6d7ce" + }, + "length": 178 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/test/unit/assets-staging-tuf/timestamp.json b/test/unit/assets-staging-tuf/timestamp.json new file mode 100644 index 000000000..acd7ca722 --- /dev/null +++ b/test/unit/assets-staging-tuf/timestamp.json @@ -0,0 +1,23 @@ +{ + "signatures": [ + { + "keyid": "8132b9a0526173757a3341d08079e4882c1d9b084f164fc397a572690183516b", + "sig": "d30a25f032304ce89235ec468781fecbe82e561a87d7fb531bfd94f4d312c6464b7dfa3d6774fbd9c47462e773868a2efeb654e8806f6d17efca18e1de5b6b03" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2032-10-20T18:54:05Z", + "meta": { + "snapshot.json": { + "hashes": { + "sha512": "8eeb2728174fa67f6e8300f7c85f3954d6d1235b415af32eee89390b921ca8c7b448435ad2954da2732f2b2ac9bbc126f69dbc99051289846d478e0ad5509557" + }, + "length": 928, + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 525e0ba1d..6bd50f91e 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -18,7 +18,10 @@ from typing import Tuple import pytest +from tuf.api.exceptions import DownloadHTTPError +from tuf.ngclient import FetcherInterface +from sigstore._internal import tuf from sigstore._internal.oidc.ambient import ( AmbientCredentialError, GitHubOidcPermissionCredentialError, @@ -31,6 +34,9 @@ _ASSETS = (Path(__file__).parent / "assets").resolve() assert _ASSETS.is_dir() +_TUF_ASSETS = (Path(__file__).parent / "assets-staging-tuf").resolve() +assert _TUF_ASSETS.is_dir() + def _is_ambient_env(): try: @@ -121,3 +127,18 @@ def verify(self, cert): return VerificationSuccess() return NullPolicy() + + +@pytest.fixture +def mock_staging_tuf(monkeypatch): + """Mock that prevents tuf module from making requests: it returns staging + assets from a local directory instead""" + + class MockFetcher(FetcherInterface): + def _fetch(self, url: str): + filename = _TUF_ASSETS / os.path.basename(url) + if filename.is_file(): + return open(filename, "rb") + raise DownloadHTTPError("File not found", 404) + + monkeypatch.setattr(tuf, "_fetcher", MockFetcher()) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 50d1660a2..bf68af93e 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -21,12 +21,13 @@ from sigstore._sign import Signer +@pytest.mark.online def test_signer_production(): signer = Signer.production() assert signer is not None -def test_signer_staging(): +def test_signer_staging(mock_staging_tuf): signer = Signer.staging() assert signer is not None diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 7017620f7..c04c59e59 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -19,12 +19,13 @@ from sigstore._verify.verifier import CertificateVerificationFailure, Verifier +@pytest.mark.online def test_verifier_production(): verifier = Verifier.production() assert verifier is not None -def test_verifier_staging(): +def test_verifier_staging(mock_staging_tuf): verifier = Verifier.staging() assert verifier is not None @@ -47,7 +48,9 @@ def test_verifier_multiple_verifications(signing_materials, null_policy): assert verifier.verify(materials, null_policy) -def test_verifier_offline_rekor_bundle(signing_materials, null_policy): +def test_verifier_offline_rekor_bundle( + signing_materials, null_policy, mock_staging_tuf +): materials = signing_materials("offline-rekor.txt") verifier = Verifier.staging() From 5e5b2800c4055529ab49b369f3011a0a16817269 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 14:21:21 +0200 Subject: [PATCH 21/38] tests: Don't require network in parametrized setup Signer.production() and Signer.staging() now require a network connection for TUF initialization: they can't be used in parametrized test setup as that happens even if the test is marked online. Signed-off-by: Jussi Kukkonen --- test/unit/test_sign.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index bf68af93e..30968737f 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -32,10 +32,7 @@ def test_signer_staging(mock_staging_tuf): assert signer is not None -@pytest.mark.online -@pytest.mark.ambient_oidc -@pytest.mark.parametrize("signer", [Signer.production(), Signer.staging()]) -def test_sign_rekor_entry_consistent(signer): +def _test_sign_rekor_entry_consistent(signer: Signer): token = detect_credential() assert token is not None @@ -48,3 +45,15 @@ def test_sign_rekor_entry_consistent(signer): assert expected_entry.integrated_time == actual_entry.integrated_time assert expected_entry.log_id == actual_entry.log_id assert expected_entry.log_index == actual_entry.log_index + + +@pytest.mark.online +@pytest.mark.ambient_oidc +def test_sign_rekor_entry_consistent_production(): + _test_sign_rekor_entry_consistent(Signer.production()) + + +@pytest.mark.online +@pytest.mark.ambient_oidc +def test_sign_rekor_entry_consistent_staging(): + _test_sign_rekor_entry_consistent(Signer.staging()) From 6a41e3a0fb63071486bfc0745e5283ce8db0654d Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 14:38:16 +0200 Subject: [PATCH 22/38] cli: Silence python-tuf logging a little Signed-off-by: Jussi Kukkonen --- sigstore/_cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index d36ebd7e9..9c7c20d60 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -53,7 +53,12 @@ ) logger = logging.getLogger(__name__) -logging.basicConfig(level=os.environ.get("SIGSTORE_LOGLEVEL", "INFO").upper()) +level = os.environ.get("SIGSTORE_LOGLEVEL", "INFO").upper() +logging.basicConfig(level=level) + +# workaround to make tuf less verbose https://github.com/theupdateframework/python-tuf/pull/2243 +if level == "INFO": + logging.getLogger("tuf").setLevel("WARNING") def _boolify_env(envvar: str) -> bool: From b54ed9f8f02f6616de251541800c26c6673fbfeb Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 15:51:49 +0200 Subject: [PATCH 23/38] tests: Add TrustUpdater test This test asserts that we make the network requests that we expect: * Uses mock staging TUF repository * Uses empty HOME dir to ensure known starting point for caches * tests both cold and hot caches Signed-off-by: Jussi Kukkonen --- test/unit/conftest.py | 26 +++++++++-- test/unit/internal/test_tuf.py | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 test/unit/internal/test_tuf.py diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 6bd50f91e..415e9c033 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -14,6 +14,7 @@ import base64 import os +from collections import defaultdict from pathlib import Path from typing import Tuple @@ -132,13 +133,30 @@ def verify(self, cert): @pytest.fixture def mock_staging_tuf(monkeypatch): """Mock that prevents tuf module from making requests: it returns staging - assets from a local directory instead""" + assets from a local directory instead + + Return a tuple of dicts with the requested files and counts""" + + success = defaultdict(int) + failure = defaultdict(int) class MockFetcher(FetcherInterface): def _fetch(self, url: str): - filename = _TUF_ASSETS / os.path.basename(url) - if filename.is_file(): - return open(filename, "rb") + filename = os.path.basename(url) + filepath = _TUF_ASSETS / filename + if filepath.is_file(): + success[filename] += 1 + return open(filepath, "rb") + + failure[filename] += 1 raise DownloadHTTPError("File not found", 404) monkeypatch.setattr(tuf, "_fetcher", MockFetcher()) + + return success, failure + + +@pytest.fixture +def temp_home(monkeypatch, tmp_path: Path): + """Set HOME to point to a test-specific tmp directory""" + monkeypatch.setenv("HOME", str(tmp_path)) diff --git a/test/unit/internal/test_tuf.py b/test/unit/internal/test_tuf.py new file mode 100644 index 000000000..383ad9375 --- /dev/null +++ b/test/unit/internal/test_tuf.py @@ -0,0 +1,80 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os + +from sigstore._internal.tuf import STAGING_TUF_URL, TrustUpdater, _get_dirs + + +def test_updater_staging_caches_and_requests(mock_staging_tuf, temp_home): + # start with empty target cache, empty local metadata dir + data_dir, cache_dir = _get_dirs(STAGING_TUF_URL) + + # keep track of successful and failed requests TrustUpdater makes + reqs, fail_reqs = mock_staging_tuf + + updater = TrustUpdater.staging() + # Expect root.json bootstrapped from _store + assert sorted(os.listdir(data_dir)) == ["root.json"] + # Expect no requests happened + assert reqs == {} + assert fail_reqs == {} + + updater.get_ctfe_keys() + # Expect local metadata to now contain all top-level metadata files + expected = ["root.json", "snapshot.json", "targets.json", "timestamp.json"] + assert sorted(os.listdir(data_dir)) == expected + # Expect requests of top-level metadata, and the ctfe targets + expected_requests = { + "ctfe.pub": 1, + "ctfe_2022.pub": 1, + "ctfe_2022_2.pub": 1, + "snapshot.json": 1, + "targets.json": 1, + "timestamp.json": 1, + } + expected_fail_reqs = {"2.root.json": 1} + assert reqs == expected_requests + # Expect 404 from the next root version + assert fail_reqs == expected_fail_reqs + + updater.get_rekor_key() + # Expect request of the rekor key but nothing else + expected_requests["rekor.pub"] = 1 + assert reqs == expected_requests + assert fail_reqs == expected_fail_reqs + + updater.get_rekor_key() + # Expect no requests + assert reqs == expected_requests + assert fail_reqs == expected_fail_reqs + + # New Updater instance, same cache dirs + updater = TrustUpdater.staging() + # Expect no requests happened + assert reqs == expected_requests + assert fail_reqs == expected_fail_reqs + + updater.get_ctfe_keys() + # Expect new timestamp and root requests + expected_requests["timestamp.json"] += 1 + expected_fail_reqs["2.root.json"] += 1 + assert reqs == expected_requests + assert fail_reqs == expected_fail_reqs + + updater.get_rekor_key() + # Expect no requests + assert reqs == expected_requests + assert fail_reqs == expected_fail_reqs From 68425ed7a1820991cd7530ec578a96f6088599b5 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Wed, 21 Dec 2022 16:02:06 +0200 Subject: [PATCH 24/38] tests: Add basic test for TrustUpdater Make sure the rekor key content is correct * use empty home dir * use mock TUF staging repository Signed-off-by: Jussi Kukkonen --- test/unit/conftest.py | 8 ++++++++ test/unit/internal/test_tuf.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 415e9c033..56db7a557 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -95,6 +95,14 @@ def _asset(name: str) -> Path: return _asset +@pytest.fixture +def tuf_asset(): + def _tuf_asset(name: str) -> Path: + return _TUF_ASSETS / name + + return _tuf_asset + + @pytest.fixture def signing_materials(): def _signing_materials(name: str) -> Tuple[bytes, bytes, bytes]: diff --git a/test/unit/internal/test_tuf.py b/test/unit/internal/test_tuf.py index 383ad9375..6a05be3da 100644 --- a/test/unit/internal/test_tuf.py +++ b/test/unit/internal/test_tuf.py @@ -78,3 +78,10 @@ def test_updater_staging_caches_and_requests(mock_staging_tuf, temp_home): # Expect no requests assert reqs == expected_requests assert fail_reqs == expected_fail_reqs + + +def test_updater_staging_get(mock_staging_tuf, temp_home, tuf_asset): + """Test that one of the get-methods returns the expected content""" + updater = TrustUpdater.staging() + with open(tuf_asset("rekor.pub"), "rb") as f: + assert updater.get_rekor_key() == f.read() From 4ad04ce0362cc425c3c02eac608f246f08f91232 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 11:04:45 -0500 Subject: [PATCH 25/38] _utils: lintage Signed-off-by: William Woodruff --- sigstore/_utils.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sigstore/_utils.py b/sigstore/_utils.py index 09a12c7f9..afdd29ff9 100644 --- a/sigstore/_utils.py +++ b/sigstore/_utils.py @@ -20,18 +20,12 @@ import base64 import hashlib -import sys from typing import IO, Union from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509 import Certificate -if sys.version_info < (3, 11): - import importlib_resources as resources -else: - from importlib import resources - PublicKey = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey] From ae9df017e3568d9c51ee8d194341387c07d31523 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:17:51 -0500 Subject: [PATCH 26/38] test/unit: put TUF assets under assets dir Signed-off-by: William Woodruff --- test/unit/assets-staging-tuf/ctfe.pub | 13 ------------- test/unit/assets-staging-tuf/ctfe_2022.pub | 4 ---- test/unit/assets-staging-tuf/ctfe_2022_2.pub | 4 ---- test/unit/assets-staging-tuf/fulcio.crt.pem | 13 ------------- .../assets-staging-tuf/fulcio_intermediate.crt.pem | 14 -------------- test/unit/assets-staging-tuf/rekor.pub | 4 ---- .../staging-tuf}/1.root.json | 0 .../staging-tuf}/snapshot.json | 0 .../staging-tuf}/targets.json | 0 .../staging-tuf}/timestamp.json | 0 test/unit/conftest.py | 2 +- 11 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 test/unit/assets-staging-tuf/ctfe.pub delete mode 100644 test/unit/assets-staging-tuf/ctfe_2022.pub delete mode 100644 test/unit/assets-staging-tuf/ctfe_2022_2.pub delete mode 100644 test/unit/assets-staging-tuf/fulcio.crt.pem delete mode 100644 test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem delete mode 100644 test/unit/assets-staging-tuf/rekor.pub rename test/unit/{assets-staging-tuf => assets/staging-tuf}/1.root.json (100%) rename test/unit/{assets-staging-tuf => assets/staging-tuf}/snapshot.json (100%) rename test/unit/{assets-staging-tuf => assets/staging-tuf}/targets.json (100%) rename test/unit/{assets-staging-tuf => assets/staging-tuf}/timestamp.json (100%) diff --git a/test/unit/assets-staging-tuf/ctfe.pub b/test/unit/assets-staging-tuf/ctfe.pub deleted file mode 100644 index 39512c214..000000000 --- a/test/unit/assets-staging-tuf/ctfe.pub +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN RSA PUBLIC KEY----- -MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3 -slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZG -z/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT5 -3cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXX -w4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K -6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZev -opmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lI -xNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0x -igwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYU -SeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7g -joCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ== ------END RSA PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/ctfe_2022.pub b/test/unit/assets-staging-tuf/ctfe_2022.pub deleted file mode 100644 index 3023b8618..000000000 --- a/test/unit/assets-staging-tuf/ctfe_2022.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bY -eSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA== ------END PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/ctfe_2022_2.pub b/test/unit/assets-staging-tuf/ctfe_2022_2.pub deleted file mode 100644 index 0f5eb8637..000000000 --- a/test/unit/assets-staging-tuf/ctfe_2022_2.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHq -c24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ== ------END PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/fulcio.crt.pem b/test/unit/assets-staging-tuf/fulcio.crt.pem deleted file mode 100644 index 47a5becff..000000000 --- a/test/unit/assets-staging-tuf/fulcio.crt.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAq -MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy -MDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUu -ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9 -BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUEC -CWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNj -MGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9C -Mrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6H -j2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm -45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTr -y3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw= ------END CERTIFICATE----- diff --git a/test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem b/test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem deleted file mode 100644 index d94a2aa40..000000000 --- a/test/unit/assets-staging-tuf/fulcio_intermediate.crt.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAq -MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy -MDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUu -ZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB -BgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9Kt -NfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWI -JEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF -BQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQF -Gn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjO -PQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1O -HHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/ -KX1SBrKQu220FmVL0Q== ------END CERTIFICATE----- diff --git a/test/unit/assets-staging-tuf/rekor.pub b/test/unit/assets-staging-tuf/rekor.pub deleted file mode 100644 index 4234e16c3..000000000 --- a/test/unit/assets-staging-tuf/rekor.pub +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9 -nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg== ------END PUBLIC KEY----- diff --git a/test/unit/assets-staging-tuf/1.root.json b/test/unit/assets/staging-tuf/1.root.json similarity index 100% rename from test/unit/assets-staging-tuf/1.root.json rename to test/unit/assets/staging-tuf/1.root.json diff --git a/test/unit/assets-staging-tuf/snapshot.json b/test/unit/assets/staging-tuf/snapshot.json similarity index 100% rename from test/unit/assets-staging-tuf/snapshot.json rename to test/unit/assets/staging-tuf/snapshot.json diff --git a/test/unit/assets-staging-tuf/targets.json b/test/unit/assets/staging-tuf/targets.json similarity index 100% rename from test/unit/assets-staging-tuf/targets.json rename to test/unit/assets/staging-tuf/targets.json diff --git a/test/unit/assets-staging-tuf/timestamp.json b/test/unit/assets/staging-tuf/timestamp.json similarity index 100% rename from test/unit/assets-staging-tuf/timestamp.json rename to test/unit/assets/staging-tuf/timestamp.json diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 56db7a557..79e56e128 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -35,7 +35,7 @@ _ASSETS = (Path(__file__).parent / "assets").resolve() assert _ASSETS.is_dir() -_TUF_ASSETS = (Path(__file__).parent / "assets-staging-tuf").resolve() +_TUF_ASSETS = (_ASSETS / "staging-tuf").resolve() assert _TUF_ASSETS.is_dir() From a210a6fd3c0e68f3b715b0c32675fe28738e062a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:21:49 -0500 Subject: [PATCH 27/38] tests/unit: re-parametrize Signed-off-by: William Woodruff --- test/unit/test_sign.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index 30968737f..cce76f528 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -32,7 +32,14 @@ def test_signer_staging(mock_staging_tuf): assert signer is not None -def _test_sign_rekor_entry_consistent(signer: Signer): +@pytest.mark.online +@pytest.mark.ambient_oidc +@pytest.mark.parametrize("signer", [Signer.production, Signer.staging]) +def test_sign_rekor_entry_consistent_production(signer): + # NOTE: The actual signer instance is produced lazily, so that parameter + # expansion doesn't fail in offline tests. + signer = signer() + token = detect_credential() assert token is not None @@ -45,15 +52,3 @@ def _test_sign_rekor_entry_consistent(signer: Signer): assert expected_entry.integrated_time == actual_entry.integrated_time assert expected_entry.log_id == actual_entry.log_id assert expected_entry.log_index == actual_entry.log_index - - -@pytest.mark.online -@pytest.mark.ambient_oidc -def test_sign_rekor_entry_consistent_production(): - _test_sign_rekor_entry_consistent(Signer.production()) - - -@pytest.mark.online -@pytest.mark.ambient_oidc -def test_sign_rekor_entry_consistent_staging(): - _test_sign_rekor_entry_consistent(Signer.staging()) From bbc6a9950bd3459d961654296446fd549a2872ae Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:27:16 -0500 Subject: [PATCH 28/38] _store, _utils: remove obsolete comment, re-add helper Signed-off-by: William Woodruff --- sigstore/_store/__init__.py | 18 ------------------ sigstore/_utils.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/sigstore/_store/__init__.py b/sigstore/_store/__init__.py index 1b42e49a5..1f5f2d0b9 100644 --- a/sigstore/_store/__init__.py +++ b/sigstore/_store/__init__.py @@ -26,21 +26,3 @@ # Why do we bother with `importlib` at all? Because we might be installed as a # ZIP file or an Egg, which in turn means that our resource files don't actually # exist separately on disk. `importlib` is the only reliable way to access them. - - -# Index of files by source: -# -# https://storage.googleapis.com/tuf-root-staging -# * ctfe.staging.pub -# * ctfe_2022.staging.pub -# * ctfe_2022.2.staging.pub -# * fulcio.crt.staging.pem -# * fulcio_intermediate.crt.staging.pem -# * rekor.staging.pub -# -# https://storage.googleapis.com/sigstore-tuf-root -# * ctfe.pub -# * ctfe_2022.pub -# * fulcio.crt.pem -# * fulcio_intermediate.crt.pem -# * rekor.pub diff --git a/sigstore/_utils.py b/sigstore/_utils.py index afdd29ff9..88fda5ed7 100644 --- a/sigstore/_utils.py +++ b/sigstore/_utils.py @@ -20,12 +20,18 @@ import base64 import hashlib +import sys from typing import IO, Union from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa from cryptography.x509 import Certificate +if sys.version_info < (3, 11): + import importlib_resources as resources +else: + from importlib import resources + PublicKey = Union[rsa.RSAPublicKey, ec.EllipticCurvePublicKey] @@ -145,3 +151,11 @@ def sha256_streaming(io: IO[bytes]) -> bytes: nbytes = io.readinto(view) # type: ignore return sha256.digest() + + +def read_embedded(name: str) -> bytes: + """ + Read a resource embedded in this distribution of sigstore-python, + returning its contents as bytes. + """ + return resources.files("sigstore._store").joinpath(name).read_bytes() From 69f249e785c8a68c7713a7be74a4501d05bd7729 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:27:29 -0500 Subject: [PATCH 29/38] test/unit: re-add store tests Signed-off-by: William Woodruff --- test/unit/test_store.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/unit/test_store.py diff --git a/test/unit/test_store.py b/test/unit/test_store.py new file mode 100644 index 000000000..6f6ac1ae0 --- /dev/null +++ b/test/unit/test_store.py @@ -0,0 +1,27 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from sigstore._utils import read_embedded + + +def test_store_reads_root_json(): + root_json = read_embedded("root.json") + assert json.loads(root_json) + + +def test_store_reads_staging_root_json(): + root_json = read_embedded("staging-root.json") + assert json.loads(root_json) From 03bdaf74ec8438ae0a8af7ba4325e81be0549d76 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:30:49 -0500 Subject: [PATCH 30/38] tuf: re-use our read_embedded helper Signed-off-by: William Woodruff --- sigstore/_internal/tuf.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 604c65fb5..31fcf354e 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -13,8 +13,6 @@ # limitations under the License. import logging -import shutil -from importlib import resources from pathlib import Path from typing import List, Optional, Tuple from urllib import parse @@ -22,6 +20,8 @@ import appdirs from tuf.ngclient import Updater +from sigstore._utils import read_embedded + logger = logging.getLogger(__name__) DEFAULT_TUF_URL = "https://sigstore-tuf-root.storage.googleapis.com/" @@ -75,8 +75,9 @@ def __init__(self, url: str) -> None: raise Exception(f"TUF root not found in {tuf_root}") self._metadata_dir.mkdir(parents=True, exist_ok=True) - with resources.path("sigstore._store", fname) as res: - shutil.copy2(res, tuf_root) + root_json = read_embedded(fname) + with tuf_root.open("wb") as io: + io.write(root_json) # intialize targets cache dir # TODO: Pre-populate with any targets we ship with sources From 476b8f44ad948046e0ddf4dc550e9f40252a28b8 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:32:39 -0500 Subject: [PATCH 31/38] README: update `--help` texts Signed-off-by: William Woodruff --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ba403744e..ce32508a2 100644 --- a/README.md +++ b/README.md @@ -138,12 +138,11 @@ Sigstore instance options: (default: https://rekor.sigstore.dev) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself - (conflicts with --staging) (default: rekor.pub - (embedded)) + (conflicts with --staging) (default: None) --fulcio-url URL The Fulcio instance to use (conflicts with --staging) (default: https://fulcio.sigstore.dev) --ctfe FILE A PEM-encoded public key for the CT log (conflicts - with --staging) (default: ctfe.pub (embedded)) + with --staging) (default: None) ``` @@ -198,8 +197,7 @@ Sigstore instance options: (default: https://rekor.sigstore.dev) --rekor-root-pubkey FILE A PEM-encoded root public key for Rekor itself - (conflicts with --staging) (default: rekor.pub - (embedded)) + (conflicts with --staging) (default: None) ``` From 3c88b2693f9a33a27af086d5d9a92d50502f146a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 21 Dec 2022 13:40:31 -0500 Subject: [PATCH 32/38] gitignore, test: allow staging-tuf assets Annoying. Signed-off-by: William Woodruff --- .gitignore | 6 ++---- test/unit/assets/staging-tuf/ctfe.pub | 13 +++++++++++++ test/unit/assets/staging-tuf/ctfe_2022.pub | 4 ++++ test/unit/assets/staging-tuf/ctfe_2022_2.pub | 4 ++++ test/unit/assets/staging-tuf/fulcio.crt.pem | 13 +++++++++++++ .../assets/staging-tuf/fulcio_intermediate.crt.pem | 14 ++++++++++++++ test/unit/assets/staging-tuf/rekor.pub | 4 ++++ 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 test/unit/assets/staging-tuf/ctfe.pub create mode 100644 test/unit/assets/staging-tuf/ctfe_2022.pub create mode 100644 test/unit/assets/staging-tuf/ctfe_2022_2.pub create mode 100644 test/unit/assets/staging-tuf/fulcio.crt.pem create mode 100644 test/unit/assets/staging-tuf/fulcio_intermediate.crt.pem create mode 100644 test/unit/assets/staging-tuf/rekor.pub diff --git a/.gitignore b/.gitignore index a8bdcd311..4533ac602 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,5 @@ build !sigstore/_store/*.crt !sigstore/_store/*.pem !sigstore/_store/*.pub -!test/unit/assets/*.txt -!test/unit/assets/*.crt -!test/unit/assets/*.sig -!test/unit/assets/*.rekor +!test/unit/assets/* +!test/unit/assets/staging-tuf/* diff --git a/test/unit/assets/staging-tuf/ctfe.pub b/test/unit/assets/staging-tuf/ctfe.pub new file mode 100644 index 000000000..39512c214 --- /dev/null +++ b/test/unit/assets/staging-tuf/ctfe.pub @@ -0,0 +1,13 @@ +-----BEGIN RSA PUBLIC KEY----- +MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3 +slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZG +z/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT5 +3cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXX +w4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K +6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZev +opmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lI +xNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0x +igwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYU +SeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7g +joCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ== +-----END RSA PUBLIC KEY----- diff --git a/test/unit/assets/staging-tuf/ctfe_2022.pub b/test/unit/assets/staging-tuf/ctfe_2022.pub new file mode 100644 index 000000000..3023b8618 --- /dev/null +++ b/test/unit/assets/staging-tuf/ctfe_2022.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bY +eSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA== +-----END PUBLIC KEY----- diff --git a/test/unit/assets/staging-tuf/ctfe_2022_2.pub b/test/unit/assets/staging-tuf/ctfe_2022_2.pub new file mode 100644 index 000000000..0f5eb8637 --- /dev/null +++ b/test/unit/assets/staging-tuf/ctfe_2022_2.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHq +c24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ== +-----END PUBLIC KEY----- diff --git a/test/unit/assets/staging-tuf/fulcio.crt.pem b/test/unit/assets/staging-tuf/fulcio.crt.pem new file mode 100644 index 000000000..47a5becff --- /dev/null +++ b/test/unit/assets/staging-tuf/fulcio.crt.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAq +MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy +MDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUu +ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9 +BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUEC +CWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNj +MGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9C +Mrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6H +j2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm +45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTr +y3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw= +-----END CERTIFICATE----- diff --git a/test/unit/assets/staging-tuf/fulcio_intermediate.crt.pem b/test/unit/assets/staging-tuf/fulcio_intermediate.crt.pem new file mode 100644 index 000000000..d94a2aa40 --- /dev/null +++ b/test/unit/assets/staging-tuf/fulcio_intermediate.crt.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAq +MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy +MDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUu +ZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIB +BgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9Kt +NfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWI +JEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF +BQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQF +Gn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjO +PQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1O +HHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/ +KX1SBrKQu220FmVL0Q== +-----END CERTIFICATE----- diff --git a/test/unit/assets/staging-tuf/rekor.pub b/test/unit/assets/staging-tuf/rekor.pub new file mode 100644 index 000000000..4234e16c3 --- /dev/null +++ b/test/unit/assets/staging-tuf/rekor.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9 +nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg== +-----END PUBLIC KEY----- From d9aa72c2e04477bf5e1d0b818f9ac31d4c91a651 Mon Sep 17 00:00:00 2001 From: Alex Cameron Date: Thu, 22 Dec 2022 16:47:19 +1100 Subject: [PATCH 33/38] tuf: Switch to using f-strings for logging Signed-off-by: Alex Cameron --- sigstore/_internal/tuf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 31fcf354e..3dca4a6b7 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -83,8 +83,8 @@ def __init__(self, url: str) -> None: # TODO: Pre-populate with any targets we ship with sources self._targets_dir.mkdir(parents=True, exist_ok=True) - logger.debug("TUF metadata: %s", self._metadata_dir) - logger.debug("TUF targets cache: %s", self._targets_dir) + logger.debug(f"TUF metadata: {self._metadata_dir}") + logger.debug(f"TUF targets cache: {self._targets_dir}") @classmethod def production(cls) -> "TrustUpdater": From b1fdc9fb6d5a901720d43865187d541b97a82b53 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 22 Dec 2022 10:41:34 +0200 Subject: [PATCH 34/38] test: document TUF staging mock better Signed-off-by: Jussi Kukkonen --- test/unit/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 79e56e128..4ec7d0abc 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -16,7 +16,7 @@ import os from collections import defaultdict from pathlib import Path -from typing import Tuple +from typing import Iterator, Tuple import pytest from tuf.api.exceptions import DownloadHTTPError @@ -149,11 +149,12 @@ def mock_staging_tuf(monkeypatch): failure = defaultdict(int) class MockFetcher(FetcherInterface): - def _fetch(self, url: str): + def _fetch(self, url: str) -> Iterator[bytes]: filename = os.path.basename(url) filepath = _TUF_ASSETS / filename if filepath.is_file(): success[filename] += 1 + # NOTE: leaves file open: could return a function yielding contents return open(filepath, "rb") failure[filename] += 1 From cf4e46f34a7c751fe0512b95eb07424d9ab19d0a Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 22 Dec 2022 11:24:53 +0200 Subject: [PATCH 35/38] _internal/rekor: Mention updater arg in docsstrings Signed-off-by: Jussi Kukkonen --- sigstore/_internal/rekor/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index f4ee461c0..95fedb9e6 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -464,6 +464,8 @@ def __del__(self) -> None: def production(cls, updater: TrustUpdater) -> RekorClient: """ Returns a `RekorClient` populated with the default Rekor production instance. + + updater must be a `TrustUpdater` for the production TUF repository. """ rekor_key = updater.get_rekor_key() ctfe_keys = updater.get_ctfe_keys() @@ -474,6 +476,8 @@ def production(cls, updater: TrustUpdater) -> RekorClient: def staging(cls, updater: TrustUpdater) -> RekorClient: """ Returns a `RekorClient` populated with the default Rekor staging instance. + + updater must be a `TrustUpdater` for the staging TUF repository. """ rekor_key = updater.get_rekor_key() ctfe_keys = updater.get_ctfe_keys() From be7a6d7b6b28d5f3a403f52040591bde2a781249 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 22 Dec 2022 11:27:36 +0200 Subject: [PATCH 36/38] _internal/tuf: Reword a TODO into a NOTE This is a potential improvement, not a necessary one. Signed-off-by: Jussi Kukkonen --- sigstore/_internal/tuf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 3dca4a6b7..eaef4d1d8 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -79,8 +79,8 @@ def __init__(self, url: str) -> None: with tuf_root.open("wb") as io: io.write(root_json) - # intialize targets cache dir - # TODO: Pre-populate with any targets we ship with sources + # Initialize targets cache dir + # NOTE: Could prime the cache here with any embedded certs/keys self._targets_dir.mkdir(parents=True, exist_ok=True) logger.debug(f"TUF metadata: {self._metadata_dir}") From b7c0bdb3834fa17f9acbcc48e5425d1b565b2a5e Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Thu, 22 Dec 2022 13:20:27 +0200 Subject: [PATCH 37/38] _internal/tuf: Add nosec for mypy-related assert Also tweak one annotation (remove unneeded quotes) Signed-off-by: Jussi Kukkonen --- sigstore/_internal/tuf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index eaef4d1d8..88ce67855 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -94,7 +94,7 @@ def production(cls) -> "TrustUpdater": def staging(cls) -> "TrustUpdater": return cls(STAGING_TUF_URL) - def _setup(self) -> "Updater": + def _setup(self) -> Updater: """Initialize and update the toplevel TUF metadata""" updater = Updater( metadata_dir=str(self._metadata_dir), @@ -115,7 +115,7 @@ def _get(self, usage: str) -> List[bytes]: self._updater = self._setup() data = [] - assert self._updater._trusted_set.targets + assert self._updater._trusted_set.targets # nosec: assert for mypys benefit targets = self._updater._trusted_set.targets.signed.targets for target_info in targets.values(): custom = target_info.unrecognized_fields["custom"]["sigstore"] From 4e7f680a1541a8b25e4794dd8770330c8e01a3eb Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 22 Dec 2022 11:03:38 -0500 Subject: [PATCH 38/38] _internal/tuf: replace nosec with type ignore Signed-off-by: William Woodruff --- sigstore/_internal/tuf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/tuf.py b/sigstore/_internal/tuf.py index 88ce67855..8d05031e2 100644 --- a/sigstore/_internal/tuf.py +++ b/sigstore/_internal/tuf.py @@ -115,8 +115,9 @@ def _get(self, usage: str) -> List[bytes]: self._updater = self._setup() data = [] - assert self._updater._trusted_set.targets # nosec: assert for mypys benefit - targets = self._updater._trusted_set.targets.signed.targets + + # NOTE: _updater has been fully initialized at this point, but mypy can't see that. + targets = self._updater._trusted_set.targets.signed.targets # type: ignore[union-attr] for target_info in targets.values(): custom = target_info.unrecognized_fields["custom"]["sigstore"] if custom["status"] == "Active" and custom["usage"] == usage: