Skip to content

Commit

Permalink
Basic bundle tests (#69)
Browse files Browse the repository at this point in the history
* wip: cli_protocol: bundle flow

Signed-off-by: Andrew Pan <[email protected]>

* cli_protocol: formattinG

Signed-off-by: Andrew Pan <[email protected]>

* sigstore-python-conformance: shim *-bundle

Signed-off-by: Andrew Pan <[email protected]>

* test: basic bundle test cases

Signed-off-by: Andrew Pan <[email protected]>

* test_simple: parametrize -> mark.parametrize

Signed-off-by: Andrew Pan <[email protected]>

* sigstore-python-conformance: correct subcmd check

Signed-off-by: Andrew Pan <[email protected]>

* fixup! sigstore-python-conformance: correct subcmd check

Signed-off-by: Andrew Pan <[email protected]>

* conftest: return construct_materials_for_cls

Signed-off-by: Andrew Pan <[email protected]>

* test/client: all_exist -> exists

Signed-off-by: Andrew Pan <[email protected]>

* test_signature_verify: blank conditions

Signed-off-by: Andrew Pan <[email protected]>

* test_signature_verify: specialize sigcrt specific

Signed-off-by: Andrew Pan <[email protected]>

* test: reformat

Signed-off-by: Andrew Pan <[email protected]>

* treewide: resolve bad merge

Signed-off-by: Andrew Pan <[email protected]>

* test: yakshave naming for `make_materials`

Signed-off-by: Andrew Pan <[email protected]>

* test: `make lint`

Signed-off-by: Andrew Pan <[email protected]>

* test: Implement suggestions from code review

Signed-off-by: Andrew Pan <[email protected]>

* test: typehints for `make_materials` & co

Signed-off-by: Andrew Pan <[email protected]>

* test/conftest: doc new fixtures

Signed-off-by: Andrew Pan <[email protected]>

* test/client: doc changes from code review

Signed-off-by: Andrew Pan <[email protected]>

* test: prefix new type aliases with `_`

Signed-off-by: Andrew Pan <[email protected]>

---------

Signed-off-by: Andrew Pan <[email protected]>
  • Loading branch information
tnytown authored May 3, 2023
1 parent 06a7e51 commit 5766a8a
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 80 deletions.
2 changes: 1 addition & 1 deletion docs/cli_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ${ENTRYPOINT} sign --identity-token TOKEN --signature FILE --certificate FILE FI
#### Bundle flow

```console
${ENTRYPOINT} sign-bundle --bundle FILE FILE
${ENTRYPOINT} sign-bundle --identity-token TOKEN --bundle FILE FILE
```

| Option | Description |
Expand Down
10 changes: 10 additions & 0 deletions sigstore-python-conformance
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ A wrapper to convert `sigstore-conformance` CLI protocol invocations to match `s
import subprocess
import sys

SUBCMD_REPLACEMENTS = {
"sign-bundle": "sign",
"verify-bundle": "verify",
}

ARG_REPLACEMENTS = {
"--certificate-identity": "--cert-identity",
"--certificate-oidc-issuer": "--cert-oidc-issuer",
Expand All @@ -15,6 +20,11 @@ ARG_REPLACEMENTS = {
# Trim the script name.
fixed_args = sys.argv[1:]

# Substitute incompatible subcommands.
subcmd = fixed_args[0]
if subcmd in SUBCMD_REPLACEMENTS:
fixed_args[0] = SUBCMD_REPLACEMENTS[subcmd]

# Replace incompatible flags.
fixed_args = [
ARG_REPLACEMENTS[arg] if arg in ARG_REPLACEMENTS else arg for arg in fixed_args
Expand Down
171 changes: 157 additions & 14 deletions test/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import annotations

import os
import subprocess
from functools import singledispatchmethod
from pathlib import Path

CERTIFICATE_IDENTITY = (
"https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/"
Expand All @@ -8,13 +12,77 @@
CERTIFICATE_OIDC_ISSUER = "https://token.actions.githubusercontent.com"


class VerificationMaterials:
"""
A wrapper around verification materials. Materials can be either bundles
or detached pairs of signatures and certificates.
"""

@classmethod
def from_input(cls, input: Path) -> VerificationMaterials:
"""
Constructs a new set of materials from the given input path.
"""

raise NotImplementedError

def exists(self) -> bool:
"""
Checks if all contained materials exist at specified paths.
"""

raise NotImplementedError


class BundleMaterials(VerificationMaterials):
"""
Materials for commands that produce or consume bundles.
"""

bundle: Path

@classmethod
def from_input(cls, input: Path) -> BundleMaterials:
mats = cls()
mats.bundle = input.parent / f"{input.name}.sigstore"

return mats

def exists(self) -> bool:
return self.bundle.exists()


class SignatureCertificateMaterials(VerificationMaterials):
"""
Materials for commands that produce or consume signatures and certificates.
"""

signature: Path
certificate: Path

@classmethod
def from_input(cls, input: Path) -> SignatureCertificateMaterials:
mats = cls()
mats.signature = input.parent / f"{input.name}.sig"
mats.certificate = input.parent / f"{input.name}.crt"

return mats

def exists(self) -> bool:
return self.signature.exists() and self.certificate.exists()


class SigstoreClient:
"""
A wrapper around the Sigstore client under test that provides helpers to
access client functionality.
The `sigstore-conformance` test suite expects that clients expose a CLI that
adheres to the protocol outlined at `docs/cli_protocol.md`.
The `sign` and `verify` methods are dispatched over the two flows that clients
should support: signature/certificate and bundle. The overloads of those
methods should not be called directly.
"""

def __init__(self, entrypoint: str, identity_token: str) -> None:
Expand All @@ -38,45 +106,120 @@ def run(self, *args) -> None:
check=True,
)

def sign(
self, artifact: os.PathLike, signature: os.PathLike, certificate: os.PathLike
) -> None:
@singledispatchmethod
def sign(self, materials: VerificationMaterials, artifact: os.PathLike) -> None:
"""
Sign an artifact with the Sigstore client.
Sign an artifact with the Sigstore client. Dispatches to `_sign_for_sigcrt`
when given `SignatureCertificateMaterials`, or `_sign_for_bundle` when given
`BundleMaterials`.
`artifact` is a path to the file to sign.
`signature` is the path to write the generated signature to.
`certificate` is the path to write the signing certificate to.
`materials` contains paths to write the generated materials to.
"""

raise NotImplementedError(f"Cannot sign with {type(materials)}")

@sign.register
def _sign_for_sigcrt(
self, materials: SignatureCertificateMaterials, artifact: os.PathLike
) -> None:
"""
Sign an artifact with the Sigstore client, producing a signature and certificate.
This is an overload of `sign` for the signature/certificate flow and should not
be called directly.
"""

self.run(
"sign",
"--identity-token",
self.identity_token,
"--signature",
signature,
materials.signature,
"--certificate",
certificate,
materials.certificate,
artifact,
)

@sign.register
def _sign_for_bundle(
self, materials: BundleMaterials, artifact: os.PathLike
) -> None:
"""
Sign an artifact with the Sigstore client, producing a bundle.
This is an overload of `sign` for the bundle flow and should not be called directly.
"""

self.run(
"sign-bundle",
"--identity-token",
self.identity_token,
"--bundle",
materials.bundle,
artifact,
)

@singledispatchmethod
def verify(
self, artifact: os.PathLike, signature: os.PathLike, certificate: os.PathLike
self,
materials: VerificationMaterials,
artifact: os.PathLike,
) -> None:
"""
Verify an artifact with the Sigstore client.
Verify an artifact with the Sigstore client. Dispatches to `_verify_for_sigcrt`
when given `SignatureCertificateMaterials`, or `_verify_for_bundle` when given
`BundleMaterials`.
`artifact` is the path to the file to verify.
`signature` is the path to the signature to verify.
`certificate` is the path to the signing certificate to verify with.
`materials` contains paths to the materials to verify with.
"""

raise NotImplementedError(f"Cannot verify with {type(materials)}")

@verify.register
def _verify_for_sigcrt(
self,
materials: SignatureCertificateMaterials,
artifact: os.PathLike,
) -> None:
"""
Verify an artifact given a signature and certificate with the Sigstore client.
This is an overload of `verify` for the signature/certificate flow and should
not be called directly.
"""

# The identity and OIDC issuer cannot be specified by the test since they remain constant
# across the GitHub Actions job.
self.run(
"verify",
"--signature",
signature,
materials.signature,
"--certificate",
certificate,
materials.certificate,
"--certificate-identity",
CERTIFICATE_IDENTITY,
"--certificate-oidc-issuer",
CERTIFICATE_OIDC_ISSUER,
artifact,
)

@verify.register
def _verify_for_bundle(
self, materials: BundleMaterials, artifact: os.PathLike
) -> None:
"""
Verify an artifact given a bundle with the Sigstore client.
This is an overload of `verify` for the bundle flow and should not be called
directly.
"""

self.run(
"verify-bundle",
"--bundle",
materials.bundle,
"--certificate-identity",
CERTIFICATE_IDENTITY,
"--certificate-oidc-issuer",
Expand Down
50 changes: 42 additions & 8 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
import shutil
import tempfile
from pathlib import Path
from typing import Callable, Tuple, TypeVar

import pytest # type: ignore

from .client import SigstoreClient
from .client import (BundleMaterials, SignatureCertificateMaterials,
SigstoreClient, VerificationMaterials)

_M = TypeVar("_M", bound=VerificationMaterials)
_MakeMaterialsByType = Callable[[str, _M], Tuple[Path, _M]]
_MakeMaterials = Callable[[str], Tuple[Path, VerificationMaterials]]


def pytest_addoption(parser):
Expand All @@ -19,13 +25,6 @@ def pytest_addoption(parser):
required=True,
type=str,
)
parser.addoption(
"--identity-token",
action="store",
help="the OIDC token to supply to the Sigstore client under test",
required=True,
type=str,
)


@pytest.fixture
Expand All @@ -38,6 +37,41 @@ def client(pytestconfig):
return SigstoreClient(entrypoint, identity_token)


@pytest.fixture
def make_materials_by_type() -> _MakeMaterialsByType:
"""
Returns a function that constructs the requested subclass of
`VerificationMaterials` alongside an appropriate input path.
"""

def _make_materials_by_type(
input_name: str, cls: VerificationMaterials
) -> Tuple[Path, VerificationMaterials]:
input_path = Path(input_name)
output = cls.from_input(input_path)

return (input_path, output)

return _make_materials_by_type


@pytest.fixture(params=[BundleMaterials, SignatureCertificateMaterials])
def make_materials(request, make_materials_by_type) -> _MakeMaterials:
"""
Returns a function that constructs `VerificationMaterials` alongside an
appropriate input path. The subclass of `VerificationMaterials` that is returned
is parameterized across `BundleMaterials` and `SignatureCertificateMaterials`.
See `make_materials_by_type` for a fixture that uses a specific subclass of
`VerificationMaterials`.
"""

def _make_materials(input_name: str):
return make_materials_by_type(input_name, request.param)

return _make_materials


@pytest.fixture(autouse=True)
def workspace():
"""
Expand Down
8 changes: 6 additions & 2 deletions test/test_certificate_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest # type: ignore

from .client import SigstoreClient
from .client import SignatureCertificateMaterials, SigstoreClient


def test_verify_invalid_certificate_chain(client: SigstoreClient) -> None:
Expand All @@ -18,6 +18,10 @@ def test_verify_invalid_certificate_chain(client: SigstoreClient) -> None:
artifact_path = Path("a.txt")
signature_path = Path("a.txt.invalid.sig")
certificate_path = Path("a.txt.invalid.crt")
materials = SignatureCertificateMaterials()

materials.certificate = certificate_path
materials.signature = signature_path

with pytest.raises(subprocess.CalledProcessError):
client.verify(artifact_path, signature_path, certificate_path)
client.verify(materials, artifact_path)
Loading

0 comments on commit 5766a8a

Please sign in to comment.