Make WordPress Core

Changeset 44954


Ignore:
Timestamp:
03/21/2019 05:48:46 AM (7 years ago)
Author:
tellyworth
Message:

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.

Location:
trunk/src/wp-admin/includes
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-wp-upgrader.php

    r42787 r44954  
    276276                $this->skin->feedback( 'downloading_package', $package );
    277277
    278                 $download_file = download_url( $package );
    279 
    280                 if ( is_wp_error( $download_file ) ) {
     278                $download_file = download_url( $package, 300, true );
     279
     280                if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) {
    281281                        return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() );
    282282                }
     
    732732                 */
    733733                $download = $this->download_package( $options['package'] );
     734
     735                // Allow for signature soft-fail.
     736                // WARNING: This may be removed in the future.
     737                if ( is_wp_error( $download ) && $download->get_error_data( 'softfail-filename' ) ) {
     738                        // Outout the failure error as a normal feedback, and not as an error:
     739                        $this->skin->feedback( $download->get_error_message() );
     740
     741                        // Report this failure back to WordPress.org for debugging purposes.
     742                        wp_version_check(
     743                                array(
     744                                        'signature_failure_code' => $download->get_error_code(),
     745                                        'signature_failure_data' => $download->get_error_data(),
     746                                )
     747                        );
     748
     749                        // Pretend this error didn't happen.
     750                        $download = $download->get_error_data( 'softfail-filename' );
     751                }
     752
    734753                if ( is_wp_error( $download ) ) {
    735754                        $this->skin->error( $download );
  • trunk/src/wp-admin/includes/file.php

    r44824 r44954  
    966966 *
    967967 * @since 2.5.0
    968  *
    969  * @param string $url  The URL of the file to download.
    970  * @param int $timeout The timeout for the request to download the file. Default 300 seconds.
     968 * @since 5.2.0 Signature Verification with SoftFail was added.
     969 *
     970 * @param string $url                The URL of the file to download.
     971 * @param int    $timeout            The timeout for the request to download the file. Default 300 seconds.
     972 * @param bool   $signature_softfail Whether to allow Signature Verification to softfail. Default true.
    971973 * @return string|WP_Error Filename on success, WP_Error on failure.
    972974 */
    973 function download_url( $url, $timeout = 300 ) {
     975function download_url( $url, $timeout = 300, $signature_softfail = true ) {
    974976        //WARNING: The file is not automatically deleted, The script must unlink() the file.
    975977        if ( ! $url ) {
     
    10351037        }
    10361038
     1039        /**
     1040         * Filters the list of hosts which should have Signature Verification attempted on.
     1041         *
     1042         * @since 5.2.0
     1043         *
     1044         * @param array List of hostnames.
     1045         */
     1046        $signed_hostnames       = apply_filters( 'wp_signature_hosts', array( 'wordpress.org', 'downloads.wordpress.org', 's.w.org' ) );
     1047        $signature_verification = in_array( parse_url( $url, PHP_URL_HOST ), $signed_hostnames, true );
     1048
     1049        // Perform the valiation
     1050        if ( $signature_verification ) {
     1051                $signature = wp_remote_retrieve_header( $response, 'x-content-signature' );
     1052                if ( ! $signature ) {
     1053                        // Retrieve signatures from a file if the header wasn't included.
     1054                        // WordPress.org stores signatures at $package_url.sig
     1055                        $signature_request = wp_safe_remote_get( $url . '.sig' );
     1056                        if ( ! is_wp_error( $signature_request ) && 200 === wp_remote_retrieve_response_code( $signature_request ) ) {
     1057                                $signature = explode( "\n", wp_remote_retrieve_body( $signature_request ) );
     1058                        }
     1059                }
     1060
     1061                // Perform the checks.
     1062                $signature_verification = verify_file_signature( $tmpfname, $signature, basename( parse_url( $url, PHP_URL_PATH ) ) );
     1063        }
     1064
     1065        if ( is_wp_error( $signature_verification ) ) {
     1066                if (
     1067                        /**
     1068                         * Filters whether Signature Verification failures should be allowed to soft fail.
     1069                         *
     1070                         * WARNING: This may be removed from a future release.
     1071                         *
     1072                         * @since 5.2.0
     1073                         *
     1074                         * @param bool   $signature_softfail If a softfail is allowed.
     1075                         * @param string $url                The url being accessed.
     1076                         */
     1077                        apply_filters( 'wp_signature_softfail', $signature_softfail, $url )
     1078                ) {
     1079                        $signature_verification->add_data( $tmpfname, 'softfail-filename' );
     1080                } else {
     1081                        // Hard-fail.
     1082                        unlink( $tmpfname );
     1083                }
     1084
     1085                return $signature_verification;
     1086        }
     1087
    10371088        return $tmpfname;
    10381089}
     
    10651116
    10661117        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 ) ) );
     1118}
     1119
     1120/**
     1121 * Verifies the contents of a file against its ED25519 signature.
     1122 *
     1123 * @since 5.2.0
     1124 *
     1125 * @param string       $filename            The file to validate.
     1126 * @param string|array $signatures          A Signature provided for the file.
     1127 * @param string       $filename_for_errors A friendly filename for errors. Optional.
     1128 *
     1129 * @return bool|WP_Error true on success, false if verificaiton not attempted, or WP_Error describing an error condition.
     1130 */
     1131function verify_file_signature( $filename, $signatures, $filename_for_errors = false ) {
     1132        if ( ! $filename_for_errors ) {
     1133                $filename_for_errors = wp_basename( $filename );
     1134        }
     1135
     1136        // Check we can process signatures.
     1137        if ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) || ! in_array( 'sha384', array_map( 'strtolower', hash_algos() ) ) ) {
     1138                return new WP_Error(
     1139                        'signature_verification_unsupported',
     1140                        sprintf(
     1141                                /* translators: 1: The filename of the package. */
     1142                                __( 'The authenticity of %1$s could not be verified as signature verification is unavailable on this system.' ),
     1143                                '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
     1144                        ),
     1145                        ( ! function_exists( 'sodium_crypto_sign_verify_detached' ) ? 'sodium_crypto_sign_verify_detached' : 'sha384' )
     1146                );
     1147        }
     1148
     1149        if ( ! $signatures ) {
     1150                return new WP_Error(
     1151                        'signature_verification_no_signature',
     1152                        sprintf(
     1153                                /* translators: 1: The filename of the package. */
     1154                                __( 'The authenticity of %1$s could not be verified as no signature was found.' ),
     1155                                '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
     1156                        )
     1157                );
     1158        }
     1159
     1160        $trusted_keys = wp_trusted_keys();
     1161        $file_hash    = hash_file( 'sha384', $filename, true );
     1162
     1163        mbstring_binary_safe_encoding();
     1164
     1165        foreach ( (array) $signatures as $signature ) {
     1166                $signature_raw = base64_decode( $signature );
     1167
     1168                // Ensure only valid-length signatures are considered.
     1169                if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $signature_raw ) ) {
     1170                        continue;
     1171                }
     1172
     1173                foreach ( (array) $trusted_keys as $key ) {
     1174                        $key_raw = base64_decode( $key );
     1175
     1176                        // Only pass valid public keys through.
     1177                        if ( SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES !== strlen( $key_raw ) ) {
     1178                                continue;
     1179                        }
     1180
     1181                        if ( sodium_crypto_sign_verify_detached( $signature_raw, $file_hash, $key_raw ) ) {
     1182                                reset_mbstring_encoding();
     1183                                return true;
     1184                        }
     1185                }
     1186        }
     1187
     1188        reset_mbstring_encoding();
     1189
     1190        return new WP_Error(
     1191                'signature_verification_failed',
     1192                sprintf(
     1193                        /* translators: 1: The filename of the package. */
     1194                        __( 'The authenticity of %1$s could not be verified.' ),
     1195                        '<span class="code">' . esc_html( $filename_for_errors ) . '</span>'
     1196                ),
     1197                // Error data helpful for debugging:
     1198                array(
     1199                        'filename'   => $filename_for_errors,
     1200                        'keys'       => $trusted_keys,
     1201                        'signatures' => $signatures,
     1202                        'hash'       => bin2hex( $file_hash ),
     1203                )
     1204        );
     1205}
     1206
     1207/**
     1208 * Retrieve the list of signing keys trusted by WordPress.
     1209 *
     1210 * @since 5.2.0
     1211 *
     1212 * @return array List of hex-encoded Signing keys.
     1213 */
     1214function wp_trusted_keys() {
     1215        $trusted_keys = array();
     1216
     1217        if ( time() < 1617235200 ) {
     1218                // WordPress.org Key #1 - This key is only valid before April 1st, 2021.
     1219                $trusted_keys[] = 'fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0=';
     1220        }
     1221
     1222        // TODO: Add key #2 with longer expiration.
     1223
     1224        /**
     1225         * Filter the valid Signing keys used to verify the contents of files.
     1226         *
     1227         * @since 5.2.0
     1228         *
     1229         * @param array $trusted_keys The trusted keys that may sign packages.
     1230         */
     1231        return apply_filters( 'wp_trusted_keys', $trusted_keys );
    10671232}
    10681233
Note: See TracChangeset for help on using the changeset viewer.