Skip to content

Commit

Permalink
Merge pull request #1 from PradyumnaKrishna/dsse_envelope
Browse files Browse the repository at this point in the history
WIP: Add DSSE Envelope
  • Loading branch information
lukpueh authored Jun 20, 2022
2 parents 015233d + b5daee6 commit d3f4888
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 0 deletions.
99 changes: 99 additions & 0 deletions securesystemslib/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Dead Simple Signing Envelope
"""

from typing import Any, List

from securesystemslib import exceptions, formats
from securesystemslib.signer import Signature
from securesystemslib.util import b64dec, b64enc


class Envelope:
"""
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 and GPG Signature
Methods:
from_dict(cls, data):
Creates a Signature object from its JSON/dict representation.
to_dict(self):
Returns the JSON-serializable dictionary representation of self.
"""

payload: bytes
payload_type: str
signatures: List[Signature]

def __init__(self, payload, payload_type, signatures):
self.payload = payload
self.payload_type = payload_type
self.signatures = signatures

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
)

@classmethod
def from_dict(cls, data: dict) -> "Envelope":
"""Creates a Signature object 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"]

signatures = []
for signature in data["signatures"]:
if formats.GPG_SIGNATURE_SCHEMA.matches(signature):
raise NotImplementedError

if formats.SIGNATURE_SCHEMA.matches(signature):
signatures.append(Signature.from_dict(signature))

else:
raise exceptions.FormatError("Wanted a 'Signature'.")

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],
}

@property
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,
)
39 changes: 39 additions & 0 deletions securesystemslib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
that tries to import a working json module, load_json_* functions, etc.
"""

import base64
import binascii
import json
import os
import logging
Expand Down Expand Up @@ -455,3 +457,40 @@ def digests_are_equal(digest1: str, digest2: str) -> bool:
are_equal = False

return are_equal


def b64enc(data: bytes) -> str:
"""To encode byte sequence into base64 string
Arguments:
data: Byte sequence to encode
Exceptions:
TypeError: If "data" is not byte sequence
Returns:
base64 string
"""

return base64.standard_b64encode(data).decode('utf-8')


def b64dec(string: str) -> bytes:
"""To decode byte sequence from base64 string
Arguments:
string: base64 string to decode
Raises:
binascii.Error: If invalid base64-encoded string
Returns:
A byte sequence
"""

data = string.encode('utf-8')
try:
return base64.b64decode(data, validate=True)
except binascii.Error:
# altchars for urlsafe encoded base64 - instead of + and _ instead of /
return base64.b64decode(data, altchars=b'-_', validate=True)
82 changes: 82 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python

"""Test cases for "metadata.py". """

import copy
import unittest

from securesystemslib import exceptions
from securesystemslib.metadata import Envelope
from securesystemslib.signer import Signature


class TestEnvelope(unittest.TestCase):
"""Test metadata interface provided by DSSE envelope."""

@classmethod
def setUpClass(cls):
cls.signature_dict = {
"keyid": "11fa391a0ed7a447cbfeb4b2667e286fc248f64d5e6d0eeed2e5e23f97f9f714",
"sig": "30460221009342e4566528fcecf6a7a5d53ebacdb1df151e242f55f8775883469cb01dbc6602210086b426cc826709acfa2c3f9214610cb0a832db94bbd266fd7c5939a48064a851",
}
cls.envelope_dict = {
"payload": "aGVsbG8gd29ybGQ=",
"payloadType": "http://example.com/HelloWorld",
"signatures": [cls.signature_dict],
}
cls.pae = b"DSSEv1 29 http://example.com/HelloWorld 11 hello world"

def test_envelope_from_to_dict(self):
"""Test envelope to_dict and from_dict methods"""

envelope_dict = copy.deepcopy(self.envelope_dict)

# create envelope object from its dict.
envelope_obj = Envelope.from_dict(envelope_dict)

# Assert envelope dict created by to_dict will be equal.
self.assertDictEqual(self.envelope_dict, envelope_obj.to_dict())

# Assert TypeError on invalid signature
envelope_dict["signatures"] = [""]
self.assertRaises(exceptions.FormatError, Envelope.from_dict, envelope_dict)

def test_envelope_eq_(self):
"""Test envelope equality"""

envelope_obj = Envelope.from_dict(copy.deepcopy(self.envelope_dict))

# Assert that object and None will not be equal.
self.assertNotEqual(None, envelope_obj)

# Assert a copy of envelope_obj will be equal to envelope_obj.
envelope_obj_2 = copy.deepcopy(envelope_obj)
self.assertEqual(envelope_obj, envelope_obj_2)

# Assert that changing the "payload" will make the objects not equal.
envelope_obj_2.payload = b"wrong_payload"
self.assertNotEqual(envelope_obj, envelope_obj_2)
envelope_obj_2.payload = envelope_obj.payload

# Assert that changing the "payload_type" will make the objects not equal.
envelope_obj_2.payload_type = "wrong_payload_type"
self.assertNotEqual(envelope_obj, envelope_obj_2)
envelope_obj_2.payload = envelope_obj.payload

# Assert that changing the "signatures" will make the objects not equal.
sig_obg = Signature("", self.signature_dict["sig"])
envelope_obj_2.signatures = [sig_obg]
self.assertNotEqual(envelope_obj, envelope_obj_2)

def test_preauthencoding(self):
"""Test envelope Pre-Auth-Encoding"""

envelope_obj = Envelope.from_dict(copy.deepcopy(self.envelope_dict))

# Checking for Pre-Auth-Encoding generated is correct.
self.assertEqual(self.pae, envelope_obj.pae)


# Run the unit tests.
if __name__ == "__main__":
unittest.main()

0 comments on commit d3f4888

Please sign in to comment.