Skip to content

Commit 873f276

Browse files
authored
Merge pull request #341 from MVrachev/gpg-signer
Add GPGSigner implementation
2 parents 5ac8012 + 01f2f40 commit 873f276

File tree

3 files changed

+181
-4
lines changed

3 files changed

+181
-4
lines changed

securesystemslib/gpg/functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def create_signature(content, keyid=None, homedir=None):
5050
identified by the passed keyid from the gpg keyring at the passed homedir.
5151
5252
The executed base command is defined in
53-
securesystemslib.gpgp.constants.GPG_SIGN_COMMAND.
53+
securesystemslib.gpg.constants.GPG_SIGN_COMMAND.
5454
5555
NOTE: On not fully supported versions of GPG, i.e. versions below
5656
securesystemslib.gpg.constants.FULLY_SUPPORTED_MIN_VERSION the returned

securesystemslib/signer.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import abc
99
import securesystemslib.keys as sslib_keys
10+
import securesystemslib.gpg.functions as gpg
1011
from typing import Any, Dict, Optional, Mapping
1112

1213

@@ -88,6 +89,60 @@ def to_dict(self) -> Dict:
8889

8990

9091

92+
class GPGSignature(Signature):
93+
"""A container class containing information about a gpg signature.
94+
95+
Besides the signature, it also contains other meta information
96+
needed to uniquely identify the key used to generate the signature.
97+
98+
Attributes:
99+
keyid: HEX string used as a unique identifier of the key.
100+
signature: HEX string representing the signature.
101+
other_headers: HEX representation of additional GPG headers.
102+
"""
103+
def __init__(
104+
self,
105+
keyid: str,
106+
signature: str,
107+
other_headers: str,
108+
):
109+
super().__init__(keyid, signature)
110+
self.other_headers = other_headers
111+
112+
113+
@classmethod
114+
def from_dict(cls, signature_dict: Dict) -> "Signature":
115+
"""Creates a GPGSignature object from its JSON/dict representation.
116+
117+
Args:
118+
signature_dict: Dict containing valid "keyid", "signature" and
119+
"other_fields" fields.
120+
121+
Raises:
122+
KeyError: If any of the "keyid", "sig" or "other_headers" fields
123+
are missing from the signature_dict.
124+
125+
Returns:
126+
GPGSignature instance.
127+
"""
128+
129+
return cls(
130+
signature_dict["keyid"],
131+
signature_dict["sig"],
132+
signature_dict["other_headers"]
133+
)
134+
135+
136+
def to_dict(self) -> Dict:
137+
"""Returns the JSON-serializable dictionary representation of self."""
138+
return {
139+
"keyid": self.keyid,
140+
"signature": self.signature,
141+
"other_headers": self.other_headers
142+
}
143+
144+
145+
91146
class Signer:
92147
"""Signer interface created to support multiple signing implementations."""
93148

@@ -160,3 +215,62 @@ def sign(self, payload: bytes) -> "Signature":
160215

161216
sig_dict = sslib_keys.create_signature(self.key_dict, payload)
162217
return Signature(**sig_dict)
218+
219+
220+
221+
class GPGSigner(Signer):
222+
"""A securesystemslib gpg implementation of the "Signer" interface.
223+
224+
Provides a sign method to generate a cryptographic signature with gpg, using
225+
an RSA, DSA or EdDSA private key identified by the keyid on the instance.
226+
227+
Args:
228+
keyid: The keyid of the gpg signing keyid. If not passed the default
229+
key in the keyring is used.
230+
231+
homedir: Path to the gpg keyring. If not passed the default keyring
232+
is used.
233+
234+
"""
235+
def __init__(
236+
self, keyid: Optional[str] = None, homedir: Optional[str] = None
237+
):
238+
self.keyid = keyid
239+
self.homedir = homedir
240+
241+
242+
def sign(self, payload: bytes) -> "GPGSignature":
243+
"""Signs a given payload by the key assigned to the GPGSigner instance.
244+
245+
Calls the gpg command line utility to sign the passed content with the
246+
key identified by the passed keyid from the gpg keyring at the passed
247+
homedir.
248+
249+
The executed base command is defined in
250+
securesystemslib.gpg.constants.GPG_SIGN_COMMAND.
251+
252+
Arguments:
253+
payload: The bytes to be signed.
254+
255+
Raises:
256+
securesystemslib.exceptions.FormatError:
257+
If the keyid was passed and does not match
258+
securesystemslib.formats.KEYID_SCHEMA.
259+
260+
ValueError: the gpg command failed to create a valid signature.
261+
OSError: the gpg command is not present or non-executable.
262+
securesystemslib.exceptions.UnsupportedLibraryError: the gpg
263+
command is not available, or the cryptography library is
264+
not installed.
265+
securesystemslib.gpg.exceptions.CommandError: the gpg command
266+
returned a non-zero exit code.
267+
securesystemslib.gpg.exceptions.KeyNotFoundError: the used gpg
268+
version is not fully supported and no public key can be found
269+
for short keyid.
270+
271+
Returns:
272+
Returns a "GPGSignature" class instance.
273+
"""
274+
275+
sig_dict = gpg.create_signature(payload, self.keyid, self.homedir)
276+
return GPGSignature(**sig_dict)

tests/test_signer.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
"""Test cases for "signer.py". """
44

55
import copy
6-
import sys
6+
import os
77
import unittest
8+
import tempfile
9+
import shutil
810

9-
import unittest
1011
import securesystemslib.formats
1112
import securesystemslib.keys as KEYS
1213
from securesystemslib.exceptions import FormatError, UnsupportedAlgorithmError
13-
from securesystemslib.signer import Signature, SSlibSigner
14+
from securesystemslib.signer import Signature, SSlibSigner, GPGSigner
15+
from securesystemslib.gpg.constants import HAVE_GPG
16+
from securesystemslib.gpg.functions import (
17+
export_pubkey,
18+
verify_signature as verify_sig
19+
)
1420

1521

1622
class TestSSlibSigner(unittest.TestCase):
@@ -95,6 +101,63 @@ def test_signature_eq_(self):
95101
sig_obj_2 = None
96102
self.assertNotEqual(sig_obj, sig_obj_2)
97103

104+
@unittest.skipIf(not HAVE_GPG, "gpg not found")
105+
class TestGPGRSA(unittest.TestCase):
106+
"""Test RSA gpg signature creation and verification."""
107+
108+
@classmethod
109+
def setUpClass(cls):
110+
cls.default_keyid = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17"
111+
cls.signing_subkey_keyid = "C5A0ABE6EC19D0D65F85E2C39BE9DF5131D924E9"
112+
113+
# Create directory to run the tests without having everything blow up.
114+
cls.working_dir = os.getcwd()
115+
cls.test_data = b'test_data'
116+
cls.wrong_data = b'something malicious'
117+
118+
# Find demo files.
119+
gpg_keyring_path = os.path.join(
120+
os.path.dirname(os.path.realpath(__file__)), "gpg_keyrings", "rsa")
121+
122+
cls.test_dir = os.path.realpath(tempfile.mkdtemp())
123+
cls.gnupg_home = os.path.join(cls.test_dir, "rsa")
124+
shutil.copytree(gpg_keyring_path, cls.gnupg_home)
125+
os.chdir(cls.test_dir)
126+
127+
128+
@classmethod
129+
def tearDownClass(cls):
130+
"""Change back to initial working dir and remove temp test directory."""
131+
132+
os.chdir(cls.working_dir)
133+
shutil.rmtree(cls.test_dir)
134+
135+
136+
def test_gpg_sign_and_verify_object_with_default_key(self):
137+
"""Create a signature using the default key on the keyring. """
138+
139+
signer = GPGSigner(homedir=self.gnupg_home)
140+
signature = signer.sign(self.test_data)
141+
142+
signature_dict = signature.to_dict()
143+
key_data = export_pubkey(self.default_keyid, self.gnupg_home)
144+
145+
self.assertTrue(verify_sig(signature_dict, key_data, self.test_data))
146+
self.assertFalse(verify_sig(signature_dict, key_data, self.wrong_data))
147+
148+
149+
def test_gpg_sign_and_verify_object(self):
150+
"""Create a signature using a specific key on the keyring. """
151+
152+
signer = GPGSigner(self.signing_subkey_keyid, self.gnupg_home)
153+
signature = signer.sign(self.test_data)
154+
155+
signature_dict = signature.to_dict()
156+
key_data = export_pubkey(self.signing_subkey_keyid, self.gnupg_home)
157+
158+
self.assertTrue(verify_sig(signature_dict, key_data, self.test_data))
159+
self.assertFalse(verify_sig(signature_dict, key_data, self.wrong_data))
160+
98161

99162
# Run the unit tests.
100163
if __name__ == "__main__":

0 commit comments

Comments
 (0)