Skip to content

Commit

Permalink
* Switch from gnupg to pretty_bad_protocol package.
Browse files Browse the repository at this point in the history
* Workaround isislovecruft/python-gnupg#224; Punch the duck till it don't quack no more!
  • Loading branch information
crashvb committed Feb 26, 2021
1 parent ed7e248 commit d6b89b9
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 27 deletions.
72 changes: 51 additions & 21 deletions docker_sign_verify/gpgsigner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,71 @@

"""Classes that provide signature functionality."""

import ast
import asyncio
import logging
import inspect
import io
import os
import subprocess
import types

from pathlib import Path
from typing import Any

import aiofiles

from aiotempfile.aiotempfile import open as aiotempfile
from gnupg._meta import GPGBase
from gnupg._parsers import Verify
from pretty_bad_protocol._meta import GPGBase
from pretty_bad_protocol._parsers import Verify

from .signer import Signer

LOGGER = logging.getLogger(__name__)


def _patch_pretty_bad_protocol():
# pylint: disable=exec-used,protected-access,undefined-variable

def getsource_dedented(obj):
lines = inspect.getsource(obj).split("\n")
indent = len(lines[0]) - len(lines[0].lstrip())
return "\n".join(line[indent:] for line in lines)

source = getsource_dedented(Verify._handle_status)
node = ast.parse(source)

# Change the function name ...
node.body[0].name = "duck_punch__handle_status"

# Change KEY_CONSIDERED processing by removing self.status from the join list ...
# FN IF ELSEIF ELSEIF Assign join List Attribute
del node.body[0].body[1].orelse[0].orelse[0].body[0].value.args[0].elts[0]
# import astpretty
# astpretty.pprint(node.body[0].body[1].orelse[0].orelse[0].body[0].value.args[0])

# Define a the method, globally ...
code = compile(node, __name__, "exec")
exec(code, globals())

# DUCK PUNCH: Override the class method
Verify._handle_status = duck_punch__handle_status

# TODO: Duck punch Verify._handle_status::KEYREVOKED to set self.value = False ...


class GPGSigner(Signer):
"""
Creates and verifies docker image signatures using GnuPG.
"""

class DuckPunchGPGBase:
"""Dummy class that doesn't do all the crap that GPGBase.__init__ does."""

# pylint: disable=too-few-public-methods
def __init__(self):
self.ignore_homedir_permissions = False
self.verbose = False

def __init__(
self,
*,
Expand Down Expand Up @@ -57,33 +96,21 @@ def __init__(

@staticmethod
async def _parse_status(status: bytes) -> Verify:
# pylint: disable=protected-access
"""
Invoke the GnuPG library parsing for status.
Args:
status: Status from GnuPG.
Returns:
The gnupg._parsers.Verify object.
The pretty_bad_protocol._parsers.Verify object.
"""
# DUCK PUNCH:
# * Define a dummy class that doesn't do all the crap that GPGBase.__init__ does.
# * Borrow the GPGBase._read_response method.
class Dummy:
# pylint: disable=missing-class-docstring,too-few-public-methods
verbose: False

# pylint: disable=protected-access
setattr(
Dummy,
GPGBase._read_response.__name__,
types.MethodType(GPGBase._read_response, Dummy),
)

result = Verify(None)
# pylint: disable=no-member
Dummy()._read_response(
io.TextIOWrapper(io.BytesIO(status), encoding="utf-8"), result
GPGBase._read_response(
GPGSigner.DuckPunchGPGBase(),
io.TextIOWrapper(io.BytesIO(status), encoding="utf-8"),
result,
)
return result

Expand Down Expand Up @@ -182,3 +209,6 @@ async def verify(self, data: bytes, signature: str) -> Any:
result = await GPGSigner._parse_status(stderr)

return result


_patch_pretty_bad_protocol()
4 changes: 2 additions & 2 deletions docker_sign_verify/imagesource.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from functools import wraps
from typing import Any, Dict, List, Optional, NamedTuple

import gnupg # Needed for type checking
import pretty_bad_protocol # Needed for type checking

from aiofiles.base import AiofilesContextManager
from docker_registry_client_async import FormattedSHA256, ImageName
Expand Down Expand Up @@ -418,7 +418,7 @@ async def verify_image_signatures(
LOGGER.debug(" signatures:")
for result in data.signatures.results:
# pylint: disable=protected-access
if isinstance(result, gnupg._parsers.Verify):
if isinstance(result, pretty_bad_protocol._parsers.Verify):
if not result.valid:
raise SignatureMismatchError(
"Verification failed for signature with keyid '{0}': {1}".format(
Expand Down
4 changes: 2 additions & 2 deletions docker_sign_verify/scripts/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ def set_log_levels(verbosity: int = LOGGING_DEFAULT):
# normal, quiet, silent ...
if verbosity <= LOGGING_DEFAULT:
_format = "%(message)s"
logging.getLogger("gnupg").setLevel(logging.FATAL)
logging.getLogger("pretty_bad_protocol").setLevel(logging.FATAL)
# debug / verbose ...
elif verbosity == LOGGING_DEFAULT + 1:
_format = "%(asctime)s %(levelname)-8s %(message)s"
logging.getLogger("gnupg").setLevel(logging.WARNING)
logging.getLogger("pretty_bad_protocol").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
# very verbose ...
else:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def find_version(*segments):
"canonicaljson",
"docker-registry-client-async>=0.1.4",
"click",
"gnupg",
"pretty-bad-protocol=3.1.1",
"pycryptodome",
],
keywords="docker docker-sign docker-verify integrity sign signatures verify",
Expand Down
12 changes: 12 additions & 0 deletions tests/data/gnupg.stderr.key_considered
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[GNUPG:] NEWSIG
gpg: Signature made Mon Mar 23 12:36:40 2020 UTC
gpg: using RSA key A328C2DC7801F08F
[GNUPG:] KEY_CONSIDERED 1111A943E5D571AC145949A67B4D120726C8BA41 0
[GNUPG:] SIG_ID YnRIfkpi48w7qQmm9smnYhcvEWA 2020-03-23 1584967000
[GNUPG:] KEY_CONSIDERED 1111A943E5D571AC145949A67B4D120726C8BA41 0
[GNUPG:] GOODSIG A328C2DC7801FF9E First Last <First.Last@domain.com>
gpg: Good signature from "First Last <First.Last@domain.com>" [ultimate]
[GNUPG:] VALIDSIG 1111E6B9F947C2F2BA3820B1A328C2DC7801F082 2020-03-23 1584967000 0 4 0 1 10 00 1111A943E5D571AC145949A67B4D120726C8BA41
[GNUPG:] KEY_CONSIDERED 1111A943E5D571AC145949A67B4D120726C8BA41 0
[GNUPG:] TRUST_ULTIMATE 0 pgp
[GNUPG:] VERIFICATION_COMPLIANCE_MODE 23
19 changes: 18 additions & 1 deletion tests/test_gpgsigner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from docker_sign_verify import GPGSigner, Signer

from .testutils import get_test_data

pytestmark = [pytest.mark.asyncio]

LOGGER = logging.getLogger(__name__)
Expand All @@ -38,9 +40,24 @@ async def gpgsigner(gnupg_keypair: GnuPGKeypair) -> GPGSigner:
yield signer


async def test__parse_status_key_considered(request):
"""Check for known parsing issues."""
status = get_test_data(request, __name__, "gnupg.stderr.key_considered")

# Mainly test that it can parse without choking on KEY_CONSIDERED ...
# https://github.com/isislovecruft/python-gnupg/issues/224
# ... is NOT fixed by isislovecruft/python-gnupg:PR#220
result = await GPGSigner._parse_status(status)
assert result
assert result.fingerprint
assert result.status
assert result.key_id
assert result.valid


def test_for_signature(caplog: LogCaptureFixture):
"""Tests subclass instantiation."""
caplog.set_level(logging.FATAL, logger="gnupg")
caplog.set_level(logging.FATAL, logger="pretty_bad_protocol")
result = Signer.for_signature("PGP SIGNATURE")
assert result
assert isinstance(result, GPGSigner)
Expand Down

0 comments on commit d6b89b9

Please sign in to comment.