Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support verifying digests in addition to artifacts #158

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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. |
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
| `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)
31 changes: 30 additions & 1 deletion 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 @@ -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
Loading