python-tuf/tuf/api/dsse.py
Lukas Puehringer f1afc98c95 lint: change base tuple order in SimpleEnvelope
Latest ruff complains about Generic[T] not being the last base class in
the bases tuple of Simple Envelope (generic-not-last-base-class
(PYI059)).

This commit applies the default fix by changing the order of the bases.
While this can change the MRO, there shouldn't be a change of behavior
given the used bases.

See https://docs.astral.sh/ruff/rules/generic-not-last-base-class/ for
details.

Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
2025-09-16 09:10:50 +02:00

153 lines
4.3 KiB
Python

"""Low-level TUF DSSE API. (experimental!)"""
from __future__ import annotations
import json
from typing import Generic, cast
from securesystemslib.dsse import Envelope as BaseSimpleEnvelope
# Expose all payload classes to use API independently of ``tuf.api.metadata``.
from tuf.api._payload import ( # noqa: F401
_ROOT,
_SNAPSHOT,
_TARGETS,
_TIMESTAMP,
SPECIFICATION_VERSION,
TOP_LEVEL_ROLE_NAMES,
BaseFile,
DelegatedRole,
Delegations,
MetaFile,
Role,
Root,
RootVerificationResult,
Signed,
Snapshot,
SuccinctRoles,
T,
TargetFile,
Targets,
Timestamp,
VerificationResult,
)
from tuf.api.serialization import DeserializationError, SerializationError
class SimpleEnvelope(BaseSimpleEnvelope, Generic[T]):
"""Dead Simple Signing Envelope (DSSE) for TUF payloads.
* Sign with ``self.sign()`` (inherited).
* Verify with ``verify_delegate`` on a ``Root`` or ``Targets``
object::
delegator.verify_delegate(
role_name,
envelope.pae(), # Note, how we don't pass ``envelope.payload``!
envelope.signatures,
)
Attributes:
payload: Serialized payload bytes.
payload_type: Payload string identifier.
signatures: Ordered dictionary of keyids to ``Signature`` objects.
"""
DEFAULT_PAYLOAD_TYPE = "application/vnd.tuf+json"
@classmethod
def from_bytes(cls, data: bytes) -> SimpleEnvelope[T]:
"""Load envelope from JSON bytes.
NOTE: Unlike ``tuf.api.metadata.Metadata.from_bytes``, this method
does not deserialize the contained payload. Use ``self.get_signed`` to
deserialize the payload into a ``Signed`` object.
Args:
data: envelope JSON bytes.
Raises:
tuf.api.serialization.DeserializationError:
data cannot be deserialized.
Returns:
TUF ``SimpleEnvelope`` object.
"""
try:
envelope_dict = json.loads(data.decode())
envelope = SimpleEnvelope.from_dict(envelope_dict)
except Exception as e:
raise DeserializationError from e
return cast("SimpleEnvelope[T]", envelope)
def to_bytes(self) -> bytes:
"""Return envelope as JSON bytes.
NOTE: Unlike ``tuf.api.metadata.Metadata.to_bytes``, this method does
not serialize the payload. Use ``SimpleEnvelope.from_signed`` to
serialize a ``Signed`` object and wrap it in an SimpleEnvelope.
Raises:
tuf.api.serialization.SerializationError:
self cannot be serialized.
"""
try:
envelope_dict = self.to_dict()
json_bytes = json.dumps(envelope_dict).encode()
except Exception as e:
raise SerializationError from e
return json_bytes
@classmethod
def from_signed(cls, signed: T) -> SimpleEnvelope[T]:
"""Serialize payload as JSON bytes and wrap in envelope.
Args:
signed: ``Signed`` object.
Raises:
tuf.api.serialization.SerializationError:
The signed object cannot be serialized.
"""
try:
signed_dict = signed.to_dict()
json_bytes = json.dumps(signed_dict).encode()
except Exception as e:
raise SerializationError from e
return cls(json_bytes, cls.DEFAULT_PAYLOAD_TYPE, {})
def get_signed(self) -> T:
"""Extract and deserialize payload JSON bytes from envelope.
Raises:
tuf.api.serialization.DeserializationError:
The signed object cannot be deserialized.
"""
try:
payload_dict = json.loads(self.payload.decode())
# TODO: can we move this to tuf.api._payload?
_type = payload_dict["_type"]
if _type == _TARGETS:
inner_cls: type[Signed] = Targets
elif _type == _SNAPSHOT:
inner_cls = Snapshot
elif _type == _TIMESTAMP:
inner_cls = Timestamp
elif _type == _ROOT:
inner_cls = Root
else:
raise ValueError(f'unrecognized role type "{_type}"')
except Exception as e:
raise DeserializationError from e
return cast("T", inner_cls.from_dict(payload_dict))