Skip to content

Commit 9f22eac

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

6 files changed

Lines changed: 131 additions & 0 deletions

File tree

src/pkgcheck/checks/network.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,3 +465,95 @@ 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(_UrlCheck):
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 = (
498+
".asc",
499+
".minisig",
500+
".sig",
501+
".sign",
502+
".sigstore",
503+
)
504+
505+
def __init__(self, *args, use_addon, **kwargs):
506+
super().__init__(*args, **kwargs)
507+
self.fetch_filter = use_addon.get_filter("fetchables")
508+
509+
def _verifysig_check(self, filename, url, *, pkg):
510+
"""Check for typical verify sig URLS."""
511+
result = None
512+
try:
513+
# Need redirects to deal with the variance of file servers and urls
514+
response = self.session.head(url, allow_redirects=True)
515+
except RequestError:
516+
pass
517+
except SSLError as e:
518+
result = SSLCertificateError("SRC_URI", url, str(e), pkg=pkg)
519+
else:
520+
content_type = response.headers.get("Content-Type")
521+
522+
# Filtering out text/html matches is useful due to possible false matches with authentication
523+
if (
524+
response.ok
525+
and content_type is not None
526+
and not content_type.startswith("text/html")
527+
):
528+
result = DetachedSignatureAvailable(filename, url, pkg=pkg)
529+
return result
530+
531+
def _get_urls(self, pkg):
532+
# ignore conditionals
533+
fetchables, _ = self.fetch_filter(
534+
(fetchable,),
535+
pkg,
536+
pkg.generate_fetchables(
537+
allow_missing_checksums=True, ignore_unknown_mirrors=True, skip_default_mirrors=True
538+
),
539+
)
540+
541+
filenames = [f.filename for f in fetchables.keys()]
542+
543+
for f in fetchables.keys():
544+
# Don't check for detached signatures if any detached signature is already present for the filename.
545+
if any(
546+
(f.filename.endswith(extension) or f"{f.filename}{extension}" in filenames)
547+
for extension in self.detached_signature_extensions
548+
):
549+
continue
550+
551+
for url in f.uri:
552+
for extension in self.detached_signature_extensions:
553+
yield (f.filename, url + extension)
554+
555+
def schedule(self, pkg, executor, futures):
556+
"""Schedule verification methods to run in separate threads for all flagged URLs."""
557+
558+
for filename, url in self._get_urls(pkg):
559+
self._schedule_check(self._verifysig_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)