Index: trunk/src/wp-admin/includes/file.php
===================================================================
--- trunk/src/wp-admin/includes/file.php	(revision 44824)
+++ trunk/src/wp-admin/includes/file.php	(revision 44954)
@@ -966,10 +966,12 @@
  *
  * @since 2.5.0
- *
- * @param string $url  The URL of the file to download.
- * @param int $timeout The timeout for the request to download the file. Default 300 seconds.
+ * @since 5.2.0 Signature Verification with SoftFail was added.
+ *
+ * @param string $url                The URL of the file to download.
+ * @param int    $timeout            The timeout for the request to download the file. Default 300 seconds.
+ * @param bool   $signature_softfail Whether to allow Signature Verification to softfail. Default true.
  * @return string|WP_Error Filename on success, WP_Error on failure.
  */
-function download_url( $url, $timeout = 300 ) {
+function download_url( $url, $timeout = 300, $signature_softfail = true ) {
 	//WARNING: The file is not automatically deleted, The script must unlink() the file.
 	if ( ! $url ) {
@@ -1035,4 +1037,53 @@
 	}
 
+	/**
+	 * Filters the list of hosts which should have Signature Verification attempted on.
+	 *
+	 * @since 5.2.0
+	 *
+	 * @param array List of hostnames.
+	 */
+	$signed_hostnames       = apply_filters( 'wp_signature_hosts', array( 'wordpress.org', 'downloads.wordpress.org', 's.w.org' ) );
+	$signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true );
+
+	// Perform the valiation
+	if ( $signature_verification ) {
+		$signature = wp_remote_retrieve_header( $response, 'x-content-signature' );
+		if ( ! $signature ) {
+			// Retrieve signatures from a file if the header wasn't included.
+			// WordPress.org stores signatures at $package_url.sig
+			$signature_request = wp_safe_remote_get( $url . '.sig' );
+			if ( ! is_wp_error( $signature_request ) && 200 === wp_remote_retrieve_response_code( $signature_request ) ) {
+				$signature = explode( "\n", wp_remote_retrieve_body( $signature_request ) );
+			}
+		}
+
+		// Perform the checks.
+		$signature_verification = verify_file_signature( $tmpfname, $signature, basename( parse_url( $url, PHP_URL_PATH ) ) );
+	}
+
+	if ( is_wp_error( $signature_verification ) ) {
+		if (
+			/**
+			 * Filters whether Signature Verification failures should be allowed to soft fail.
+			 *
+			 * WARNING: This may be removed from a future release.
+			 *
+			 * @since 5.2.0
+			 *
+			 * @param bool   $signature_softfail If a softfail is allowed.
+			 * @param string $url                The url being accessed.
+			 */
+			apply_filters( 'wp_signature_softfail', $signature_softfail, $url )
+		) {
+			$signature_verification->add_data( $tmpfname, 'softfail-filename' );
+		} else {
+			// Hard-fail.
+			unlink( $tmpfname );
+		}
+
+		return $signature_verification;
+	}
+
 	return $tmpfname;
 }
@@ -1065,4 +1116,118 @@
 
 	return new WP_Error( 'md5_mismatch', sprintf( __( 'The checksum of the file (%1$s) does not match the expected checksum value (%2$s).' ), bin2hex( $file_md5 ), bin2hex( $expected_raw_md5 ) ) );
+}
+
+/**
+ * Verifies the contents of a file against its ED25519 signature.
+ *
+ * @since 5.2.0
+ *
+ * @param string       $filename            The file to validate.
+ * @param string|array $signatures          A Signature provided for the file.
+ * @param string       $filename_for_errors A friendly filename for errors. Optional.
+ *
+ * @return bool|WP_Error true on success, false if verificaiton not attempted, or WP_Error describing an error condition.
+ */
+function verify_file_signature( $filename, $signatures, $filename_for_errors = false ) {
+	if ( ! $filename_for_errors ) {
+		$filename_for_errors = wp_basename( $filename );
+	}
+
+	// Check we can process signatures.
+	if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) || ! in_array( 'sha384', array_map( 'strtolower', hash_algos() ) ) ) {
+		return new WP_Error(
+			'signature_verification_unsupported',
+			sprintf(
+				/* translators: 1: The filename of the package. */
+				__( 'The authenticity of %1$s could not be verified as signature verification is unavailable on this system.' ),
+				'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
+			),
+			( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ? 'sodium_crypto_sign_verify_detached' : 'sha384' )
+		);
+	}
+
+	if ( ! $signatures ) {
+		return new WP_Error(
+			'signature_verification_no_signature',
+			sprintf(
+				/* translators: 1: The filename of the package. */
+				__( 'The authenticity of %1$s could not be verified as no signature was found.' ),
+				'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
+			)
+		);
+	}
+
+	$trusted_keys = wp_trusted_keys();
+	$file_hash    = hash_file( 'sha384', $filename, true );
+
+	mbstring_binary_safe_encoding();
+
+	foreach ( (array) $signatures as $signature ) {
+		$signature_raw = base64_decode( $signature );
+
+		// Ensure only valid-length signatures are considered.
+		if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) {
+			continue;
+		}
+
+		foreach ( (array) $trusted_keys as $key ) {
+			$key_raw = base64_decode( $key );
+
+			// Only pass valid public keys through.
+			if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) {
+				continue;
+			}
+
+			if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) {
+				reset_mbstring_encoding();
+				return true;
+			}
+		}
+	}
+
+	reset_mbstring_encoding();
+
+	return new WP_Error(
+		'signature_verification_failed',
+		sprintf(
+			/* translators: 1: The filename of the package. */
+			__( 'The authenticity of %1$s could not be verified.' ),
+			'<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
+		),
+		// Error data helpful for debugging:
+		array(
+			'filename'   => $filename_for_errors,
+			'keys'       => $trusted_keys,
+			'signatures' => $signatures,
+			'hash'       => bin2hex( $file_hash ),
+		)
+	);
+}
+
+/**
+ * Retrieve the list of signing keys trusted by WordPress.
+ *
+ * @since 5.2.0
+ *
+ * @return array List of hex-encoded Signing keys.
+ */
+function wp_trusted_keys() {
+	$trusted_keys = array();
+
+	if ( time() < 1617235200 ) {
+		// WordPress.org Key #1 - This key is only valid before April 1st, 2021.
+		$trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=';
+	}
+
+	// TODO: Add key #2 with longer expiration.
+
+	/**
+	 * Filter the valid Signing keys used to verify the contents of files.
+	 *
+	 * @since 5.2.0
+	 *
+	 * @param array $trusted_keys The trusted keys that may sign packages.
+	 */
+	return apply_filters( 'wp_trusted_keys', $trusted_keys );
 }
 
