Skip to content

Conversation

@costdev
Copy link
Member

@costdev costdev commented Sep 9, 2025

See #231

This PR introduces signature verification for packages. The PR still needs initial testing.

Approach

The 'upgrader_pre_download' hook is used to:

  • Download the package
  • Set the trusted keys
  • Perform signature verification

The callback includes guards and clean up surrounding the steps above.

Using this particular hook means minimal duplication of Core's code while returning back into Core's process as soon as possible.

New dependency

This PR introduces yocLib - Multibase as a dependency. This is used to convert signing keys from Base58BTC to Base64. While we could use another implementation, this is used elsewhere in the project and I erred on the side of getting something testable out quicker.

Pre-merge note

If we continue with this dependency, or find another external dependency, the bundler script will need to be checked to ensure it includes/excludes files as its supposed to. This should be done before this PR is merged.

Testing

Ensure you have enabled signature verification. You can drop this into a file in wp-content/mu-plugins:

<?php
/**
 * Plugin Name: FAIR: Verify Package Signatures
 * Description: Enables FAIR package signature verification
 * Author:      FAIR Contributors
 * License:     GPLv2 or later
 * Version:     1.0.0
 */
add_filter( 'fair.packages.updater.verify_signatures', '__return_true' );

@costdev costdev force-pushed the verify_signature_on_download branch from 8c48893 to a1b0f1e Compare September 9, 2025 06:21
@costdev costdev requested review from afragen and rmccue September 9, 2025 06:43
Copy link

@kasparsd kasparsd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

Please see the comments inline for feedback. I believe we might want to adjust the return value of the upgrader_pre_download filter to ensure it stops WP core from downloading the package if the verification fails.

@kasparsd
Copy link

kasparsd commented Sep 9, 2025

I'm thinking about the alternative workflows where this could apply. Will this apply to the did: installer too? Or if a user goes through the manual install workflow to upload an update to an existing package?

@costdev
Copy link
Member Author

costdev commented Sep 9, 2025

Will this apply to the did: installer too?

Yeah this should work with the did: direct installer right away.

Or if a user goes through the manual install workflow to upload an update to an existing package?

Yes I believe so. The FAIR plugin sets up the Updater for each installed package on init. The Updater::run() method calls Updater::load_hooks(), which is where this PR hooks to upgrader_pre_download for that package.

I'm not sure whether this is the case when uploading a new package though. I expect that is different.

Copy link

@kasparsd kasparsd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now!

@kasparsd
Copy link

kasparsd commented Sep 9, 2025

I'm not sure whether this is the case when uploading a new package though. I expect that is different.

Yeah, there is no reliable way to determine the package vendor if we only have the ZIP. For a secure installation users should use the did: installer.

We could attempt to extract the vendor did from the uploaded package meta header and then compare the downloaded file hash/checksum + signature with any of the available releases. But that doesn't really help because a malicious actor could obviously remove the did info from the meta.

I love the did installer! Such a simple UI for a super-secure installer.

@costdev
Copy link
Member Author

costdev commented Sep 9, 2025

FYI: After chatting with @rmccue, I'm going to rework some of this to use a subclass of the WP_Upgrader class so we have some more control and it'll read better.

@costdev costdev force-pushed the verify_signature_on_download branch from 7d438d7 to 9c8ee6e Compare September 12, 2025 04:44
@costdev costdev force-pushed the verify_signature_on_download branch from bd2b3af to 488bd27 Compare September 16, 2025 00:21
verify_file_signature() is documented to return bool|WP_Error, with false meaning verification was not attempted. However, the function's code doesn't actually return false.

Signed-off-by: costdev <[email protected]>
The plugin will include distribution dependencies, whereas workflows install development dependencies too, so this check will always fail.

Signed-off-by: costdev <[email protected]>
This was previously sending the whole key object, resulting in a Fatal Error.

Signed-off-by: costdev <[email protected]>
This reverts commit be67b25.

Signed-off-by: costdev <[email protected]>
@costdev costdev force-pushed the verify_signature_on_download branch from 696e20a to 3ac5dac Compare September 18, 2025 21:12
@costdev
Copy link
Member Author

costdev commented Sep 18, 2025

The force-push was a rebase on main due to a merge conflict.

Every FAIR package hooks to verify signature on download. Simply using `md5( $package )` to protect against an infinite loop is overkill, because if some other FAIR package checks for a matching URL first, the actual matching FAIR package will bail out.

Example:
- User clicks to install FAIR Plugin B.
- FAIR Plugin A's filter runs first (alphabetical order). `$has_run[ md5( $package ) ]` is set.
- FAIR Plugin B's filter runs. `$has_run[ md5( $package ) ]` has already been set, so it bails.
- FAIR Plugin B's package doesn't get signature verification.

To resolve this, each FAIR package's filter now uses `md5( $this->did . '_' . $package )`, which is specific to _that package_ for _that URL_.

Signed-off-by: costdev <[email protected]>
@rmccue
Copy link
Member

rmccue commented Sep 21, 2025

In fairpm/fair-beacon#52 we switched the signature algorithm to sha384, which I think was the last blocker here. @costdev is this now ready for review/merge?

@costdev costdev marked this pull request as ready for review September 21, 2025 15:01
@costdev
Copy link
Member Author

costdev commented Sep 21, 2025

@rmccue Yes, ready for review, testing and merge. At the moment, you can test by enabling signature verification and installing Git Updater via Direct Install - did:plc:afjf7gsjzsqmgc7dlhb553mv.

MU Plugin to enable signature verification:

<?php
/**
 * Plugin Name: FAIR: Verify Package Signatures
 * Description: Enables FAIR package signature verification
 * Author:      FAIR Contributors
 * License:     GPLv2 or later
 * Version:     1.0.0
 */
add_filter( 'fair.packages.updater.verify_signatures', '__return_true' );

@costdev
Copy link
Member Author

costdev commented Sep 21, 2025

I've added an error_log() so testers can see when it's successfully verified the package. Just ensure WP_DEBUG and WP_DEBUG_LOG are set to true.

**DO NOT MERGE WITH THIS error_log() in the PR - I'll remove it once the PR has 1-2 approvals **

@afragen afragen self-requested a review September 21, 2025 17:51
@afragen
Copy link
Contributor

afragen commented Sep 21, 2025

I now see this issue

screenshot_717

@afragen
Copy link
Contributor

afragen commented Sep 21, 2025

If the package is not yet installed then Updater\get_packages() returns null

@costdev
Copy link
Member Author

costdev commented Sep 21, 2025

That's related to another PR. I'll get that fixed shortly.

@afragen
Copy link
Contributor

afragen commented Sep 21, 2025

screenshot_718

@afragen
Copy link
Contributor

afragen commented Sep 21, 2025

Cache issue. All working/verifying.

return $result;
}

( new Updater\Updater( $did ) )->run();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future: we should rename this method to be clearer, and move some of the global state checks out of it.

@rmccue rmccue merged commit d59d2d6 into fairpm:main Sep 22, 2025
46 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants