Skip to content

Commit 694a268

Browse files
committed
network: add DetachedSignatureAvailableCheck
Signed-off-by: Alfred Wingate <parona@protonmail.com>
1 parent 3ffbd0e commit 694a268

File tree

6 files changed

+160
-0
lines changed

6 files changed

+160
-0
lines changed

src/pkgcheck/checks/network.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,124 @@ def schedule(self, pkg, executor, futures):
465465

466466
for filename, url in self._get_urls(pkg):
467467
self._schedule_check(filename, url, executor, futures, pkg=pkg)
468+
469+
470+
class DetachedSignatureAvailable(results.VersionResult, results.Info):
471+
"""Detached signature available for a distfile in the package."""
472+
473+
def __init__(self, filename, url, **kwargs):
474+
super().__init__(**kwargs)
475+
self.filename = filename
476+
self.url = url
477+
478+
@property
479+
def desc(self):
480+
return f"Detached signature for distfile {self.filename} is available at {self.url}."
481+
482+
483+
class DetachedSignatureAvailableCheck(NetworkCheck):
484+
"""Check for available detached signatures."""
485+
486+
required_addons = (addons.UseAddon,)
487+
488+
_source = sources.LatestVersionRepoSource
489+
490+
known_results = frozenset(
491+
{
492+
DetachedSignatureAvailable,
493+
SSLCertificateError,
494+
}
495+
)
496+
497+
detached_signature_extensions = [".asc", ".minisig", ".sig", ".sign", ".sigstore"]
498+
499+
def __init__(self, *args, use_addon, **kwargs):
500+
super().__init__(*args, **kwargs)
501+
self.fetch_filter = use_addon.get_filter("fetchables")
502+
503+
def _verifysig_check(self, filename, url, *, pkg):
504+
"""Check for typical verify sig URLS."""
505+
result = None
506+
try:
507+
# Need redirects to deal with the variance of file servers and urls
508+
response = self.session.head(url, allow_redirects=True)
509+
except RequestError:
510+
pass
511+
except SSLError as e:
512+
result = SSLCertificateError("SRC_URI", url, str(e), pkg=pkg)
513+
else:
514+
content_type = response.headers.get("Content-Type")
515+
516+
# Filtering out text/html matches is useful due to possible false matches with authentication
517+
if (
518+
response.ok
519+
and content_type is not None
520+
and not content_type.startswith("text/html")
521+
):
522+
result = DetachedSignatureAvailable(filename, url, pkg=pkg)
523+
return result
524+
525+
def task_done(self, pkg, filename, future):
526+
"""Determine the result of a given URL verification task."""
527+
exc = future.exception()
528+
if exc is not None:
529+
# traceback can't be pickled so serialize it
530+
tb = traceback.format_exc()
531+
# return exceptions that occurred in threads
532+
self.results_q.put(tb)
533+
return
534+
535+
result = future.result()
536+
if result is not None:
537+
if pkg is not None:
538+
# recreate result object with different pkg target and attr
539+
attrs = result._attrs.copy()
540+
attrs["filename"] = filename
541+
result = result._create(**attrs, pkg=pkg)
542+
self.results_q.put([result])
543+
544+
def _schedule_check(self, filename, url, executor, futures, **kwargs):
545+
"""Schedule verification method to run in a separate thread against a given URL.
546+
547+
Note that this tries to avoid hitting the network for the same URL
548+
twice using a mapping from requested URLs to future objects, adding
549+
result-checking callbacks to the futures of existing URLs.
550+
"""
551+
future = futures.get(url)
552+
if future is None:
553+
future = executor.submit(self._verifysig_check, filename, url, **kwargs)
554+
future.add_done_callback(partial(self.task_done, None, None))
555+
futures[url] = future
556+
else:
557+
future.add_done_callback(partial(self.task_done, kwargs["pkg"], filename))
558+
559+
def _get_urls(self, pkg):
560+
# ignore conditionals
561+
fetchables, _ = self.fetch_filter(
562+
(fetchable,),
563+
pkg,
564+
pkg.generate_fetchables(
565+
allow_missing_checksums=True, ignore_unknown_mirrors=True, skip_default_mirrors=True
566+
),
567+
)
568+
569+
filenames = [f.filename for f in fetchables.keys()]
570+
571+
for f in fetchables.keys():
572+
# Don't check for detached signatures if any detached signature is already present for the filename.
573+
if any(
574+
(f.filename.endswith(extension) or f"{f.filename}{extension}" in filenames)
575+
for extension in self.detached_signature_extensions
576+
):
577+
continue
578+
579+
for url in f.uri:
580+
for extension in self.detached_signature_extensions:
581+
yield (f.filename, f"{url}{extension}")
582+
return []
583+
584+
def schedule(self, pkg, executor, futures):
585+
"""Schedule verification methods to run in separate threads for all flagged URLs."""
586+
587+
for filename, url in self._get_urls(pkg):
588+
self._schedule_check(filename, url, executor, futures, pkg=pkg)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"__class__": "DetachedSignatureAvailable", "category": "DetachedSignatureAvailableCheck", "package": "DetachedSignatureAvailable", "version": "0", "filename": "foo.tar.gz", "url": "https://github.com/pkgcore/pkgcheck/foo.tar.gz.minisig"}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DESCRIPTION="Ebuild with an available detached signature"
2+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
3+
SRC_URI="https://github.com/pkgcore/pkgcheck/foo.tar.gz"
4+
LICENSE="BSD"
5+
SLOT="0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DIST foo.tar.gz 153310 BLAKE2B b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255ccf810ce8cd16a957fb5bca3d1e71c088cd894968641db5dfae1c4c059df836 SHA512 86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from contextlib import contextmanager
2+
3+
from requests.models import Response
4+
5+
6+
@contextmanager
7+
def responses(req, **kwargs):
8+
possible_responses = {
9+
# success
10+
"minisig": {
11+
"status_code": 200,
12+
"reason": "OK",
13+
"headers": {"Content-Type": "application/pgp-signature"},
14+
},
15+
# false success (like 404 behind authentication redirect)
16+
"sign": {
17+
"status_code": 200,
18+
"reason": "OK",
19+
"headers": {"Content-Type": "text/html"},
20+
},
21+
}
22+
23+
r = Response()
24+
r.status_code = 404
25+
r.reason = "Not Found"
26+
27+
possible_response = possible_responses.get(req.url.split(".")[-1])
28+
if possible_response is not None:
29+
for key, value in possible_response.items():
30+
setattr(r, key, value)
31+
yield r

testdata/repos/network/profiles/categories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
DetachedSignatureAvailableCheck
12
FetchablesUrlCheck
23
HomepageUrlCheck
34
MetadataUrlCheck

0 commit comments

Comments
 (0)