Skip to content

Initial DSSE verify APIs #962

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

Merged
merged 21 commits into from
Apr 15, 2024
Merged
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
4 changes: 2 additions & 2 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
@@ -813,7 +813,7 @@ def _verify_identity(args: argparse.Namespace) -> None:
)

try:
verifier.verify(
verifier.verify_artifact(
input_=hashed,
bundle=bundle,
policy=policy_,
@@ -851,7 +851,7 @@ def _verify_github(args: argparse.Namespace) -> None:
verifier, materials = _collect_verification_state(args)
for file, hashed, bundle in materials:
try:
verifier.verify(input_=hashed, bundle=bundle, policy=policy_)
verifier.verify_artifact(input_=hashed, bundle=bundle, policy=policy_)
print(f"OK: {file}")
except VerificationError as exc:
_logger.error(f"FAIL: {file}")
6 changes: 5 additions & 1 deletion sigstore/_internal/rekor/__init__.py
Original file line number Diff line number Diff line change
@@ -27,7 +27,11 @@
from .checkpoint import SignedCheckpoint
from .client import RekorClient

__all__ = ["RekorClient", "SignedCheckpoint"]
__all__ = [
"RekorClient",
"SignedCheckpoint",
"_hashedrekord_from_parts",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose at this point it should be just hashedrekord_from_parts() without underscore. Since rekor is private, it does not seem important tough...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it probably should since it's entirely private APIs. I'll iterate on this with the follow-up PRs for refactoring.

]


# TODO: This should probably live somewhere better.
50 changes: 44 additions & 6 deletions sigstore/dsse.py
Original file line number Diff line number Diff line change
@@ -21,12 +21,15 @@
import logging
from typing import Any, Dict, List, Literal, Optional, Union

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError
from sigstore_protobuf_specs.io.intoto import Envelope as _Envelope
from sigstore_protobuf_specs.io.intoto import Signature

from sigstore.errors import VerificationError

_logger = logging.getLogger(__name__)

_Digest = Union[
@@ -103,12 +106,7 @@ def _pae(self) -> bytes:
Construct the PAE encoding for this statement.
"""

# See:
# https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md
# https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md
pae = f"DSSEv1 {len(Envelope._TYPE)} {Envelope._TYPE} ".encode()
pae += b" ".join([str(len(self._contents)).encode(), self._contents])
return pae
return _pae(Envelope._TYPE, self._contents)


class _StatementBuilder:
@@ -193,6 +191,19 @@ def to_json(self) -> str:
return self._inner.to_json() # type: ignore[no-any-return]


def _pae(type_: str, body: bytes) -> bytes:
"""
Compute the PAE encoding for the given `type_` and `body`.
"""

# See:
# https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md
# https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md
pae = f"DSSEv1 {len(type_)} {type_} ".encode()
pae += b" ".join([str(len(body)).encode(), body])
return pae


def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope:
"""
Sign for the given in-toto `Statement`, and encapsulate the resulting
@@ -209,3 +220,30 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope:
signatures=[Signature(sig=signature, keyid=None)],
)
)


def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes:
"""
Verify the given in-toto `Envelope`, returning the verified inner payload.

This function does **not** check the envelope's payload type. The caller
is responsible for performing this check.
"""

pae = _pae(evp._inner.payload_type, evp._inner.payload)

if not evp._inner.signatures:
raise VerificationError("DSSE: envelope contains no signatures")

# In practice checking more than one signature here is frivolous, since
# they're all being checked against the same key. But there's no
# particular harm in checking them all either.
for signature in evp._inner.signatures:
try:
key.verify(signature.sig, pae, ec.ECDSA(hashes.SHA256()))
except InvalidSignature:
raise VerificationError("DSSE: invalid signature")

# TODO: Remove ignore when protobuf-specs contains a py.typed marker.
# See: <https://github.com/sigstore/protobuf-specs/pull/287>
return evp._inner.payload # type: ignore[no-any-return]
11 changes: 11 additions & 0 deletions sigstore/verify/models.py
Original file line number Diff line number Diff line change
@@ -245,6 +245,17 @@ def log_entry(self) -> LogEntry:
"""
return self._log_entry

@property
def _dsse_envelope(self) -> dsse.Envelope | None:
"""
Returns the DSSE envelope within this Bundle as a `dsse.Envelope`.

@private
"""
if self._inner.dsse_envelope:
return dsse.Envelope(self._inner.dsse_envelope)
return None

@classmethod
def from_json(cls, raw: bytes | str) -> Bundle:
"""
Loading