Skip to content

Commit

Permalink
Support verifying digests in addition to artifacts
Browse files Browse the repository at this point in the history
Signed-off-by: Facundo Tuesca <[email protected]>
  • Loading branch information
facutuesca committed Sep 19, 2024
1 parent bcbeee6 commit 5c4cda3
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
cache: "pip"

- name: install sigstore-python
run: pip install "sigstore ~= 3.0"
run: pip install "sigstore >= 3.3.0, < 4.0"

- name: conformance test sigstore-python
uses: ./
Expand Down
5 changes: 3 additions & 2 deletions docs/cli_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ ${ENTRYPOINT} verify [--staging] --signature FILE --certificate FILE --certifica
#### Bundle flow

```console
${ENTRYPOINT} verify-bundle [--staging] --bundle FILE --certificate-identity IDENTITY --certificate-oidc-issuer URL [--trusted-root FILE] FILE
${ENTRYPOINT} verify-bundle [--staging] --bundle FILE --certificate-identity IDENTITY --certificate-oidc-issuer URL [--trusted-root FILE] [--verify-digest] FILE_OR_DIGEST
```

| Option | Description |
Expand All @@ -87,4 +87,5 @@ ${ENTRYPOINT} verify-bundle [--staging] --bundle FILE --certificate-identity IDE
| `--certificate-identity IDENTITY` | The expected identity in the signing certificate's SAN extension |
| `--certificate-oidc-issuer URL` | The expected OIDC issuer for the signing certificate |
| `--trusted-root` | The path of the custom trusted root to use to verify the bundle |
| `FILE` | The path to the artifact to verify |
| `--verify-digest` | Presence indicates client should interpret `FILE_OR_DIGEST` as a digest. |
| `FILE_OR_DIGEST` | The path to the artifact to verify, or its digest. The digest should start with the `sha256:` prefix. |
4 changes: 4 additions & 0 deletions sigstore-python-conformance
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ SUBCMD_REPLACEMENTS = {
ARG_REPLACEMENTS = {
"--certificate-identity": "--cert-identity",
"--certificate-oidc-issuer": "--cert-oidc-issuer",
# sigstore-python detects if the input is a file path or a digest without needing a flag
"--verify-digest": None,
}

# Trim the script name.
Expand Down Expand Up @@ -43,5 +45,7 @@ else:

# Replace incompatible flags.
command.extend(ARG_REPLACEMENTS[arg] if arg in ARG_REPLACEMENTS else arg for arg in fixed_args)
# Remove unneeded flags
command = [arg for arg in command if arg is not None]

os.execvp("sigstore", command)
40 changes: 35 additions & 5 deletions test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,13 @@ def _sign_for_bundle(self, materials: BundleMaterials, artifact: os.PathLike) ->
self.run(*args)

@singledispatchmethod
def verify(self, materials: VerificationMaterials, artifact: os.PathLike) -> None:
def verify(self, materials: VerificationMaterials, artifact: os.PathLike | str) -> None:
"""
Verify an artifact with the Sigstore client. Dispatches to `_verify_for_sigcrt`
when given `SignatureCertificateMaterials`, or `_verify_for_bundle` when given
`BundleMaterials`.
when given `SignatureCertificateMaterials`, or
`_verify_{artifact|digest}_for_bundle` when given `BundleMaterials`.
`artifact` is the path to the file to verify.
`artifact` is the path to the file to verify, or its digest.
`materials` contains paths to the materials to verify with.
"""

Expand Down Expand Up @@ -272,7 +272,9 @@ def _verify_for_sigcrt(
self.run(*args, artifact)

@verify.register
def _verify_for_bundle(self, materials: BundleMaterials, artifact: os.PathLike) -> None:
def _verify_artifact_for_bundle(
self, materials: BundleMaterials, artifact: os.PathLike
) -> None:
"""
Verify an artifact given a bundle with the Sigstore client.
Expand All @@ -297,3 +299,31 @@ def _verify_for_bundle(self, materials: BundleMaterials, artifact: os.PathLike)
args.extend(["--trusted-root", materials.trusted_root])

self.run(*args, artifact)

@verify.register
def _verify_digest_for_bundle(self, materials: BundleMaterials, digest: str) -> None:
"""
Verify a digest given a bundle with the Sigstore client.
This is an overload of `verify` for the bundle flow and should not be called
directly. The digest string is expected to start with the `sha256:` prefix.
"""
args: list[str | os.PathLike] = ["verify-bundle"]
if self.staging:
args.append("--staging")
args.extend(
[
"--bundle",
materials.bundle,
"--certificate-identity",
CERTIFICATE_IDENTITY,
"--certificate-oidc-issuer",
CERTIFICATE_OIDC_ISSUER,
"--verify-digest",
]
)

if getattr(materials, "trusted_root", None) is not None:
args.extend(["--trusted-root", materials.trusted_root])

self.run(*args, digest)
33 changes: 31 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import enum
import functools
import hashlib
import json
import os
import shutil
Expand All @@ -24,6 +26,7 @@
_M = TypeVar("_M", bound=VerificationMaterials)
_MakeMaterialsByType = Callable[[str, _M], tuple[Path, _M]]
_MakeMaterials = Callable[[str], tuple[Path, VerificationMaterials]]
_VerifyBundle = Callable[[VerificationMaterials, Path], None]

_OIDC_BEACON_API_URL = (
"https://api.github.com/repos/sigstore-conformance/extremely-dangerous-public-oidc-beacon/"
Expand Down Expand Up @@ -170,7 +173,7 @@ def client(pytestconfig, identity_token):


@pytest.fixture
def make_materials_by_type() -> _MakeMaterialsByType:
def make_materials_by_type(request) -> _MakeMaterialsByType:
"""
Returns a function that constructs the requested subclass of
`VerificationMaterials` alongside an appropriate input path.
Expand All @@ -182,7 +185,7 @@ def _make_materials_by_type(
input_path = Path(input_name)
output = cls.from_input(input_path)

return (input_path, output)
return input_path, output

return _make_materials_by_type

Expand All @@ -204,6 +207,32 @@ def _make_materials(input_name: str):
return _make_materials


class ArtifactInputType(enum.Enum):
PATH = enum.auto()
DIGEST = enum.auto()


@pytest.fixture(params=[ArtifactInputType.PATH, ArtifactInputType.DIGEST])
def verify_bundle(request, client) -> _VerifyBundle:
"""
Returns a function that verifies an artifact using the given verification materials
The fixture is parametrized to run twice, one verifying the artifact itself (passing
the file path to the verification function), and another verifying the artifact's
digest.
"""

def _verify_bundle(materials: VerificationMaterials, input_path: Path) -> None:
if request.param == ArtifactInputType.PATH:
client.verify(materials, input_path)
else:
with open(input_path, "rb") as f:
digest = f"sha256:{hashlib.sha256(f.read()).hexdigest()}"
client.verify(materials, digest)

return _verify_bundle


@pytest.fixture(autouse=True)
def workspace():
"""
Expand Down
Loading

0 comments on commit 5c4cda3

Please sign in to comment.