Verify

This page is inspired by the curl project's Verify page.

Do not trust, verify!

Signed releases

Every PHPUnit release is shipped as a PHP Archive (PHAR). PHAR releases are available on https://phar.phpunit.de/.

Each PHAR is signed by Sebastian Bergmann. PGP signatures and SHA256 hashes are provided alongside every release for verification. The digital signatures can be used to verify that the PHARs were produced by Sebastian Bergmann and have not been tampered with.

Starting with PHPUnit 13.4.1, 12.5.38, 11.5.57, 10.5.66, 9.6.38, and 8.5.56, every PHAR is also accompanied by a signed Software Bill of Materials (SBOM) that describes the PHAR and the components it contains.

If the PHPUnit website or PHAR distribution server were breached and fake PHPUnit releases were provided, they could be detected using these signatures.

Sebastian Bergmann's public GPG key: D840 6D0D 8294 7747 2937 7831 4AA3 9408 6372 C20A

For information on all keys Sebastian Bergmann uses to sign code, see his code signing policy.

Verifying PHAR releases with GPG

Here is how you can manually verify a PHP Archive of a PHPUnit release using its detached PGP signature. We use PHPUnit 13 as an example throughout, but the process is the same for any version.

Step 1: Download the PHAR and its signature

wget -O phpunit.phar https://phar.phpunit.de/phpunit-13.phar
wget -O phpunit.phar.asc https://phar.phpunit.de/phpunit-13.phar.asc

Step 2: Import the public key

You need Sebastian Bergmann's public key in your local GPG keyring. You can import it directly from his website:

curl --silent https://sebastian-bergmann.de/gpg.asc | gpg --import

Alternatively, you can fetch it from a key server:

gpg --keyserver pgp.uni-mainz.de --recv-keys 0x4AA394086372C20A

Step 3: Verify the signature

gpg --verify phpunit.phar.asc phpunit.phar

If the PHAR has not been tampered with, GPG will report a good signature:

gpg: Signature made Mon Jul 19 06:13:42 2021 UTC
gpg: using RSA key D8406D0D82947747293778314AA394086372C20A
gpg: issuer "sb@sebastian-bergmann.de"
gpg: Good signature from "Sebastian Bergmann <sb@sebastian-bergmann.de>" [unknown]
gpg:                 aka "Sebastian Bergmann <sebastian@thephp.cc>" [unknown]
gpg:                 aka "Sebastian Bergmann <sebastian@phpunit.de>" [unknown]
gpg:                 aka "Sebastian Bergmann <sebastian@php.net>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
     Primary key fingerprint: D840 6D0D 8294 7747 2937 7831 4AA3 9408 6372 C20A

Understanding the output

At this point, the signature is good, which means the file has not been tampered with. However, you may see a warning that the key is not certified with a trusted signature. This is expected if you have not established a chain of trust to Sebastian Bergmann's key through the GPG web of trust.

A good signature confirms the file was signed by whoever controls the private key matching the fingerprint D840 6D0D 8294 7747 2937 7831 4AA3 9408 6372 C20A. To be certain that this key actually belongs to Sebastian Bergmann, you need to validate the authenticity of the key itself. For example, by verifying the fingerprint through an independent channel or through the PGP web of trust.

What if the signature is bad?

If you see BAD signature, the file has been modified after it was signed. Do not use it. Download the PHAR again from a trusted source and repeat the verification.

Verifying SHA256 hashes

In addition to PGP signatures, SHA256 hashes are published on https://phar.phpunit.de/. You can verify the integrity of a downloaded PHAR by comparing its hash:

sha256sum phpunit.phar

Compare the output against the hash published on the distribution page.

Inspecting the contents of the PHAR

PHPUnit's PHAR bundles all of its dependencies in a single file. The PHAR itself provides three CLI options that let you inspect exactly what is inside it, without having to extract or reverse-engineer anything.

--manifest

Prints a list of every package bundled in the PHAR together with its exact version number:

php phpunit.phar --manifest
phpunit/phpunit: 13.4.1
myclabs/deep-copy: 1.14.0
nikic/php-parser: v5.9.0
phar-io/manifest: 2.0.4
phar-io/version: 3.2.1
phpunit/php-code-coverage: 14.4.1
...

This is the quickest way to check which version of a particular dependency is bundled.

--sbom

Prints the Software Bill of Materials (SBOM) that is embedded in the PHAR. The SBOM uses the CycloneDX 1.7 XML format. It provides the NTIA minimum elements as well as the data fields that BSI TR-03183-2 (version 2.1.0) requires.

For PHPUnit itself and for every bundled component, the SBOM includes its creators, name, version, description, licences, Package URL (purl), and where its source code can be found. The PHP runtime and the PHP extensions that PHPUnit requires are listed as external components, and the dependencies between all components are recorded as well:

php phpunit.phar --sbom
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" version="1">
 <metadata>
  ...
  <component type="framework" bom-ref="pkg:composer/phpunit/phpunit@13.4.1">
   ...
   <group>phpunit</group>
   <name>phpunit</name>
   <version>13.4.1</version>
   <description>The PHP Unit Testing framework.</description>
   <licenses>
    <expression acknowledgement="declared">BSD-3-Clause</expression>
    <expression acknowledgement="concluded">BSD-3-Clause</expression>
   </licenses>
   <purl>pkg:composer/phpunit/phpunit@13.4.1</purl>
   ...
  </component>
  ...
 </metadata>
 <components>
  <component type="library" bom-ref="pkg:composer/myclabs/deep-copy@1.14.0">
   ...
  </component>
  ...
  <component type="platform" bom-ref="php" isExternal="true">
   <manufacturer>
    <url>https://www.php.net/</url>
   </manufacturer>
   <name>php</name>
   <version>8.4.1</version>
   <cpe>cpe:2.3:a:php:php:8.4.1:*:*:*:*:*:*:*</cpe>
  </component>
 </components>
 <dependencies>
  ...
 </dependencies>
 ...
</bom>

The SBOM can be fed into supply-chain security tools that track known vulnerabilities across your dependency tree.

PHARs of PHPUnit releases prior to 13.4.1, 12.5.38, 11.5.57, 10.5.66, 9.6.38, and 8.5.56 embed a less detailed SBOM in CycloneDX 1.4 XML format.

--composer-lock

Prints the composer.lock file that was used to install PHPUnit's dependencies when the PHAR was built:

php phpunit.phar --composer-lock
{
    "_readme": [
        "This file locks the dependencies of your project to a known state",
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
        "This file is @generated automatically"
    ],
    "content-hash": "2241ffdbbe5e4379c21a3e012c3e2f34",
    ...
}

This is useful for reproducing the exact build or for source-based distributions (such as Nix) that compile everything from source and need a lock file to guarantee reproducibility.

Together, these three options give you full transparency into what the PHAR contains and how it was built, complementing the signature-based verification described above.

Verifying the SBOM

Starting with PHPUnit 13.4.1, 12.5.38, 11.5.57, 10.5.66, 9.6.38, and 8.5.56, an SBOM is published on https://phar.phpunit.de/ alongside every PHAR, together with a detached PGP signature of the SBOM. For phpunit-13.4.1.phar, for example, these are phpunit-13.4.1.phar.cdx.xml and phpunit-13.4.1.phar.cdx.xml.asc.

This SBOM contains everything that the SBOM embedded in the PHAR contains. In addition, it describes the PHAR file itself: its filename, its download URL, and its SHA-512 hash. Because you do not have to run the PHAR to read this SBOM, you can inspect it, or feed it into supply-chain security tools, before you execute any of the code it describes.

Step 1: Download the PHAR, its SBOM, and the SBOM's signature

Just like phpunit-13.phar always points to the PHAR of the most recent PHPUnit 13 release, phpunit-13.phar.cdx.xml and phpunit-13.phar.cdx.xml.asc always point to its SBOM and the SBOM's signature:

wget -O phpunit.phar https://phar.phpunit.de/phpunit-13.phar
wget -O phpunit.phar.cdx.xml https://phar.phpunit.de/phpunit-13.phar.cdx.xml
wget -O phpunit.phar.cdx.xml.asc https://phar.phpunit.de/phpunit-13.phar.cdx.xml.asc

Step 2: Verify the signature of the SBOM

The SBOM is signed with the same key as the PHAR. After you have imported the public key, verify the signature:

gpg --verify phpunit.phar.cdx.xml.asc phpunit.phar.cdx.xml

As for the PHAR, GPG must report a good signature made with the key that has the fingerprint D840 6D0D 8294 7747 2937 7831 4AA3 9408 6372 C20A.

Step 3: Check that the SBOM describes the PHAR you downloaded

Calculate the SHA-512 hash of the PHAR:

sha512sum phpunit.phar
1112455d8e2a5650c9747b4146af425f02d14c9fcea99f974a420a25d49066ffce3569ee22804aff8e8b4f70c3d7bd04a16453b84e9c02e8d9e29a015d8a9ae9  phpunit.phar

Compare it against the hash that the SBOM records for the PHAR. The SBOM also records the original filename of the PHAR, which tells you which release you downloaded:

<component type="framework" bom-ref="pkg:composer/phpunit/phpunit@13.4.1">
  ...
  <hashes>
    <hash alg="SHA-512">1112455d8e2a5650c9747b4146af425f02d14c9fcea99f974a420a25d49066ffce3569ee22804aff8e8b4f70c3d7bd04a16453b84e9c02e8d9e29a015d8a9ae9</hash>
  </hashes>
  ...
  <properties>
    ...
    <property name="bsi:component:filename">phpunit-13.4.1.phar</property>
    ...
  </properties>
</component>

If the signature of the SBOM is good and the hashes match, then the PHAR you downloaded is exactly the file that the signed SBOM describes.

Using PHIVE for automatic verification

Manually verifying PGP signatures every time you download or update PHPUnit is tedious. PHIVE (the PHAR Installation and Verification Environment) was created to automate this process. PHIVE handles downloading, signature verification, and management of PHAR-based tools for your PHP projects.

Install PHIVE:

wget -O phive.phar https://phar.io/releases/phive.phar
wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc
gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x6AF725270AB81E04D79442549D8A98B29B2D5D79
gpg --verify phive.phar.asc phive.phar
chmod +x phive.phar
sudo mv phive.phar /usr/local/bin/phive

Then install PHPUnit with automatic signature verification:

phive install phpunit

PHIVE downloads the PHAR, fetches the PGP signature, verifies it, and only installs the tool if the signature is valid.

Git commits and tags

Sebastian Bergmann signs Git commits and tags for the PHPUnit source code repository.

Since February 18, 2026, Git commits and tags are signed with an SSH key.

From August 18, 2020 until February 18, 2025, Git commits and tags were signed with a GPG key.

Prior to August 18, 2020, Git commits and tags were not signed.

You can verify signed commits and tags in the PHPUnit source repository on GitHub using the corresponding public key.

Source code

How do you then verify that what is in the PHPUnit source repository is fine to use in your development process?

The PHPUnit project maintains quality and integrity of its source code through several measures:

Verifying a Composer-based installation

When you install PHPUnit via Composer (as a development dependency in your project), the verification story is different from the PHAR approach. Composer does not natively verify signatures on individual packages. Instead, integrity relies on a combination of Composer's built-in mechanisms and additional tooling.

What Composer already does

Composer downloads package metadata over HTTPS from Packagist and checks that the downloaded archive matches the commit hash or tag recorded in composer.lock. When you commit composer.lock to version control, subsequent runs of composer install will fetch the exact same versions and verify that the downloaded contents match the recorded hashes. This protects against silent changes to a package after it was locked, but it does not verify that the package was authored by the person you expect.

Composer verifies its own integrity during self-updates. Composer treats repositories as canonical by default. This prevents an attacker from injecting a higher version from a repository they control.

Signature verification of Git tags

Composer does not verify GPG or SSH signatures on the Git tags it downloads. There is currently no maintained Composer plugin that fills this gap.

If you need to verify tag signatures, you would have to do so manually against the PHPUnit source repository using the appropriate public key (see the Git commits and tags section above). You would also have to do this for all dependencies of PHPUnit.

When to prefer the PHAR

The PHAR distribution provides stronger verification out of the box: every release has a detached GPG signature that can be verified before execution. The PHAR also avoids dependency conflicts with your project's own dependencies. If cryptographic verification of PHPUnit itself is important to you, the PHAR (verified manually or via PHIVE) is the better choice.

Verify the verify

You should not blindly trust this page or the verification tools. The GPG commands shown here are standard and can be independently confirmed from the GPG documentation. The key fingerprint published here can be cross-referenced against Sebastian Bergmann's code signing page, his profiles on GitHub, and other independent sources.

If in doubt, verify through multiple channels.