Notes on FAIR Package Manager

Had a deeper look at the FAIR package manager today.

Right now the WordPress integration plugin doesn’t verify package signatures. That means WordPress sites can’t yet cryptographically confirm that a downloaded plugin or theme really came from the claimed publisher.

Verification isn’t trivial either. To check a did:plc identity properly you have to walk the entire chain of signed operations all the way back to the genesis operation. That’s where the DID is anchored.

That requires some pretty heavy crypto for a WordPress host (where the client plugin is installed):

  • multibase / base58 decoding
  • DAG-CBOR + CID recomputation
  • ECDSA (secp256k1, P-256) signature checks

Not every shared host is going to have PHP extensions for all of that. The protocol does elegantly handle key rotation but the burden of verification falls entirely on the consumer.

Just “trusting” whatever keys come back from plc.directory for each package DID identifier isn’t secure. You have to validate the full audit log yourself because otherwise you’re open to tampering. 

So the standard is promising but until FAIR bakes in real signature checks, WordPress users aren’t getting the security guarantees this model could deliver. 

The rest of plugin features are really nice for privacy and general data protection — you no longer report all published content to Ping-o-Matic or send every admin dashboard request to WP-org servers. Here is a report of all external calls made by standard WordPress installs.

How FAIR Works?

A plugin author publishes a WordPress plugin with a PLC:DID identifier such as:

did:plc:deoui6ztyx6paqajconl67rz

The FAIR client plugin fetches the JSON metadata for the DID from the plc.directory URL:

https://plc.directory/did:plc:deoui6ztyx6paqajconl67rz

which returns:

{
	"@context": [
		"https://www.w3.org/ns/did/v1",
		"https://w3id.org/security/multikey/v1",
		"https://w3id.org/security/suites/secp256k1-2019/v1"
	],
	"id": "did:plc:deoui6ztyx6paqajconl67rz",
	"alsoKnownAs": [],
	"verificationMethod": [
		{
			"id": "did:plc:deoui6ztyx6paqajconl67rz#fairpm",
			"type": "Multikey",
			"controller": "did:plc:deoui6ztyx6paqajconl67rz",
			"publicKeyMultibase": "zQ3shjiQmfcvNg5ExJuCcX8Bfzaa77y3yxD9iPMYmeRYbk4Vf"
		}
	],
	"service": [
		{
			"id": "#fairpm_repo",
			"type": "FairPackageManagementRepo",
			"serviceEndpoint": "https://fair.git-updater.com/wp-json/minifair/v1/packages/did:plc:deoui6ztyx6paqajconl67rz"
		}
	]
}

where the key parts are:

  1. the public keys defined under verificationMethod with type equal to Multikey and the id containing #fairpm.
  2. the package URL under serviceEndpoint with type FairPackageManagementRepo and id containing #fairpm_.

In order to ensure that the publicKeyMultibase value is actually a valid public key belonging to the DID, you need to parse the full DID activity log and verify that each operation is signed and derived from the previous one, including the DID itself at the genesis operation.

It then fetches the resolved serviceEndpoint URL with the Accept: application/json request header:

https://fair.git-updater.com/wp-json/minifair/v1/packages/did:plc:deoui6ztyx6paqajconl67rz

which returns:

{
    "@context": "https://fair.pm/ns/metadata/v1",
    "id": "did:plc:deoui6ztyx6paqajconl67rz",
    "type": "wp-plugin",
    "name": "Handbook Callout Blocks",
    "slug": "handbook-callout-blocks",
    "filename": "handbook-callout-blocks/handbook.php",
    "description": "The make.wordpress.org blog has wonderful callout blocks but they seem to be baked in as part of the WP.org Handbook plugin.\nI was able to \u2026",
    "authors": [
        {
            "name": "WordPress.org, Andy Fragen",
            "url": ""
        }
    ],
    "license": "GPL-2.0-or-later",
    "security": [],
    "keywords": [
        "wporg",
        "handbook",
        "callout"
    ],
    "sections": {
        "description": "<p>The make.wordpress.org blog has wonderful callout blocks but they seem to be baked in as part of the WP.org <a href=\"https://meta.trac.wordpress.org/browser/sites/trunk/wordpress.org/public_html/wp-content/plugins/handbook\">Handbook plugin</a>.</p>\n<p>I was able to strip out the relevant parts of the Handbook plugin retaining only the callout blocks.</p>\n<h3>Special Thanks</h3>\n<ul>\n<li><a href=\"https://profiles.wordpress.org/ipstenu/\">@Ipstenu</a> for her terrific sleuthing skills.</li>\n<li><a href=\"https://profiles.wordpress.org/Clorith\">@Clorith</a> for bringing code up to current standards and best practices.</li>\n</ul>",
        "changelog": "<p>[unreleased]</p>\n<ul>\n<li>add support for <code>core/list</code></li>\n<li>refactor for current standards and practices courtesy of @Clorith</li>\n<li>make editor padding match</li>\n<li>load dashicons for non-logged in users</li>\n<li>initial release</li>\n<li>add composer.json</li>\n</ul>"
    },
    "releases": [
        {
            "version": "1.0.3",
            "requires": {
                "env:php": ">=7.4",
                "env:wp": ">=5.9"
            },
            "suggests": {
                "env:wp": ">=6.7.2"
            },
            "provides": [],
            "artifacts": {
                "icon": [
                    {
                        "url": "https://s.w.org/plugins/geopattern-icon/handbook-callout-blocks.svg",
                        "content-type": "image/svg+xml",
                        "height": null,
                        "width": null
                    }
                ],
                "package": [
                    {
                        "url": "https://api.github.com/repos/afragen/handbook-callout-blocks/releases/assets/274184925",
                        "content-type": "application/octet-stream",
                        "signature": "AcKSOVp2EHQCSWBO5LZCDv4puOpvJILsovIynQkf-hcpBOGpkuc4aaLi5NC9Gd4s3IBNzbqFzM75a6lcx4kk_w",
                        "checksum": "sha256:bb2f21d4f5b3e6a8daa361abb75b366d90059a7e1a15c18100ca3492cdb252af"
                    }
                ]
            }
        }
    ]
}

which is now used to render all the information about the plugin in the WP admin before the installation and during upgrades. The signature of the package ZIP file can now be verified using the public key extracted from the first step.

2 Comments

  1. Weston Ruter says:

    Not every shared host is going to have PHP extensions for all of that. The protocol does elegantly handle key rotation but the burden of verification falls entirely on the consumer.

    Could this verification be offloaded to the client in WASM? PHP could fetch the ZIP file and signature, and then they could be loaded client-side for verification, and then PHP could proceed with installation once verified.

    • Kaspars says:

      That could possibly work but it would require the updates to happen only when the user is logged-in. Also trusting crypto that happened on the client has its own challenges, I think.

Likes

Leave a Reply

Reply on Mastodon or search for ️this permalink in your ActivityPub client, and it will show up here too (also follow this blog). Or simply leave a comment below: