Skip to content

Commit

Permalink
[5.1] TUF-based core updates (#42799)
Browse files Browse the repository at this point in the history
Co-authored-by: Franciska Perisa <9084265+fancyFranci@users.noreply.github.com>
Co-authored-by: Magnus Singer <magnussinger@icloud.com>
Co-authored-by: Tobias Zulauf <zero-24@users.noreply.github.com>
Co-authored-by: Benjamin Trenkle <bembelimen@users.noreply.github.com>
Co-authored-by: Niels Nübel <niels@kicktemp.com>
Co-authored-by: Timo Feuerstein <timo.feuerstein@renolit.com>
Co-authored-by: Martina  Scholz <64533137+LadySolveig@users.noreply.github.com>
  • Loading branch information
8 people committed Feb 26, 2024
1 parent dacda5c commit 75fca46
Show file tree
Hide file tree
Showing 34 changed files with 2,678 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--
-- Table structure for table `#__tuf_metadata`
--

CREATE TABLE IF NOT EXISTS `#__tuf_metadata` (
`id` int NOT NULL AUTO_INCREMENT,
`update_site_id` int DEFAULT 0,
`root` text DEFAULT NULL,
`targets` text DEFAULT NULL,
`snapshot` text DEFAULT NULL,
`timestamp` text DEFAULT NULL,
`mirrors` text DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Secure TUF Updates';

-- --------------------------------------------------------
INSERT INTO `#__tuf_metadata` (`update_site_id`, `root`)
VALUES ((SELECT ue.`update_site_id` FROM `#__update_sites_extensions` AS ue JOIN `#__extensions` AS e ON (e.`extension_id` = ue.`extension_id`) WHERE e.`type`='file' AND e.`element`='joomla'), '{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2025-03-02T11:22:17Z","keys":{"07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9"}},"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799"}},"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9"}},"31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2"}},"9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9"}}},"roles":{"root":{"keyids":["1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"snapshot":{"keyids":["07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"targets":{"keyids":["31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3"],"threshold":1},"timestamp":{"keyids":["9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e","sig":"2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501"},{"keyid":"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","sig":"8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005"}]}');

-----------------------------------------------------------
UPDATE `#__update_sites`
SET `type` = 'tuf', `location` = 'https://update.joomla.org/cms/'
WHERE `update_site_id` = (SELECT ue.`update_site_id` FROM `#__update_sites_extensions` AS ue JOIN `#__extensions` AS e ON (e.`extension_id` = ue.`extension_id`) WHERE e.`type`='file' AND e.`element`='joomla');
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--
-- Table structure for table "#__tuf_metadata"
--

CREATE TABLE IF NOT EXISTS "#__tuf_metadata" (
"id" serial NOT NULL,
"update_site_id" bigint DEFAULT 0 NOT NULL,
"root" text DEFAULT NULL,
"targets" text DEFAULT NULL,
"snapshot" text DEFAULT NULL,
"timestamp" text DEFAULT NULL,
"mirrors" text DEFAULT NULL,
PRIMARY KEY ("id")
);

COMMENT ON TABLE "#__tuf_metadata" IS 'Secure TUF Updates';

INSERT INTO "#__tuf_metadata" ("update_site_id", "root")
VALUES ((SELECT ue."update_site_id" FROM "#__update_sites_extensions" AS ue JOIN "#__extensions" AS e ON (e."extension_id" = ue."extension_id") WHERE e."type"='file' AND e."element"='joomla'), '{"signed":{"_type":"root","spec_version":"1.0","version":2,"expires":"2025-03-02T11:22:17Z","keys":{"07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"9b2af2d9b9727227735253d795bd27ea8f0e294a5f3603e822dc5052b44802b9"}},"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"a18e5ebabc19d5d5984b601a292ece61ba3662ab2d071dc520da5bd4f8948799"}},"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"cb0a7a131961a20edea051d6dc2b091fb650bd399bd8514adb67b3c60db9f8f9"}},"31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"589d029a68b470deff1ca16dbf3eea6b5b3fcba0ae7bb52c468abc7fb058b2a2"}},"9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc":{"keytype":"ed25519","scheme":"ed25519","keyid_hash_algorithms":["sha256","sha512"],"keyval":{"public":"6043c8bacc76ac5c9750f45454dd865c6ca1fc57d69e14cc192cfd420f6a66a9"}}},"roles":{"root":{"keyids":["1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"snapshot":{"keyids":["07eb082f367c034a95878687f6648aa76d93652b6ee73e58817053d89af6c44f","2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e"],"threshold":1},"targets":{"keyids":["31dd7c7290d664c9b88c0dead2697175293ea7df81b7f24153a37370fd3901c3"],"threshold":1},"timestamp":{"keyids":["9e41a9d62d94c6a1c8a304f62c5bd72d84a9f286f27e8327cedeacb09e5156cc"],"threshold":1}},"consistent_snapshot":true},"signatures":[{"keyid":"2dcaf3d0e552f150792f7c636d45429246dcfa34ac35b46a44f5c87cd17d457e","sig":"2a225a560ec0837b721d4c5e379fedbd3c7c9079a94e6b31e47e0184c8b95421b6036b4286c5d90f29ab4c468d79a712fdb65e96511394ceb3aa8e2b3983a501"},{"keyid":"1b1b1dd55b2c1c7258714cf1c1ae06f23e4607b28c762d016a9d81c48ffe5669","sig":"8ce0b2a7bdc1e6dcba12081f440510df0a593c072dcf591631c2dd0f456844a7da63be8e8ac31ffbddf42641fde84dc733a336031d182c2163b4c1eaf2117005"}]}');

UPDATE "#__update_sites"
SET "type" = 'tuf', "location" = 'https://update.joomla.org/cms/'
WHERE "update_site_id" = (SELECT ue."update_site_id" FROM "#__update_sites_extensions" AS ue JOIN "#__extensions" AS e ON (e."extension_id" = ue."extension_id") WHERE e."type"='file' AND e."element"='joomla');

17 changes: 17 additions & 0 deletions administrator/components/com_installer/src/Model/UpdateModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,23 @@ public function update($uids, $minimumStability = Updater::STABILITY_STABLE)
continue;
}

$app = Factory::getApplication();
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('type')
->from('#__update_sites')
->where($db->quoteName('update_site_id') . ' = :id')
->bind(':id', $instance->update_site_id, ParameterType::INTEGER);

$updateSiteType = (string) $db->setQuery($query)->loadResult();

// TUF is currently only supported for Joomla core
if ($updateSiteType === 'tuf') {
$app->enqueueMessage(Text::_('JLIB_INSTALLER_TUF_NOT_AVAILABLE'), 'error');

return;
}

$update->loadFromXml($instance->detailsurl, $minimumStability);

// Find and use extra_query from update_site if available
Expand Down
1 change: 0 additions & 1 deletion administrator/components/com_joomlaupdate/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
validate="options"
>
<!-- Note: Changed the values lts to default and sts to next with 3.4.0 -->
<!-- Eliminated the 'nochange' option with 3.4.0 -->
<!-- All invalid/unsupported/obsolete options equated to default in code with 3.4.0 -->
<option value="default">COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT</option>
<option value="next">COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ public function download()
$message = null;
$messageType = null;

// The versions mismatch
if ($result['version'] !== $this->input->get('targetVersion')) {
$message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_VERSION_WRONG');
$messageType = 'error';
$url = 'index.php?option=com_joomlaupdate';

$this->app->setUserState('com_joomlaupdate.file', null);
$this->setRedirect($url, $message, $messageType);

Log::add($message, Log::ERROR, 'Update');

return;
}

// The validation was not successful so stop.
if ($result['check'] === false) {
$message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG');
Expand All @@ -71,11 +85,7 @@ public function download()
$this->app->setUserState('com_joomlaupdate.file', null);
$this->setRedirect($url, $message, $messageType);

try {
Log::add($message, Log::ERROR, 'Update');
} catch (\RuntimeException $exception) {
// Informational log only
}
Log::add($message, Log::ERROR, 'Update');

return;
}
Expand Down
53 changes: 35 additions & 18 deletions administrator/components/com_joomlaupdate/src/Model/UpdateModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Table\Tuf as TufMetadata;
use Joomla\CMS\Updater\Update;
use Joomla\CMS\Updater\Updater;
use Joomla\CMS\User\UserHelper;
Expand Down Expand Up @@ -87,12 +88,7 @@ public function applyUpdateSite()
// Determine the intended update URL.
$params = ComponentHelper::getParams('com_joomlaupdate');

switch ($params->get('updatesource', 'nochange')) {
case 'next':
// "Minor & Patch Release for Current version AND Next Major Release".
$updateURL = 'https://update.joomla.org/core/sts/list_sts.xml';
break;

switch ($params->get('updatesource', 'default')) {
case 'testing':
// "Testing"
$updateURL = 'https://update.joomla.org/core/test/list_test.xml';
Expand All @@ -112,16 +108,19 @@ public function applyUpdateSite()

default:
/**
* "Minor & Patch Release for Current version (recommended and default)".
* All "non-testing" releases of the official project hosted in Joomla's TUF-based update repo.
* The commented "case" below are for documenting where 'default' and legacy options falls
* case 'default':
* case 'next':
* case 'lts':
* case 'sts': (It's shown as "Default" because that option does not exist any more)
* case 'nochange':
*/
$updateURL = 'https://update.joomla.org/core/list.xml';
$updateURL = 'https://update.joomla.org/cms/';
}

$updateType = (pathinfo($updateURL, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf';

$id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id;
$db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase();
$query = $db->getQuery(true)
Expand All @@ -137,10 +136,11 @@ public function applyUpdateSite()
$db->setQuery($query);
$update_site = $db->loadObject();

if ($update_site->location != $updateURL) {
if ($update_site->location !== $updateURL || $update_site->type !== $updateType) {
// Modify the database record.
$update_site->last_check_timestamp = 0;
$update_site->location = $updateURL;
$update_site->type = $updateType;
$db->updateObject('#__update_sites', $update_site, 'update_site_id');

// Remove cached updates.
Expand Down Expand Up @@ -176,7 +176,7 @@ public function refreshUpdates($force = false)
$minimumStability = Updater::STABILITY_STABLE;
$comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate');

if (\in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) {
if (\in_array($comJoomlaupdateParams->get('updatesource', 'default'), ['testing', 'custom'])) {
$minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE);
}

Expand Down Expand Up @@ -298,14 +298,34 @@ public function getUpdateInformation()

$minimumStability = Updater::STABILITY_STABLE;
$comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate');
$channel = $comJoomlaupdateParams->get('updatesource', 'default');

if (\in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), ['testing', 'custom'])) {
if (\in_array($channel, ['testing', 'custom'])) {
$minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE);
}

// Fetch the full update details from the update details URL.
$update = new Update();
$update->loadFromXml($updateObject->detailsurl, $minimumStability);

$updateType = (pathinfo($updateObject->detailsurl, PATHINFO_EXTENSION) === 'xml') ? 'collection' : 'tuf';

// Check if we have a local JSON string with update metadata
if ($updateType === 'tuf') {
// Use the correct identifier for the update channel
$updateChannel = Version::MAJOR_VERSION . '.x';

if ($channel === 'next') {
$updateChannel = (Version::MAJOR_VERSION + 1) . '.x';
}

$metadata = new TufMetadata($this->getDatabase());
$metadata->load(['update_site_id' => $updateObject->update_site_id]);

// Fetch update data from TUF repo
$update->loadFromTuf($metadata, $updateObject->detailsurl, $minimumStability, $updateChannel);
} else {
// We are using the legacy XML method
$update->loadFromXml($updateObject->detailsurl, $minimumStability, $channel);
}

// Make sure we use the current information we got from the detailsurl
$this->updateInformation['object'] = $update;
Expand Down Expand Up @@ -370,12 +390,12 @@ public function download()
$httpOptions = new Registry();
$httpOptions->set('follow_location', false);

$response = ['basename' => false, 'check' => null, 'version' => $updateInfo['latest']];

try {
$head = HttpFactory::getHttp($httpOptions)->head($packageURL);
} catch (\RuntimeException $e) {
// Passing false here -> download failed message
$response['basename'] = false;

return $response;
}

Expand All @@ -387,8 +407,6 @@ public function download()
$head = HttpFactory::getHttp($httpOptions)->head($packageURL);
} catch (\RuntimeException $e) {
// Passing false here -> download failed message
$response['basename'] = false;

return $response;
}
}
Expand All @@ -409,7 +427,6 @@ public function download()
)
->clean(Factory::getApplication()->get('tmp_path'), 'path');
$target = $tempdir . '/' . $basename;
$response = [];

// Do we have a cached file?
$exists = is_file($target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@
</div>';

if ($this->getCurrentUser()->authorise('core.admin', 'com_joomlaupdate')) :
$displayData['formAppend'] = '<div class="text-center"><a href="' . $uploadLink . '" class="btn btn-sm btn-outline-secondary">' . Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND') . '</a></div>';
$displayData['formAppend'] = '
<div class="text-center"><a href="' . $uploadLink . '" class="btn btn-sm btn-outline-secondary">' . Text::_('COM_JOOMLAUPDATE_EMPTYSTATE_APPEND') . '</a></div>
<input type="hidden" name="targetVersion" value="' . $this->updateInfo['latest'] . '" />
';
endif;

echo '<div id="joomlaupdate-wrapper">';
Expand Down
1 change: 1 addition & 0 deletions administrator/language/en-GB/com_joomlaupdate.ini
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ COM_JOOMLAUPDATE_VIEW_DEFAULT_UPLOAD_INTRO="You can use this feature to update J
COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESEXTRACTED="Bytes extracted"
COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESREAD="Bytes read"
COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG="File Checksum Failed"
COM_JOOMLAUPDATE_VIEW_UPDATE_VERSION_WRONG="The version of the update package and the requested version do not match, try to refresh the update information."
COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED="Download of update package failed."
COM_JOOMLAUPDATE_VIEW_UPDATE_ITEMS="items"
COM_JOOMLAUPDATE_VIEW_UPDATE_FILESEXTRACTED="Files extracted"
Expand Down
7 changes: 7 additions & 0 deletions administrator/language/en-GB/lib_joomla.ini
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,13 @@ JLIB_INSTALLER_SQL_BEGIN="Start of SQL updates."
JLIB_INSTALLER_SQL_BEGIN_SCHEMA="The current database version (schema) is %s."
JLIB_INSTALLER_SQL_END="End of SQL updates."
JLIB_INSTALLER_SQL_END_NOT_COMPLETE="End of SQL updates - INCOMPLETE."
JLIB_INSTALLER_TUF_FREEZE_ATTACK="Update not possible because the offered update has expired."
JLIB_INSTALLER_TUF_DEBUG_MESSAGE="TUF Debug Message: %s"
JLIB_INSTALLER_TUF_INVALID_METADATA="The saved TUF update information is invalid."
JLIB_INSTALLER_TUF_NOT_AVAILABLE="TUF is not available for extensions yet."
JLIB_INSTALLER_TUF_DOWNLOAD_SIZE="The size of the update downloaded did not match the expected size."
JLIB_INSTALLER_TUF_ROLLBACK_ATTACK="Update not possible because the offered update is older than the currently installed version."
JLIB_INSTALLER_TUF_SIGNATURE_THRESHOLD="Update not possible because the offered update does not have enough signatures."
JLIB_INSTALLER_UNINSTALL="Uninstall"
JLIB_INSTALLER_UPDATE="Update"
JLIB_INSTALLER_UPDATE_LOG_QUERY="Ran query from file %1$s. Query text: %2$s."
Expand Down
11 changes: 10 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"type": "vcs",
"url": "https://github.com/joomla-backports/json-api-php.git",
"no-api": true
},
{
"type": "vcs",
"url": "https://github.com/joomla-backports/php-tuf.git",
"no-api": true
}
],
"autoload": {
Expand Down Expand Up @@ -100,7 +105,8 @@
"web-token/signature-pack": "^3.2.8",
"phpseclib/bcmath_compat": "^2.0.1",
"jfcherng/php-diff": "^6.15.3",
"voku/portable-utf8": "^6.0.13"
"voku/portable-utf8": "^6.0.13",
"php-tuf/php-tuf": "dev-main"
},
"require-dev": {
"phpunit/phpunit": "^9.6.13",
Expand All @@ -121,6 +127,9 @@
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"extra": {
"composer-exit-on-patch-failure": true
},
"scripts": {
"post-install-cmd": [
"php build/update_fido_cache.php"
Expand Down

0 comments on commit 75fca46

Please sign in to comment.