diff --git a/test/integration/cli/test_sign.py b/test/integration/cli/test_sign.py new file mode 100644 index 00000000..b666e807 --- /dev/null +++ b/test/integration/cli/test_sign.py @@ -0,0 +1,333 @@ +# Copyright 2024 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 pathlib import Path +from typing import List, Optional + +import pytest + +from sigstore.models import Bundle +from sigstore.verify import Verifier +from sigstore.verify.policy import UnsafeNoOp + + +def get_cli_params( + artifact_paths: List[Path], + overwrite: bool = False, + no_default_files: bool = False, + output_directory: Optional[Path] = None, + bundle_path: Optional[Path] = None, + signature_path: Optional[Path] = None, + certificate_path: Optional[Path] = None, +) -> List[str]: + cli_params = ["--staging", "sign"] + if output_directory is not None: + cli_params.extend(["--output-directory", str(output_directory)]) + if bundle_path is not None: + cli_params.extend(["--bundle", str(bundle_path)]) + if signature_path is not None: + cli_params.extend(["--signature", str(signature_path)]) + if certificate_path is not None: + cli_params.extend(["--certificate", str(certificate_path)]) + if overwrite: + cli_params.append("--overwrite") + if no_default_files: + cli_params.append("--no-default-files") + + cli_params.extend([str(p) for p in artifact_paths]) + + return cli_params + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_sign_success_default_output_bundle(capsys, sigstore, asset): + artifact = asset("a.txt") + expected_output_bundle = artifact.with_name("a.txt.sigstore.json") + + assert not expected_output_bundle.exists() + sigstore( + *get_cli_params( + artifact_paths=[artifact], + ) + ) + + assert expected_output_bundle.exists() + verifier = Verifier.staging() + with open(expected_output_bundle, "r") as bundle_file, open( + artifact, "rb" + ) as input_file: + bundle = Bundle.from_json(bundle_file.read()) + verifier.verify_artifact( + input_=input_file.read(), bundle=bundle, policy=UnsafeNoOp() + ) + + expected_output_bundle.unlink() + + captures = capsys.readouterr() + assert captures.out.endswith( + f"Sigstore bundle written to {str(expected_output_bundle)}\n" + ) + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_sign_success_custom_outputs(capsys, sigstore, asset, tmp_path): + artifact = asset("a.txt") + output_bundle = tmp_path / "bundle.json" + output_cert = tmp_path / "cert.cert" + output_signature = tmp_path / "signature.sig" + + sigstore( + *get_cli_params( + artifact_paths=[artifact], + bundle_path=output_bundle, + certificate_path=output_cert, + signature_path=output_signature, + ) + ) + + assert output_bundle.exists() + assert output_cert.exists() + assert output_signature.exists() + + captures = capsys.readouterr() + assert captures.out.endswith( + f"Signature written to {str(output_signature)}\nCertificate written to {str(output_cert)}\nSigstore bundle written to {str(output_bundle)}\n" + ) + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_sign_success_custom_output_dir(capsys, sigstore, asset, tmp_path): + artifact = asset("a.txt") + expected_output_bundle = tmp_path / "a.txt.sigstore.json" + + sigstore( + *get_cli_params( + artifact_paths=[artifact], + output_directory=tmp_path, + ) + ) + + assert expected_output_bundle.exists() + + captures = capsys.readouterr() + assert captures.out.endswith( + f"Sigstore bundle written to {str(expected_output_bundle)}\n" + ) + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_sign_success_no_default_files(capsys, sigstore, asset, tmp_path): + artifact = asset("a.txt") + default_output_bundle = tmp_path / "a.txt.sigstore.json" + output_cert = tmp_path / "cert.cert" + output_signature = tmp_path / "sig.sig" + + sigstore( + *get_cli_params( + artifact_paths=[artifact], + signature_path=output_signature, + certificate_path=output_cert, + no_default_files=True, + ) + ) + assert output_cert.exists() + assert output_signature.exists() + assert not default_output_bundle.exists() + + captures = capsys.readouterr() + assert captures.out.endswith( + f"Signature written to {str(output_signature)}\nCertificate written to {str(output_cert)}\n" + ) + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_sign_overwrite_existing_bundle(capsys, sigstore, asset): + artifact = asset("a.txt") + expected_output_bundle = artifact.with_name("a.txt.sigstore.json") + + assert not expected_output_bundle.exists() + sigstore( + *get_cli_params( + artifact_paths=[artifact], + ) + ) + + assert expected_output_bundle.exists() + + sigstore( + *get_cli_params( + artifact_paths=[artifact], + overwrite=True, + ) + ) + assert expected_output_bundle.exists() + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + overwrite=False, + ) + ) + assert e.value.code == 2 + + captures = capsys.readouterr() + assert captures.err.endswith( + f"Refusing to overwrite outputs without --overwrite: {str(expected_output_bundle)}\n" + ) + + expected_output_bundle.unlink() + + +def test_sign_fails_with_default_files_and_bundle_options(capsys, sigstore, asset): + artifact = asset("a.txt") + output_bundle = artifact.with_name("a.txt.sigstore.json") + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + bundle_path=output_bundle, + no_default_files=True, + ) + ) + assert e.value.code == 2 + + captures = capsys.readouterr() + assert captures.err.endswith( + "--no-default-files may not be combined with --bundle.\n" + ) + + +def test_sign_fails_with_multiple_inputs_and_custom_output(capsys, sigstore, asset): + artifact = asset("a.txt") + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact, artifact], + bundle_path=artifact.with_name("a.txt.sigstore.json"), + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\n" + ) + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact, artifact], + certificate_path=artifact.with_name("a.txt.cert"), + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\n" + ) + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact, artifact], + signature_path=artifact.with_name("a.txt.sig"), + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature, --certificate, and --bundle can't be used with explicit outputs for multiple inputs.\n" + ) + + +def test_sign_fails_with_output_dir_and_custom_output_files(capsys, sigstore, asset): + artifact = asset("a.txt") + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + bundle_path=artifact.with_name("a.txt.sigstore.json"), + output_directory=artifact.parent, + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\n" + ) + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + certificate_path=artifact.with_name("a.txt.cert"), + output_directory=artifact.parent, + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\n" + ) + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + signature_path=artifact.with_name("a.txt.sig"), + output_directory=artifact.parent, + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature, --certificate, and --bundle can't be used with an explicit output directory.\n" + ) + + +def test_sign_fails_without_both_output_cert_and_signature(capsys, sigstore, asset): + artifact = asset("a.txt") + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + certificate_path=artifact.with_name("a.txt.cert"), + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature and --certificate must be used together.\n" + ) + + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + artifact_paths=[artifact], + signature_path=artifact.with_name("a.txt.sig"), + ) + ) + assert e.value.code == 2 + captures = capsys.readouterr() + assert captures.err.endswith( + "Error: --signature and --certificate must be used together.\n" + )