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

Move DSSE Implementation to securesystemslib #487

Merged
merged 22 commits into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6a5efd2
Add DSSE Envelope
PradyumnaKrishna Jun 14, 2022
0807b01
Move b64enc and b64dec to util.py
PradyumnaKrishna Jun 14, 2022
26de11e
Code formatting and update documentation
PradyumnaKrishna Jun 15, 2022
58c7124
Add PreAuthEncoding Method
PradyumnaKrishna Jun 15, 2022
bcaca59
Add tests for metadata.py
PradyumnaKrishna Jun 17, 2022
34fd48b
Update docs and minor changes
PradyumnaKrishna Jun 20, 2022
ca2b1ad
Correct exception in from_dict
PradyumnaKrishna Jun 20, 2022
1ab712a
Envelope: Remove GPGSignature case handling
PradyumnaKrishna Dec 14, 2022
d1f9602
Changes related to DSSE envelope
PradyumnaKrishna Jul 21, 2022
b8225f7
Add sign and verify methods to DSSE envelope
PradyumnaKrishna Jul 22, 2022
7ec955d
Add tests for sign and verify methods of Envelope
PradyumnaKrishna Jul 22, 2022
ba23d4c
Add Serialization module with Abstract methods
PradyumnaKrishna Aug 9, 2022
cece2e9
Creation of JSON Serializer and Deserializer
PradyumnaKrishna Aug 10, 2022
7536f87
Add payload deserialization in DSSE Envelope
PradyumnaKrishna Aug 10, 2022
12c2690
Add test case for serialization.py
PradyumnaKrishna Aug 10, 2022
1b40254
Add JSONSerializable and default option
PradyumnaKrishna Aug 11, 2022
cd2381b
Add Todo and fix Type hints for SerializationMixin
PradyumnaKrishna Aug 16, 2022
aade3f2
Changes in serialization API
PradyumnaKrishna Sep 5, 2022
4c6be46
Fix failing tests
PradyumnaKrishna Dec 17, 2022
99f13cf
Rename metadata subpackage to dsse
PradyumnaKrishna Feb 13, 2023
1ecb06b
Remove serialization module
PradyumnaKrishna Feb 17, 2023
c7602b5
Update documentation and lint fix
PradyumnaKrishna Mar 3, 2023
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
8 changes: 8 additions & 0 deletions securesystemslib/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,11 @@ class UnverifiedSignatureError(Error):

class VerificationError(UnverifiedSignatureError):
"""Signature could not be verified because something failed in the process"""


class SerializationError(Error):
"""Error during serialization."""


class DeserializationError(Error):
"""Error during deserialization."""
218 changes: 218 additions & 0 deletions securesystemslib/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
"""Dead Simple Signing Envelope
"""

import logging
from typing import Any, Dict, List

from securesystemslib import exceptions, formats
from securesystemslib.serialization import (
BaseDeserializer,
BaseSerializer,
JSONDeserializer,
JSONSerializable,
JSONSerializer,
SerializationMixin,
)
from securesystemslib.signer import Key, Signature, Signer
from securesystemslib.util import b64dec, b64enc

logger = logging.getLogger(__name__)


class EnvelopeJSONDeserializer(JSONDeserializer):
"""Deserializes raw bytes and creates an Envelope object using JSON
Deserialization."""

def deserialize(self, raw_data: bytes) -> "Envelope":
"""Deserialize utf-8 encoded JSON bytes into an instance of Envelope.

Arguments:
raw_data: A utf-8 encoded bytes string.

Raises:
DeserializationError: If fails to deserialize raw_data.

Returns:
dict.
"""
try:
return Envelope.from_dict(super().deserialize(raw_data))
except Exception as e:
raise exceptions.DeserializationError(
"Failed to create Envelope"
) from e


class Envelope(SerializationMixin, JSONSerializable):
"""DSSE Envelope to provide interface for signing arbitrary data.

Attributes:
payload: Arbitrary byte sequence of serialized body.
payload_type: string that identifies how to interpret payload.
signatures: list of Signature.

"""

def __init__(
self, payload: bytes, payload_type: str, signatures: List[Signature]
):
self.payload = payload
self.payload_type = payload_type
self.signatures = signatures
lukpueh marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the decision to internally store signatures as a dict in Metadata was a good idea. Is there a reason it's a list here (other than simpler de/serialize)?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think we kept it a list for simplicity, and because that's what the DSSE spec describes. I'm fine with using a dict internally, if there's a clear usage advantage. Though I don't know if the same requirements apply as for TUF Metadata, since I expect Envelope objects to usually have a much shorter lifetime (also see theupdateframework/python-tuf#2246 (review)).

Copy link
Member

Choose a reason for hiding this comment

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

Any opinions about this, @PradyumnaKrishna?


def __eq__(self, other: Any) -> bool:
if not isinstance(other, Envelope):
return False

return (
self.payload == other.payload
and self.payload_type == other.payload_type
and self.signatures == other.signatures
)

@staticmethod
def _default_deserializer() -> BaseDeserializer:
return EnvelopeJSONDeserializer()

@staticmethod
def _default_serializer() -> BaseSerializer:
return JSONSerializer()

@classmethod
def from_dict(cls, data: dict) -> "Envelope":
"""Creates a DSSE Envelope from its JSON/dict representation.

Arguments:
data: A dict containing a valid payload, payloadType and signatures

Raises:
KeyError: If any of the "payload", "payloadType" and "signatures"
fields are missing from the "data".

FormatError: If signature in "signatures" is incorrect.

Returns:
A "Envelope" instance.
"""

payload = b64dec(data["payload"])
payload_type = data["payloadType"]

formats.SIGNATURES_SCHEMA.check_match(data["signatures"])
Copy link
Member

Choose a reason for hiding this comment

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

Let's not use schema/formats in any new code. The from_dict parser hierarchy should be format validation enough.

Copy link
Member

Choose a reason for hiding this comment

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

Ping

signatures = [
Signature.from_dict(signature) for signature in data["signatures"]
]

return cls(payload, payload_type, signatures)

def to_dict(self) -> dict:
"""Returns the JSON-serializable dictionary representation of self."""

return {
"payload": b64enc(self.payload),
"payloadType": self.payload_type,
"signatures": [
signature.to_dict() for signature in self.signatures
],
}

def pae(self) -> bytes:
"""Pre-Auth-Encoding byte sequence of self."""

return b"DSSEv1 %d %b %d %b" % (
len(self.payload_type),
self.payload_type.encode("utf-8"),
len(self.payload),
self.payload,
)

def sign(self, signer: Signer) -> Signature:
"""Sign the payload and create the signature.

Arguments:
signer: A "Signer" class instance.

Returns:
A "Signature" instance.
"""

signature = signer.sign(self.pae())
self.signatures.append(signature)
lukpueh marked this conversation as resolved.
Show resolved Hide resolved

return signature

def verify(self, keys: List[Key], threshold: int) -> Dict[str, Key]:
lukpueh marked this conversation as resolved.
Show resolved Hide resolved
"""Verify the payload with the provided Keys.

Arguments:
keys: A list of public keys to verify the signatures.
threshold: Number of signatures needed to pass the verification.

Raises:
ValueError: If "threshold" is not valid.
SignatureVerificationError: If the enclosed signatures do not pass
Copy link
Collaborator

Choose a reason for hiding this comment

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

SignatureVerificationError or VerificationError?

Copy link
Member

Choose a reason for hiding this comment

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

Ping

the verification.

Note:
Mandating keyid in signatures and matching them with keyid of Key
in order to consider them for verification, is not a DSSE spec
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
in order to consider them for verification, is not a DSSE spec
in order to consider them for verification, is not DSSE spec

Copy link
Member

Choose a reason for hiding this comment

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

Ping

compliant (Issue #416).

Returns:
accepted_keys: A dict of unique public keys.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't object to it being a dict if that's useful for other purposes but looking at the code it looks more like a set

Copy link
Member

Choose a reason for hiding this comment

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

Ping

Copy link
Collaborator

Choose a reason for hiding this comment

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

I resolved an earlier comment related to this... but I've decided I'm not happy about it: we should not return "accepted keys" that is not actually a complete list but just some of the valid keys.

This return value is also not part of the protocol spec.

To be clear: I don't object to the method returning a set of keys, but I do object to it returning a vaguely defined set of keys: returning None would be better than that.

Copy link
Member

Choose a reason for hiding this comment

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

Let's return None then for the time being so that we provide a spec compliant implementation. Also, AFAICS, we don't currently need the return value in in-toto.

"""

accepted_keys = {}
pae = self.pae()

# checks for threshold value.
if threshold <= 0:
raise ValueError("Threshold must be greater than 0")

if len(keys) < threshold:
raise ValueError("Number of keys can't be less than threshold")
jku marked this conversation as resolved.
Show resolved Hide resolved

for signature in self.signatures:
for key in keys:
# If Signature keyid doesn't match with Key, skip.
if not key.keyid == signature.keyid:
continue

# If a key verifies the signature, we exit and use the result.
try:
key.verify_signature(signature, pae)
accepted_keys[key.keyid] = key
break
except exceptions.UnverifiedSignatureError:
# TODO: Log, Raise or continue with error?
continue

# Break, if amount of recognized_signer are more than threshold.
if len(accepted_keys) >= threshold:
break
jku marked this conversation as resolved.
Show resolved Hide resolved

if threshold > len(accepted_keys):
raise exceptions.VerificationError(
"Accepted signatures do not match threshold,"
f" Found: {len(accepted_keys)}, Expected {threshold}"
)

return accepted_keys

def get_payload(
self,
deserializer: BaseDeserializer,
) -> Any:
"""Parse DSSE payload.

Arguments:
deserializer: ``BaseDeserializer`` implementation to use.

Raises:
DeserializationError: The payload cannot be deserialized.

Returns:
The deserialized object of payload.
"""

return deserializer.deserialize(self.payload)
Loading