From 5bc5948bf38532a0310906996605443b8a37a56b Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 4 Jul 2020 10:41:00 +0200 Subject: [PATCH] packages: show who signed the package and when Fixes #23 --- app/appstate.py | 7 +++-- app/pgp.py | 57 ++++++++++++++++++++++++++++++++++++++ app/templates/package.html | 6 ++++ poetry.lock | 13 ++++++++- pyproject.toml | 1 + tests/test_main.py | 25 +++++++++++++++++ 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 app/pgp.py diff --git a/app/appstate.py b/app/appstate.py index c0ddca7..aa85c10 100644 --- a/app/appstate.py +++ b/app/appstate.py @@ -4,6 +4,7 @@ from __future__ import annotations import re +import base64 import uuid import time from functools import cmp_to_key @@ -12,6 +13,7 @@ from typing import List, Set, Dict, Tuple, Optional, Type, Sequence, NamedTuple from .appconfig import REPOSITORIES from .utils import vercmp, version_is_newer_than, extract_upstream_version +from .pgp import parse_signature CygwinVersions = Dict[str, Tuple[str, str, str]] @@ -222,7 +224,7 @@ class Package: makedepends: List[str], md5sum: str, name: str, pgpsig: str, sha256sum: str, arch: str, base_url: str, repo: str, repo_variant: str, provides: List[str], conflicts: List[str], replaces: List[str], version: str, base: str, desc: str, groups: List[str], licenses: List[str], optdepends: List[str], - checkdepends: List[str]) -> None: + checkdepends: List[str], sig_data: str) -> None: self.builddate = int(builddate) self.csize = csize @@ -235,6 +237,7 @@ class Package: r.append((first, second)) return r + self.signature = parse_signature(base64.b64decode(sig_data)) self.depends = split_depends(depends) self.checkdepends = split_depends(checkdepends) self.filename = filename @@ -317,7 +320,7 @@ class Package: d.get("%REPLACES%", []), d["%VERSION%"][0], base, d.get("%DESC%", [""])[0], d.get("%GROUPS%", []), d.get("%LICENSE%", []), d.get("%OPTDEPENDS%", []), - d.get("%CHECKDEPENDS%", [])) + d.get("%CHECKDEPENDS%", []), d.get("%PGPSIG%", [""])[0]) class Source: diff --git a/app/pgp.py b/app/pgp.py new file mode 100644 index 0000000..867cc2a --- /dev/null +++ b/app/pgp.py @@ -0,0 +1,57 @@ +from pgpdump import BinaryData +from pgpdump.utils import PgpdumpException +from datetime import datetime +from typing import NamedTuple +import struct +import binascii + + +KNOWN_KEYS = { + "5F92EFC1A47D45A1": "Alexey Pavlov", + "4DF3B7664CA56930": "Ray Donnelly", + "D595C9AB2C51581E": "Martell Malone", + "974C8BE49078F532": "David Macek", + "FA11531AA0AA7F57": "Christoph Reiter", +} + + +class Signature(NamedTuple): + keyid: str + date: datetime + + @property + def url(self) -> str: + return "http://pool.sks-keyservers.net/pks/lookup?op=vindex&fingerprint=on&search=0x" + self.keyid + + @property + def name(self) -> str: + return KNOWN_KEYS.get(self.keyid.upper(), "Unknown") + + +class SigError(Exception): + pass + + +def parse_signature(sig_data: bytes) -> Signature: + date = None + keyid = None + + try: + parsed = BinaryData(sig_data) + except PgpdumpException as e: + raise SigError(e) + + for x in parsed.packets(): + if x.raw == 2: + for sub in x.subpackets: + if sub.subtype == 2: + date = datetime.utcfromtimestamp(struct.unpack('>I', sub.data)[0]) + if sub.subtype == 16: + keyid = binascii.hexlify(sub.data).decode() + + if keyid is None: + raise SigError("keyid missing") + if date is None: + raise SigError("date missing") + + return Signature(keyid, date) diff --git a/app/templates/package.html b/app/templates/package.html index 8f53c1a..c2c85be 100644 --- a/app/templates/package.html +++ b/app/templates/package.html @@ -43,6 +43,12 @@
Build Date:
{{ p.builddate|timestamp }}
+
Signed By:
+
{{ p.signature.name }}
+ +
Signature Date:
+
{{ p.signature.date }}
+
Package Size:
{{ p.csize|filesize }}
diff --git a/poetry.lock b/poetry.lock index ae6e599..7262c99 100644 --- a/poetry.lock +++ b/poetry.lock @@ -297,6 +297,14 @@ version = "20.4" pyparsing = ">=2.0.2" six = "*" +[[package]] +category = "main" +description = "PGP packet parser library" +name = "pgpdump" +optional = false +python-versions = "*" +version = "1.5" + [[package]] category = "dev" description = "plugin and hook calling mechanisms for python" @@ -538,7 +546,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "65b0ed368759d67e2016c027ea98952449a27db72c2555fc779d54936ae4f31c" +content-hash = "2c191d8d34f6865392bd1df633bf1ec72ea1ed51f7c6d33940b2e37679c4d78d" python-versions = "^3.7" [metadata.files] @@ -707,6 +715,9 @@ packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] +pgpdump = [ + {file = "pgpdump-1.5.tar.gz", hash = "sha256:1c4700857bf7ba735b08cfe4101aa3a4f5fd839657af249c17b2697c20829668"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, diff --git a/pyproject.toml b/pyproject.toml index d61378f..ea9f7bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ uvicorn = "^0.11.5" aiofiles = "^0.5.0" jinja2 = "^2.11.2" fastapi-etag = "^0.2.1" +pgpdump = "^1.5" [tool.poetry.dev-dependencies] mypy = "^0.770" diff --git a/tests/test_main.py b/tests/test_main.py index bf0c3fc..d3b8fca 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,14 @@ # type: ignore import os +import base64 +import datetime import respx import pytest from app import app from app.fetch import parse_cygwin_versions +from app.pgp import parse_signature, SigError, Signature from fastapi.testclient import TestClient @@ -81,3 +84,25 @@ def test_webhook_push(client, monkeypatch): r.raise_for_status() assert "msg" in r.json() assert "1234" in r.json()["msg"] + + +EXAMPLE_SIG = ( + "iHUEABEIAB0WIQStNRxQrghXdetZMztfku/BpH1FoQUCXlOY5wAKCRBfku" + "/BpH1FodQoAP4nQnPNLnx5MVIJgZgCwW/hplW7Ai9MqkmFBqD8/+EXfAD/" + "Rgxtz2XH7RZ1JKh7PN5NsVz9UlBM7977PjFg9WptNGU=") + + +def test_pgp(): + with pytest.raises(SigError): + parse_signature(b"") + + with pytest.raises(SigError): + parse_signature(b"foobar") + + data = base64.b64decode(EXAMPLE_SIG) + sig = parse_signature(data) + assert isinstance(sig, Signature) + assert sig.keyid == "5f92efc1a47d45a1" + assert sig.date == datetime.datetime(2020, 2, 24, 9, 35, 35) + assert sig.name == "Alexey Pavlov" + assert sig.url == "http://pool.sks-keyservers.net/pks/lookup?op=vindex&fingerprint=on&search=0x5f92efc1a47d45a1"