Upgrade/Install: Add experimental package signing to some updates.

This adds code for soft verification of signatures for theme and plugin installs and updates, when provided by the update server. This experimental version does not reject unverified packages or failed signatures; it simply reports anonymous errors so we can evaluate its feasibility and detect incompatibilities.

This code relies on the new sodium_compat library for PHP versions prior to 7.2.

Props dd32, paragoninitiativeenterprises.
See #39309, #45806.


git-svn-id: https://develop.svn.wordpress.org/trunk@44954 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Alex Shiels 2019-03-21 05:48:46 +00:00
parent 53f45b4dfc
commit 2976cfb7ca
2 changed files with 189 additions and 5 deletions

View File

@ -275,9 +275,9 @@ class WP_Upgrader {
$this->skin->feedback( 'downloading_package', $package );
$download_file = download_url( $package );
$download_file = download_url( $package, 300, true );
if ( is_wp_error( $download_file ) ) {
if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) {
return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() );
}
@ -731,6 +731,25 @@ class WP_Upgrader {
* of the file if the package is a local file)
*/
$download = $this->download_package( $options['package'] );
// Allow for signature soft-fail.
// WARNING: This may be removed in the future.
if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
// Outout the failure error as a normal feedback, and not as an error:
$this->skin->feedback( $download->get_error_message() );
// Report this failure back to WordPress.org for debugging purposes.
wp_version_check(
array(
'signature_failure_code' => $download->get_error_code(),
'signature_failure_data' => $download->get_error_data(),
)
);
// Pretend this error didn't happen.
$download = $download->get_error_data( 'softfail-filename' );
}
if ( is_wp_error( $download ) ) {
$this->skin->error( $download );
$this->skin->after();

View File

@ -965,12 +965,14 @@ function wp_handle_sideload( &$file, $overrides = false, $time = null ) {
* Please note that the calling function must unlink() the file.
*
* @since 2.5.0
* @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 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 ) {
return new WP_Error( 'http_no_url', __( 'Invalid URL Provided.' ) );
@ -1034,6 +1036,55 @@ function download_url( $url, $timeout = 300 ) {
}
}
/**
* 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;
}
@ -1066,6 +1117,120 @@ function verify_file_md5( $filename, $expected_md5 ) {
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 );
}
/**
* Unzips a specified ZIP file to a location on the filesystem via the WordPress
* Filesystem Abstraction.