Security in 5.2

Post originally written by Scott Arciszewski.

Protection Against Supply-Chain Attacks

Starting with WordPress 5.2, your website will remain secure even if the wordpress.org servers get hacked.

We are now cryptographically signing WordPress updates with a key that is held offline, and your website will verify these signatures before applying updates.

Signature Verification in WordPress 5.2

When your WordPress site installs an automatic update, from version 5.2 onwards it will first check for the existence of an x-content-signature header. If one isn’t provided by our update server, your WordPress site will instead query for a filenamehere.sig file and parse it.

The signatures were calculated using Ed25519 of the SHA384 hash of the file’s contents. The signature is then base64-encoded for safe transport, no matter how it’s delivered.

The signing keys used to release updates are managed by the WordPress.org core development team. The verification key for the initial release of WordPress 5.2 is fRPyrxb/MvVLbdsYi+OOEv4xc+Eqpsj+kkAS6gNOkI0= (expires April 1, 2021).

(For the sake of specificity: Signing key here means Ed25519 secret key, while verification key means Ed25519 public key.)

To verify an update file, your WordPress site will calculate the SHA384 hash of the update file and then verify the Ed25519 signature of this hash. If you’re running PHP 7.1 or older and have not installed the Sodium extension, the signature verification code is provided by sodium compat.

Our signature verification is implemented in the new verify_file_signature() function, inside wp-admin/includes/file.php.

Modern Cryptography for WordPress Plugins

The inclusion of sodium_compat on WordPress 5.2 means that plugin developers can start to migrate their custom cryptography code away from mcrypt (which was deprecated in PHP 7.1, and removed in PHP 7.2) and towards libsodium.

Example Functions

<?php
/**
 * @param string $message
 * @param string $key
 * @return string
 */
function wp_custom_encrypt( $message, $key )
{
    $nonce = random_bytes(24);
    return base64_encode(
        $nonce . sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
            $message,
            $nonce,
            $nonce,
            $key
        )
    );
}

/**
 * @param string $message
 * @param string $key
 * @return string
 */
function wp_custom_decrypt( $message, $key )
{
    $decoded = base64_decode($message);
    $nonce = substr($decoded, 0, 24);
    $ciphertext = substr($decoded, 24);
    return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
        $ciphertext,
        $nonce,
        $nonce,
        $key
    );
}

How to Seamlessly and Securely Upgrade your Plugins to Use the New Cryptography APIs

If your plugin uses encryption provided by the abandoned mcrypt extension, there are two strategies for securely migrating your code to use libsodium.

Strategy 1: All Data Decryptable at Run-Time

If you can encrypt/decrypt arbitrary records, the most straightforward thing to do is to use mcrypt_decrypt() to obtain the plaintext, then re-encrypt your code using libsodium in one sitting.

Then remove the runtime code for handling mcrypt-encrypted messages.

<?php
// Do this in one sitting
$plaintext = mcrypt_decrypt( $mcryptCipher, $oldKey, $ciphertext, $mode, $iv );
$encrypted = wp_custom_encrypt( $plaintext, $newKey );

Strategy 2: Only Some Data Decryptable at Run-Time

If you can’t decrypt all records at once, the best thing to do is to immediately re-encrypt everything using sodium_crypto_secretbox() and then, at a later time, apply the mcrypt-flavored decryption routine (if it’s still encrypted).

<?php
/**
 * Migrate legacy ciphertext to libsodium
 * 
 * @param string $message
 * @param string $newKey
 * @return string
 */
function wp_migrate_encrypt( $message, $newKey )
{
    return wp_custom_encrypt(
        'legacy:' . base64_encode($message),
        $newKey
    );
}

/**
 * @param string $message
 * @param string $newKey
 * @param string $oldKey
 * @return string 
 */
function wp_migrate_decrypt( $message, $newKey, $oldKey )
{ 
    $plaintext = wp_custom_decrypt($message, $newKey);
    if ( substr($plaintext, 0, 7) === 'legacy:' ) {
        $decoded = base64_decode( substr($plaintext, 7) );
        if ( is_string($decoded) ) {
            // Now apply your mcrypt-based decryption code
            $plaintext = mcrypt_decrypt( $mcryptCipher, $oldKey, $decoded, $mode, $iv );

            // Call a re-encrypt routine here
        }
    }
    return $plaintext;
}

Avoid Opportunistic Upgrades

A common mistake some developers make is to try to do an “opportunistic” upgrade: Only perform the decrypt-then-re-encrypt routine on an as-needed basis. This is a disaster waiting to happen, and there is a lot of historical precedence to this.

Of particular note, Yahoo made this mistake, and as a result, had lots of MD5 password hashing lying around their database when they were breached, even though their active users had long since upgraded to bcrypt.

Detailed technical information about this new security feature, written by Paragon Initiative Enterprises (the cryptography team that developed it) are available here.

#5-2 #dev-notes