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

Make GPGSigner.sign return Signature #486

Merged
merged 4 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 securesystemslib/signer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
Some implementations are provided by default but more can be added by users.
"""
from securesystemslib.signer._gcp_signer import GCPSigner
from securesystemslib.signer._gpg_signer import GPGSigner
from securesystemslib.signer._hsm_signer import HSMSigner
from securesystemslib.signer._key import KEY_FOR_TYPE_AND_SCHEME, Key, SSlibKey
from securesystemslib.signer._signature import GPGSignature, Signature
from securesystemslib.signer._signature import Signature
from securesystemslib.signer._signer import (
SIGNER_FOR_URI_SCHEME,
GPGSigner,
SecretsHandler,
Signer,
SSlibSigner,
Expand Down
75 changes: 75 additions & 0 deletions securesystemslib/signer/_gpg_signer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Signer implementation for OpenPGP """
from typing import Dict, Optional

import securesystemslib.gpg.functions as gpg
from securesystemslib.signer._key import Key
from securesystemslib.signer._signer import SecretsHandler, Signature, Signer


class GPGSigner(Signer):
"""OpenPGP Signer

Runs command in ``GNUPG`` environment variable to sign, fallback commands are
``gpg2`` and ``gpg``.

Supported signing schemes are: "pgp+rsa-pkcsv1.5", "pgp+dsa-fips-180-2" and
"pgp+eddsa-ed25519", with SHA-256 hashing.


Arguments:
keyid: GnuPG local user signing key id. If not passed, the default key is used.
homedir: GnuPG home directory path. If not passed, the default homedir is used.

"""

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

@classmethod
def from_priv_key_uri(
cls,
priv_key_uri: str,
public_key: Key,
secrets_handler: Optional[SecretsHandler] = None,
) -> "GPGSigner":
raise NotImplementedError("Incompatible with private key URIs")

@staticmethod
def _to_gpg_sig(sig: Signature) -> Dict:
"""Helper to convert Signature -> internal gpg signature format."""
sig_dict = sig.to_dict()
sig_dict["signature"] = sig_dict.pop("sig")
return sig_dict

@staticmethod
def _from_gpg_sig(sig_dict: Dict) -> Signature:
"""Helper to convert internal gpg signature format -> Signature."""
sig_dict["sig"] = sig_dict.pop("signature")
return Signature.from_dict(sig_dict)

def sign(self, payload: bytes) -> Signature:
"""Signs payload with ``gpg``.

Arguments:
payload: bytes to be signed.

Raises:
ValueError: The gpg command failed to create a valid signature.
OSError: the gpg command is not present or non-executable.
securesystemslib.exceptions.UnsupportedLibraryError: The gpg
command is not available, or the cryptography library is
not installed.
securesystemslib.gpg.exceptions.CommandError: The gpg command
returned a non-zero exit code.
securesystemslib.gpg.exceptions.KeyNotFoundError: The used gpg
version is not fully supported.

Returns:
Signature.
"""
return self._from_gpg_sig(
gpg.create_signature(payload, self.keyid, self.homedir)
)
52 changes: 0 additions & 52 deletions securesystemslib/signer/_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,55 +81,3 @@ def to_dict(self) -> Dict:
"sig": self.signature,
**self.unrecognized_fields,
}


class GPGSignature(Signature):
"""A container class containing information about a gpg signature.

Besides the signature, it also contains other meta information
needed to uniquely identify the key used to generate the signature.

Attributes:
keyid: HEX string used as a unique identifier of the key.
signature: HEX string representing the signature.
other_headers: HEX representation of additional GPG headers.
"""

def __init__(
self,
keyid: str,
signature: str,
other_headers: str,
):
super().__init__(keyid, signature)
self.other_headers = other_headers

@classmethod
def from_dict(cls, signature_dict: Dict) -> "GPGSignature":
"""Creates a GPGSignature object from its JSON/dict representation.

Args:
signature_dict: Dict containing valid "keyid", "signature" and
"other_fields" fields.

Raises:
KeyError: If any of the "keyid", "sig" or "other_headers" fields
are missing from the signature_dict.

Returns:
GPGSignature instance.
"""

return cls(
signature_dict["keyid"],
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,
}
62 changes: 1 addition & 61 deletions securesystemslib/signer/_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
from typing import Callable, Dict, Optional, Type
from urllib import parse

import securesystemslib.gpg.functions as gpg
import securesystemslib.keys as sslib_keys
from securesystemslib.signer._key import Key, SSlibKey
from securesystemslib.signer._signature import GPGSignature, Signature
from securesystemslib.signer._signature import Signature

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -212,62 +211,3 @@ def sign(self, payload: bytes) -> Signature:
"""
sig_dict = sslib_keys.create_signature(self.key_dict, payload)
return Signature(**sig_dict)


class GPGSigner(Signer):
"""A securesystemslib gpg implementation of the "Signer" interface.

Provides a sign method to generate a cryptographic signature with gpg, using
an RSA, DSA or EdDSA private key identified by the keyid on the instance.
"""

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

@classmethod
def from_priv_key_uri(
cls,
priv_key_uri: str,
public_key: Key,
secrets_handler: Optional[SecretsHandler] = None,
) -> "GPGSigner":
raise NotImplementedError("Incompatible with private key URIs")

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
key identified by the passed keyid from the gpg keyring at the passed
homedir.

The executed base command is defined in
securesystemslib.gpg.constants.gpg_sign_command.

Arguments:
payload: The bytes to be signed.

Raises:
securesystemslib.exceptions.FormatError:
If the keyid was passed and does not match
securesystemslib.formats.KEYID_SCHEMA.

ValueError: the gpg command failed to create a valid signature.
OSError: the gpg command is not present or non-executable.
securesystemslib.exceptions.UnsupportedLibraryError: the gpg
command is not available, or the cryptography library is
not installed.
securesystemslib.gpg.exceptions.CommandError: the gpg command
returned a non-zero exit code.
securesystemslib.gpg.exceptions.KeyNotFoundError: the used gpg
version is not fully supported and no public key can be found
for short keyid.

Returns:
Returns a "GPGSignature" class instance.
"""

sig_dict = gpg.create_signature(payload, self.keyid, self.homedir)
return GPGSignature(**sig_dict)
32 changes: 16 additions & 16 deletions tests/test_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from securesystemslib.signer import (
KEY_FOR_TYPE_AND_SCHEME,
SIGNER_FOR_URI_SCHEME,
GPGSignature,
GPGSigner,
Key,
SecretsHandler,
Expand Down Expand Up @@ -398,39 +397,40 @@ def tearDownClass(cls):

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

# pylint: disable=protected-access
signer = GPGSigner(homedir=self.gnupg_home)
signature = signer.sign(self.test_data)

signature_dict = signature.to_dict()
signature_dict = GPGSigner._to_gpg_sig(signature)
key_data = export_pubkey(self.default_keyid, self.gnupg_home)

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."""

# pylint: disable=protected-access
signer = GPGSigner(self.signing_subkey_keyid, self.gnupg_home)
signature = signer.sign(self.test_data)

signature_dict = signature.to_dict()
signature_dict = GPGSigner._to_gpg_sig(signature)
key_data = export_pubkey(self.signing_subkey_keyid, self.gnupg_home)

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())
def test_gpg_signature_data_structure(self):
"""Test custom fields and legacy data structure in gpg signatures."""
# pylint: disable=protected-access
signer = GPGSigner(homedir=self.gnupg_home)
sig = signer.sign(self.test_data)
self.assertIn("other_headers", sig.unrecognized_fields)

sig_dict = GPGSigner._to_gpg_sig(sig)
self.assertIn("signature", sig_dict)
self.assertNotIn("sig", sig_dict)
sig2 = GPGSigner._from_gpg_sig(sig_dict)
self.assertEqual(sig, sig2)


# Run the unit tests.
Expand Down