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
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -813,7 +813,7 @@ def _verify_identity(args: argparse.Namespace) -> None:
)

try:
verifier.verify(
verifier.verify_artifact(
input_=hashed,
bundle=bundle,
policy=policy_,
Expand Down Expand Up @@ -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}")
Expand Down
21 changes: 21 additions & 0 deletions sigstore/_internal/rekor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import rekor_types
from cryptography.x509 import Certificate

from sigstore import dsse
from sigstore._utils import base64_encode_pem_cert
from sigstore.hashes import Hashed

Expand All @@ -30,6 +31,26 @@
__all__ = ["RekorClient", "SignedCheckpoint"]


def _dsse_from_parts(cert: Certificate, evp: dsse.Envelope) -> rekor_types.Dsse:
signature = rekor_types.dsse.Signature(
signature=evp._inner.signatures[0].sig,
verifier=base64_encode_pem_cert(cert),
)
return rekor_types.Dsse(
spec=rekor_types.dsse.DsseV001Schema(
signatures=[signature],
envelope_hash=rekor_types.dsse.EnvelopeHash(
algorithm=rekor_types.dsse.Algorithm.SHA256,
value=None,
),
payload_hash=rekor_types.dsse.PayloadHash(
algorithm=rekor_types.dsse.Algorithm.SHA256,
value=None,
),
)
)


# TODO: This should probably live somewhere better.
def _hashedrekord_from_parts(
cert: Certificate, sig: bytes, hashed: Hashed
Expand Down
46 changes: 40 additions & 6 deletions sigstore/dsse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -209,3 +220,26 @@ 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.

Assumes that the envelope only has a single signature.
"""

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

try:
# NB: Assumes only one signature in the DSSE envelope.
key.verify(evp._inner.signatures[0].sig, pae, ec.ECDSA(hashes.SHA256()))
except IndexError:
raise VerificationError("DSSE: no signature to verify")
except InvalidSignature:
raise VerificationError("DSSE: invalid signature")

return evp._inner.payload
11 changes: 11 additions & 0 deletions sigstore/verify/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
Loading