@@ -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 )
0 commit comments