<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[44954] trunk/src/wp-admin/includes: Upgrade/Install: Add experimental package signing to some updates.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/44954">44954</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/44954","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>tellyworth</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2019-03-21 05:48:46 +0000 (Thu, 21 Mar 2019)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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 <a href="https://core.trac.wordpress.org/ticket/39309">#39309</a>, <a href="https://core.trac.wordpress.org/ticket/45806">#45806</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadminincludesclasswpupgraderphp">trunk/src/wp-admin/includes/class-wp-upgrader.php</a></li>
<li><a href="#trunksrcwpadminincludesfilephp">trunk/src/wp-admin/includes/file.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadminincludesclasswpupgraderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/class-wp-upgrader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/class-wp-upgrader.php 2019-03-21 04:55:21 UTC (rev 44953)
+++ trunk/src/wp-admin/includes/class-wp-upgrader.php   2019-03-21 05:48:46 UTC (rev 44954)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -275,9 +275,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->skin->feedback( 'downloading_package', $package );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $download_file = download_url( $package );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $download_file = download_url( $package, 300, true );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( is_wp_error( $download_file ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( is_wp_error( $download_file ) && ! $download_file->get_error_data( 'softfail-filename' ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         return new WP_Error( 'download_failed', $this->strings['download_failed'], $download_file->get_error_message() );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -731,6 +731,25 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * of the file if the package is a local file)
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                $download = $this->download_package( $options['package'] );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               // 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' );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( is_wp_error( $download ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->skin->error( $download );
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->skin->after();
</span></span></pre></div>
<a id="trunksrcwpadminincludesfilephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/includes/file.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/includes/file.php      2019-03-21 04:55:21 UTC (rev 44953)
+++ trunk/src/wp-admin/includes/file.php        2019-03-21 05:48:46 UTC (rev 44954)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -965,12 +965,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Please note that the calling function must unlink() the file.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.5.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 5.2.0 Signature Verification with SoftFail was added.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @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.
</ins><span class="cx" style="display: block; padding: 0 10px">  * @return string|WP_Error Filename on success, WP_Error on failure.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function download_url( $url, $timeout = 300 ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function download_url( $url, $timeout = 300, $signature_softfail = true ) {
</ins><span class="cx" style="display: block; padding: 0 10px">         //WARNING: The file is not automatically deleted, The script must unlink() the file.
</span><span class="cx" style="display: block; padding: 0 10px">        if ( ! $url ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return new WP_Error( 'http_no_url', __( 'Invalid URL Provided.' ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1034,6 +1036,55 @@
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * 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;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         return $tmpfname;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1067,6 +1118,120 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * 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 );
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Unzips a specified ZIP file to a location on the filesystem via the WordPress
</span><span class="cx" style="display: block; padding: 0 10px">  * Filesystem Abstraction.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span></span></pre>
</div>
</div>

</body>
</html>