Make WordPress Core


Ignore:
Timestamp:
03/21/2019 05:48:46 AM (5 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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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.