Skip to content

Commit

Permalink
Fix serialization in GPGSignature and formatting
Browse files Browse the repository at this point in the history
GPG signature requires `sig` on load but dumps `signature` key in a
dict which should be the same key name #418.
Some formatting fixes are made to the singer.py.

Signed-off-by: Pradyumna Krishna <git@onpy.in>
  • Loading branch information
PradyumnaKrishna committed Jul 11, 2022
1 parent 873f276 commit ecd7ef0
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 50 deletions.
44 changes: 17 additions & 27 deletions securesystemslib/signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"""

import abc
from typing import Any, Dict, Optional, Mapping

import securesystemslib.keys as sslib_keys
import securesystemslib.gpg.functions as gpg
from typing import Any, Dict, Optional, Mapping


class Signature:
Expand All @@ -27,17 +28,17 @@ class Signature:
by securesystemslib.
"""

def __init__(
self,
keyid: str,
sig: str,
unrecognized_fields: Optional[Mapping[str, Any]] = None
unrecognized_fields: Optional[Mapping[str, Any]] = None,
):
self.keyid = keyid
self.signature = sig
self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {}


def __eq__(self, other: Any) -> bool:
if not isinstance(other, Signature):
return False
Expand All @@ -48,7 +49,6 @@ def __eq__(self, other: Any) -> bool:
and self.unrecognized_fields == other.unrecognized_fields
)


@classmethod
def from_dict(cls, signature_dict: Dict) -> "Signature":
"""Creates a Signature object from its JSON/dict representation.
Expand All @@ -73,10 +73,7 @@ def from_dict(cls, signature_dict: Dict) -> "Signature":
keyid = signature_dict.pop("keyid")
sig = signature_dict.pop("sig")
# All fields left in the signature_dict are unrecognized.
return cls(
keyid, sig, signature_dict
)

return cls(keyid, sig, signature_dict)

def to_dict(self) -> Dict:
"""Returns the JSON-serializable dictionary representation of self."""
Expand All @@ -88,7 +85,6 @@ def to_dict(self) -> Dict:
}



class GPGSignature(Signature):
"""A container class containing information about a gpg signature.
Expand All @@ -100,6 +96,7 @@ class GPGSignature(Signature):
signature: HEX string representing the signature.
other_headers: HEX representation of additional GPG headers.
"""

def __init__(
self,
keyid: str,
Expand All @@ -109,9 +106,8 @@ def __init__(
super().__init__(keyid, signature)
self.other_headers = other_headers


@classmethod
def from_dict(cls, signature_dict: Dict) -> "Signature":
def from_dict(cls, signature_dict: Dict) -> "GPGSignature":
"""Creates a GPGSignature object from its JSON/dict representation.
Args:
Expand All @@ -128,28 +124,26 @@ def from_dict(cls, signature_dict: Dict) -> "Signature":

return cls(
signature_dict["keyid"],
signature_dict["sig"],
signature_dict["other_headers"]
signature_dict["signature"],
signature_dict["other_headers"],
)


def to_dict(self) -> Dict:
"""Returns the JSON-serializable dictionary representation of self."""
return {
"keyid": self.keyid,
"signature": self.signature,
"other_headers": self.other_headers
"other_headers": self.other_headers,
}



class Signer:
"""Signer interface created to support multiple signing implementations."""

__metaclass__ = abc.ABCMeta

@abc.abstractmethod
def sign(self, payload: bytes) -> "Signature":
def sign(self, payload: bytes) -> Signature:
"""Signs a given payload by the key assigned to the Signer instance.
Arguments:
Expand All @@ -158,8 +152,7 @@ def sign(self, payload: bytes) -> "Signature":
Returns:
Returns a "Signature" class instance.
"""
raise NotImplementedError # pragma: no cover

raise NotImplementedError # pragma: no cover


class SSlibSigner(Signer):
Expand Down Expand Up @@ -193,11 +186,11 @@ class SSlibSigner(Signer):
The public and private keys are strings in PEM format.
"""

def __init__(self, key_dict: Dict):
self.key_dict = key_dict


def sign(self, payload: bytes) -> "Signature":
def sign(self, payload: bytes) -> Signature:
"""Signs a given payload by the key assigned to the SSlibSigner instance.
Arguments:
Expand All @@ -217,7 +210,6 @@ def sign(self, payload: bytes) -> "Signature":
return Signature(**sig_dict)



class GPGSigner(Signer):
"""A securesystemslib gpg implementation of the "Signer" interface.
Expand All @@ -232,14 +224,12 @@ class GPGSigner(Signer):
is used.
"""
def __init__(
self, keyid: Optional[str] = None, homedir: Optional[str] = None
):

def __init__(self, keyid: Optional[str] = None, homedir: Optional[str] = None):
self.keyid = keyid
self.homedir = homedir


def sign(self, payload: bytes) -> "GPGSignature":
def sign(self, payload: bytes) -> GPGSignature:
"""Signs a given payload by the key assigned to the GPGSigner instance.
Calls the gpg command line utility to sign the passed content with the
Expand Down
50 changes: 27 additions & 23 deletions tests/test_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@
import securesystemslib.formats
import securesystemslib.keys as KEYS
from securesystemslib.exceptions import FormatError, UnsupportedAlgorithmError
from securesystemslib.signer import Signature, SSlibSigner, GPGSigner
from securesystemslib.signer import GPGSignature, Signature, SSlibSigner, GPGSigner
from securesystemslib.gpg.constants import HAVE_GPG
from securesystemslib.gpg.functions import (
export_pubkey,
verify_signature as verify_sig
)
from securesystemslib.gpg.functions import export_pubkey, verify_signature as verify_sig


class TestSSlibSigner(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.rsakey_dict = KEYS.generate_rsa_key()
cls.ed25519key_dict = KEYS.generate_ed25519_key()
cls.ecdsakey_dict = KEYS.generate_ecdsa_key()
cls.DATA_STR = "SOME DATA REQUIRING AUTHENTICITY."
cls.DATA = securesystemslib.formats.encode_canonical(
cls.DATA_STR).encode("utf-8")

cls.DATA = securesystemslib.formats.encode_canonical(cls.DATA_STR).encode(
"utf-8"
)

def test_sslib_sign(self):
dicts = [self.rsakey_dict, self.ecdsakey_dict, self.ed25519key_dict]
Expand All @@ -39,8 +35,7 @@ def test_sslib_sign(self):
sig_obj = sslib_signer.sign(self.DATA)

# Verify signature
verified = KEYS.verify_signature(scheme_dict, sig_obj.to_dict(),
self.DATA)
verified = KEYS.verify_signature(scheme_dict, sig_obj.to_dict(), self.DATA)
self.assertTrue(verified, "Incorrect signature.")

# Removing private key from "scheme_dict".
Expand All @@ -63,12 +58,11 @@ def test_sslib_sign(self):

scheme_dict["scheme"] = valid_scheme


def test_signature_from_to_dict(self):
signature_dict = {
"sig": "30460221009342e4566528fcecf6a7a5d53ebacdb1df151e242f55f8775883469cb01dbc6602210086b426cc826709acfa2c3f9214610cb0a832db94bbd266fd7c5939a48064a851",
"keyid": "11fa391a0ed7a447cbfeb4b2667e286fc248f64d5e6d0eeed2e5e23f97f9f714",
"foo": "bar" # unrecognized_field
"foo": "bar", # unrecognized_field
}
sig_obj = Signature.from_dict(copy.copy(signature_dict))

Expand All @@ -77,11 +71,10 @@ def test_signature_from_to_dict(self):

self.assertDictEqual(signature_dict, sig_obj.to_dict())


def test_signature_eq_(self):
signature_dict = {
"sig": "30460221009342e4566528fcecf6a7a5d53ebacdb1df151e242f55f8775883469cb01dbc6602210086b426cc826709acfa2c3f9214610cb0a832db94bbd266fd7c5939a48064a851",
"keyid": "11fa391a0ed7a447cbfeb4b2667e286fc248f64d5e6d0eeed2e5e23f97f9f714"
"keyid": "11fa391a0ed7a447cbfeb4b2667e286fc248f64d5e6d0eeed2e5e23f97f9f714",
}
sig_obj = Signature.from_dict(signature_dict)
sig_obj_2 = copy.deepcopy(sig_obj)
Expand All @@ -101,6 +94,7 @@ def test_signature_eq_(self):
sig_obj_2 = None
self.assertNotEqual(sig_obj, sig_obj_2)


@unittest.skipIf(not HAVE_GPG, "gpg not found")
class TestGPGRSA(unittest.TestCase):
"""Test RSA gpg signature creation and verification."""
Expand All @@ -112,29 +106,28 @@ def setUpClass(cls):

# Create directory to run the tests without having everything blow up.
cls.working_dir = os.getcwd()
cls.test_data = b'test_data'
cls.wrong_data = b'something malicious'
cls.test_data = b"test_data"
cls.wrong_data = b"something malicious"

# Find demo files.
gpg_keyring_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa")
os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa"
)

cls.test_dir = os.path.realpath(tempfile.mkdtemp())
cls.gnupg_home = os.path.join(cls.test_dir, "rsa")
shutil.copytree(gpg_keyring_path, cls.gnupg_home)
os.chdir(cls.test_dir)


@classmethod
def tearDownClass(cls):
"""Change back to initial working dir and remove temp test directory."""

os.chdir(cls.working_dir)
shutil.rmtree(cls.test_dir)


def test_gpg_sign_and_verify_object_with_default_key(self):
"""Create a signature using the default key on the keyring. """
"""Create a signature using the default key on the keyring."""

signer = GPGSigner(homedir=self.gnupg_home)
signature = signer.sign(self.test_data)
Expand All @@ -145,9 +138,8 @@ def test_gpg_sign_and_verify_object_with_default_key(self):
self.assertTrue(verify_sig(signature_dict, key_data, self.test_data))
self.assertFalse(verify_sig(signature_dict, key_data, self.wrong_data))


def test_gpg_sign_and_verify_object(self):
"""Create a signature using a specific key on the keyring. """
"""Create a signature using a specific key on the keyring."""

signer = GPGSigner(self.signing_subkey_keyid, self.gnupg_home)
signature = signer.sign(self.test_data)
Expand All @@ -158,6 +150,18 @@ def test_gpg_sign_and_verify_object(self):
self.assertTrue(verify_sig(signature_dict, key_data, self.test_data))
self.assertFalse(verify_sig(signature_dict, key_data, self.wrong_data))

def test_gpg_serialization(self):
"""Tests from_dict and to_dict methods of GPGSignature."""

sig_dict = {
"keyid": "f4f90403af58eef6",
"signature": "c39f86e70e12e70e11d87eb7e3ab7d3b",
"other_headers": "d8f8a89b5d71f07b842a",
}

signature = GPGSignature.from_dict(sig_dict)
self.assertEqual(sig_dict, signature.to_dict())


# Run the unit tests.
if __name__ == "__main__":
Expand Down

0 comments on commit ecd7ef0

Please sign in to comment.