├── .gitignore ├── .scrutinizer.yml ├── .travis.coverage.sh ├── .travis.install.sh ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── examples └── successful-installation.sh ├── humbug.json.dist ├── phpcs.xml.dist ├── phpstan.neon ├── phpstan.test.neon ├── phpunit.xml.dist ├── src └── Roave │ └── ComposerGpgVerify │ ├── Exception │ ├── PackagesTrustCheckFailed.php │ └── PreferredInstallIsNotSource.php │ ├── Package │ ├── Git │ │ └── GitSignatureCheck.php │ ├── GitPackage.php │ ├── PackageVerification.php │ └── UnknownPackageFormat.php │ └── Verify.php └── test └── RoaveTest └── ComposerGpgVerify ├── Exception ├── PackagesTrustCheckFailedTest.php └── PreferredInstallIsNotSourceTest.php ├── Package ├── Git │ └── GitSignatureCheckTest.php ├── GitPackageTest.php └── UnknownPackageFormatTest.php └── VerifyTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | clover.xml 4 | humbuglog.txt 5 | humbuglog.json 6 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | before_commands: 2 | - "composer install --no-dev --prefer-source --no-scripts" 3 | 4 | tools: 5 | external_code_coverage: 6 | timeout: 1200 7 | php_code_coverage: 8 | enabled: true 9 | php_code_sniffer: 10 | enabled: true 11 | config: 12 | standard: PSR2 13 | filter: 14 | paths: ["src/*", "test/*"] 15 | php_cpd: 16 | enabled: true 17 | excluded_dirs: ["examples", "test", "vendor"] 18 | php_cs_fixer: 19 | enabled: true 20 | config: 21 | level: all 22 | filter: 23 | paths: ["src/*", "test/*"] 24 | php_loc: 25 | enabled: true 26 | excluded_dirs: ["examples", "test", "vendor"] 27 | php_mess_detector: 28 | enabled: true 29 | filter: 30 | paths: ["src/*"] 31 | php_pdepend: 32 | enabled: true 33 | excluded_dirs: ["examples", "test", "vendor"] 34 | php_analyzer: 35 | enabled: true 36 | filter: 37 | paths: ["src/*", "test/*"] 38 | sensiolabs_security_checker: true 39 | -------------------------------------------------------------------------------- /.travis.coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | IFS=$'\n\t' 5 | 6 | if [ "$TRAVIS_PHP_VERSION" = '7.1' ] ; then 7 | wget https://scrutinizer-ci.com/ocular.phar 8 | php ocular.phar code-coverage:upload --format=php-clover ./clover.xml 9 | fi 10 | -------------------------------------------------------------------------------- /.travis.install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeu 4 | IFS=$'\n\t' 5 | 6 | composer self-update 7 | composer clear-cache 8 | composer update --no-scripts 9 | 10 | if [ "$DEPENDENCIES" = 'low' ] ; then 11 | composer update --prefer-lowest --prefer-stable --no-scripts 12 | fi 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: php 4 | 5 | php: 6 | - 7.1 7 | - nightly 8 | 9 | env: 10 | matrix: 11 | - DEPENDENCIES="high" 12 | - DEPENDENCIES="low" 13 | 14 | before_install: 15 | - sudo add-apt-repository ppa:git-core/ppa -y 16 | - sudo apt-get update -q 17 | - sudo apt-get install -y --only-upgrade git 18 | - git fetch --unshallow 19 | - git fetch --all 20 | 21 | before_script: 22 | - sh .travis.install.sh 23 | 24 | script: 25 | - ./vendor/bin/phpunit --coverage-clover ./clover.xml 26 | - ./vendor/bin/phpcs --standard=./phpcs.xml.dist src test 27 | - ./vendor/bin/phpstan analyse -c phpstan.neon -l 7 src 28 | - ./vendor/bin/phpstan analyse -c phpstan.test.neon -l 5 test 29 | - ./vendor/bin/humbug 30 | - ./examples/successful-installation.sh 31 | 32 | matrix: 33 | allow_failures: 34 | - php: nightly 35 | 36 | after_script: 37 | - sh .travis.coverage.sh 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Roave, LLC. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Composer GPG signature verification plugin 2 | 3 | [![Packagist](https://img.shields.io/packagist/v/roave/composer-gpg-verify.svg)](https://packagist.org/packages/roave/composer-gpg-verify) 4 | [![Build Status](https://travis-ci.org/Roave/composer-gpg-verify.svg?branch=master)](https://travis-ci.org/Roave/composer-gpg-verify) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Roave/composer-gpg-verify/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Roave/composer-gpg-verify/?branch=master) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/Roave/composer-gpg-verify/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Roave/composer-gpg-verify/?branch=master) 7 | 8 | This package provides pluggable composer tag signature verification. 9 | 10 | Specifically, all this package does is stop the installation process 11 | when an un-trusted package is encountered. 12 | 13 | The aim of this package is to be a first reference implementation to 14 | be later used in composer itself to enforce good dependency checking 15 | hygiene. 16 | 17 | ## Usage 18 | 19 | This package provides no usable public API, but will only act during 20 | the composer installation setup: 21 | 22 | ```php 23 | composer require roave/composer-gpg-verify --prefer-source 24 | ``` 25 | 26 | Please note that the above may already fail if you have un-trusted 27 | dependencies. In order to skip the checks provided by this package, 28 | use the `--no-scripts` flag if you didn't yet figure out your 29 | un-trusted dependencies: 30 | 31 | ```php 32 | composer require roave/composer-gpg-verify --prefer-source --no-scripts 33 | ``` 34 | 35 | ## Trusted dependencies 36 | 37 | This package extensively uses [`GPG`](https://www.gnupg.org/) to 38 | validate that all downloaded dependencies have a good and trusted 39 | GIT tag or commit signature. 40 | 41 | At this moment, the package will just use your local GPG trust 42 | database to determine which signatures are to be trusted or not, 43 | and will not mess with it other than reading from it. 44 | 45 | In practice, this means that: 46 | 47 | * every package you install must be a `git` repository (use 48 | `--prefer-source`) 49 | * the `HEAD` (current state) of each repository must be either a 50 | signed tag or a signed commit 51 | * you must have a local copy of the public key corresponding to 52 | each tag/commit signature 53 | * you must either have explicitly trusted, locally signed or 54 | signed each of the involved public keys 55 | 56 | While this must sound like a useless complication to most users, 57 | as they just trust packagist to provide "good" dependencies, these 58 | may have been forged by an attacker that stole information from 59 | your favorite maintainers. 60 | 61 | Good dependency hygiene is extremely important, and this package 62 | encourages maintainers to always sign their releases, and users 63 | to always check them. 64 | 65 | ## Trusting someone's work 66 | 67 | Assuming that you downloaded a signed package, you will likely 68 | get the following failure during the first installation: 69 | 70 | ``` 71 | composer require some-vendor/some-package --prefer-source 72 | # ... lots of lines here ... 73 | The following packages need to be signed and verified, or added to exclusions: 74 | 75 | some-vendor/some-package 76 | [SIGNED] [NOT VERIFIED] Commit #4b825dc642cb6eb9a060e54bf8d69288fbee4904 (Key AABBCCDDEEFF1122) 77 | Command: git verify-commit --verbose HEAD 78 | Exit code: 1 79 | Output: tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 80 | author Mr. Maintainer 1495040303 +0200 81 | committer Mr. Maintainer 1495040303 +0200 82 | 83 | signed commit 84 | gpg: Signature made Mi 17 Mai 2017 18:58:23 CEST 85 | gpg: using RSA key AABBCCDDEEFF1122 86 | gpg: Can't check signature: No public key 87 | ... more lines ... 88 | ``` 89 | 90 | This means that `some-vendor/some-package` is not trusted. 91 | 92 | That `AABBCCDDEEFF1122` is the key you are missing. Let's download it: 93 | 94 | ``` 95 | gpg --recv-keys AABBCCDDEEFF1122 96 | ``` 97 | 98 | Now the key is in your local DB, but it isn't yet trusted. 99 | 100 | **IMPORTANT**: do not blindly trust or sign other people's GPG 101 | keys - only do so if you effectively know that the key is provided 102 | by them, and you know them at least marginally. Usually, contacting 103 | the key author is the best way to check authenticity. 104 | 105 | To trust a key, you can edit it: 106 | 107 | ``` 108 | gpg --edit-key AABBCCDDEEFF1122 109 | ... 110 | 111 | gpg> trust 112 | 113 | ... 114 | 115 | Please decide how far you trust this user to correctly verify other users' keys 116 | (by looking at passports, checking fingerprints from different sources, etc.) 117 | 118 | 1 = I don't know or won't say 119 | 2 = I do NOT trust 120 | 3 = I trust marginally 121 | 4 = I trust fully 122 | 5 = I trust ultimately 123 | m = back to the main menu 124 | 125 | Your decision? 3 126 | 127 | gpg> save 128 | ``` 129 | 130 | Alternatively, if you want to sign the gpg key, you can create a 131 | local signature: 132 | 133 | ```sh 134 | gpg --lsign-key AABBCCDDEEFF1122 135 | ``` 136 | 137 | If you *really* trust a key, you can create a generic signature 138 | that may be uploaded: 139 | 140 | ```sh 141 | gpg --sign-key AABBCCDDEEFF1122 142 | ``` 143 | 144 | Once you did any of the above (signing or trusting), then you may 145 | resume your composer installation or upgrade process. 146 | 147 | ## Examples 148 | 149 | Please refer to the [examples](examples) directory for running 150 | examples in your system. All examples are designed in a way that 151 | will leave your current GPG settings untouched. 152 | 153 | ## Limitations 154 | 155 | This package still has few serious limitations: 156 | 157 | * it needs `gpg` `2.x` to run - this means that you should probably 158 | be on Ubuntu 16.04 or equivalent. 159 | * it needs `gpg` `2.x` 160 | * it can only verify signatures of downloaded GIT repositories: any 161 | non-git packages will cause the validation to fail 162 | 163 | These limitations will eventually be softened as development of 164 | further versions of the library continues. 165 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roave/composer-gpg-verify", 3 | "description": "Composer plugin that verifies GPG signatures of downloaded dependencies, enforcing trusted GIT tags", 4 | "type": "composer-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Marco Pivetta", 9 | "email": "ocramius@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.1.4", 14 | "composer-plugin-api": "^1.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^5.7.19", 18 | "humbug/humbug": "dev-master", 19 | "phpstan/phpstan": "^0.7", 20 | "nikic/php-parser": ">=3.0.5", 21 | "composer/composer": "^1.4.2", 22 | "squizlabs/php_codesniffer": "^3.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Roave\\ComposerGpgVerify\\": "src/Roave/ComposerGpgVerify" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "RoaveTest\\ComposerGpgVerify\\": "test/RoaveTest/ComposerGpgVerify" 32 | } 33 | }, 34 | "extra": { 35 | "class": "Roave\\ComposerGpgVerify\\Verify", 36 | "branch-alias": { 37 | "dev-master": "2.0.x-dev" 38 | } 39 | }, 40 | "scripts": { 41 | "post-update-cmd": "Roave\\ComposerGpgVerify\\Verify::verify", 42 | "post-install-cmd": "Roave\\ComposerGpgVerify\\Verify::verify" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/successful-installation.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | IFS=$'\n\t' 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | LIBRARY_COMMIT="$( git rev-parse HEAD )" 7 | BRANCH=${TRAVIS_BRANCH:-"$( git branch --contains HEAD | head -n 1 | cut -d' ' -f2)"} 8 | 9 | echo "First, we will work in a temporary directory" 10 | BASEDIR="$( mktemp -d )" 11 | 12 | echo "Let's not mess with your system's GPG environment :-)" 13 | export GNUPGHOME="$BASEDIR/gnupg-home" 14 | mkdir "$GNUPGHOME" 15 | chmod 0700 "$GNUPGHOME" 16 | 17 | echo "This is ocramius@gmail.com's GPG key that we are downloading:" 18 | gpg --keyserver pgp.mit.edu --recv-keys 7BB9B50FE8E021187914A5C09F65FBC212EC2DF8 19 | 20 | echo "Here we instructing GPG to trust this key:" 21 | echo -e "trust\n5\ny\nsave" | gpg --command-fd 0 --edit-key 7BB9B50FE8E021187914A5C09F65FBC212EC2DF8 22 | 23 | echo "Installing and configuring our composer project:" 24 | COMPOSER_PHAR="$BASEDIR/composer.phar" 25 | curl -sS https://getcomposer.org/installer | php -- 26 | mv composer.phar "$COMPOSER_PHAR" 27 | cat > "$BASEDIR/composer.json" << JSON 28 | { 29 | "minimum-stability": "dev", 30 | "config": { 31 | "preferred-install": "source" 32 | }, 33 | "repositories": [ 34 | { 35 | "type": "vcs", 36 | "url": "$DIR/../" 37 | } 38 | ] 39 | } 40 | JSON 41 | 42 | echo "Here we install the roave/composer-gpg-verify plugin, using the version from this repository:" 43 | "$COMPOSER_PHAR" --working-dir="$BASEDIR" require "roave/composer-gpg-verify:dev-$BRANCH#$LIBRARY_COMMIT" --prefer-source 44 | 45 | echo "This package installation will work, because this guy knows how to do releases, and this release is signed:" 46 | "$COMPOSER_PHAR" --working-dir="$BASEDIR" require ocramius/package-versions:1.1.2 --prefer-source 47 | 48 | echo "This commit is not signed - this command should fail with an error:" 49 | "$COMPOSER_PHAR" --working-dir="$BASEDIR" require "ocramius/lazy-map:dev-master#5c77102e225d225ae2d74d5f2cc488527834b821" --prefer-source || exit 0 50 | 51 | echo "Something went wrong - the last command should have failed, and this location should never be reached!" 52 | exit 1 53 | -------------------------------------------------------------------------------- /humbug.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "timeout": 15, 8 | "logs": { 9 | "text": "humbuglog.txt", 10 | "json": "humbuglog.json" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | My custom coding standard. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | -------------------------------------------------------------------------------- /phpstan.test.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | ./test/RoaveTest 13 | 14 | 15 | 16 | ./src 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Roave/ComposerGpgVerify/Exception/PackagesTrustCheckFailed.php: -------------------------------------------------------------------------------- 1 | printReason(); 24 | }, 25 | array_merge([$failedVerification], $furtherFailedVerifications) 26 | ) 27 | ) 28 | )); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Roave/ComposerGpgVerify/Exception/PreferredInstallIsNotSource.php: -------------------------------------------------------------------------------- 1 | packageName = $packageName; // @TODO get rid of this, or add it to the error messages 82 | $this->commitHash = $commitHash; 83 | $this->tagName = $tagName; 84 | $this->command = $command; 85 | $this->exitCode = $exitCode; 86 | $this->output = $output; 87 | $this->isSigned = $isSigned; 88 | $this->isVerified = $isVerified; 89 | $this->signatureAuthor = $signatureAuthor; 90 | $this->signatureKey = $signatureKey; 91 | } 92 | 93 | public static function fromGitCommitCheck( 94 | PackageInterface $package, 95 | string $command, 96 | int $exitCode, 97 | string $output 98 | ) : self { 99 | $signatureKey = self::extractKeyIdentifier($output); 100 | $signed = $signatureKey && ! $exitCode; 101 | 102 | return new self( 103 | $package->getName(), 104 | self::extractCommitHash($output), 105 | null, 106 | $command, 107 | $exitCode, 108 | $output, 109 | $signed, 110 | $signed && self::signatureValidationHasNoWarnings($output), 111 | self::extractSignatureAuthor($output), 112 | $signatureKey 113 | ); 114 | } 115 | 116 | public static function fromGitTagCheck( 117 | PackageInterface $package, 118 | string $command, 119 | int $exitCode, 120 | string $output 121 | ) : self { 122 | $signatureKey = self::extractKeyIdentifier($output); 123 | $signed = $signatureKey && ! $exitCode; 124 | 125 | return new self( 126 | $package->getName(), 127 | self::extractCommitHash($output), 128 | self::extractTagName($output), 129 | $command, 130 | $exitCode, 131 | $output, 132 | $signed, 133 | $signed && self::signatureValidationHasNoWarnings($output), 134 | self::extractSignatureAuthor($output), 135 | self::extractKeyIdentifier($output) 136 | ); 137 | } 138 | 139 | public function asHumanReadableString() : string 140 | { 141 | return implode( 142 | "\n", 143 | [ 144 | (($this->isSigned || $this->signatureKey) ? '[SIGNED]' : '[NOT SIGNED]') 145 | . ' ' . ($this->isVerified ? '[VERIFIED]' : '[NOT VERIFIED]') 146 | . ' ' . ($this->commitHash ? 'Commit #' . $this->commitHash : '') 147 | . ' ' . ($this->tagName ? 'Tag ' . $this->tagName : '') 148 | . ' ' . ($this->signatureAuthor ? 'By "' . $this->signatureAuthor . '"' : '') 149 | . ' ' . ($this->signatureKey ? '(Key ' . $this->signatureKey . ')' : ''), 150 | 'Command: ' . $this->command, 151 | 'Exit code: ' . $this->exitCode, 152 | 'Output: ' . $this->output, 153 | ] 154 | ); 155 | } 156 | 157 | public function canBeTrusted() : bool 158 | { 159 | return $this->isVerified; 160 | } 161 | 162 | private static function extractCommitHash(string $output) : ?string 163 | { 164 | $keys = array_filter(array_map( 165 | function (string $outputRow) { 166 | preg_match('/^(tree|object) ([a-fA-F0-9]{40})$/i', $outputRow, $matches); 167 | 168 | return $matches[2] ?? false; 169 | }, 170 | explode("\n", $output) 171 | )); 172 | 173 | return reset($keys) ?: null; 174 | } 175 | 176 | private static function extractTagName(string $output) : ?string 177 | { 178 | $keys = array_filter(array_map( 179 | function (string $outputRow) { 180 | preg_match('/^tag (.+)$/i', $outputRow, $matches); 181 | 182 | return $matches[1] ?? false; 183 | }, 184 | explode("\n", $output) 185 | )); 186 | 187 | return reset($keys) ?: null; 188 | } 189 | 190 | private static function extractKeyIdentifier(string $output) : ?string 191 | { 192 | $keys = array_filter(array_map( 193 | function (string $outputRow) { 194 | preg_match('/gpg:.*using .* key ([a-fA-F0-9]+)/i', $outputRow, $matches); 195 | 196 | return $matches[1] ?? false; 197 | }, 198 | explode("\n", $output) 199 | )); 200 | 201 | return reset($keys) ?: null; 202 | } 203 | 204 | private static function extractSignatureAuthor(string $output) : ?string 205 | { 206 | $keys = array_filter(array_map( 207 | function (string $outputRow) { 208 | preg_match('/gpg: Good signature from "(.+)" \\[.*\\]/i', $outputRow, $matches); 209 | 210 | return $matches[1] ?? false; 211 | }, 212 | explode("\n", $output) 213 | )); 214 | 215 | return reset($keys) ?: null; 216 | } 217 | 218 | private static function signatureValidationHasNoWarnings(string $output) : bool 219 | { 220 | return ! array_filter( 221 | explode("\n", $output), 222 | function (string $outputRow) { 223 | return false !== strpos($outputRow, 'gpg: WARNING: '); 224 | } 225 | ); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/Roave/ComposerGpgVerify/Package/GitPackage.php: -------------------------------------------------------------------------------- 1 | packageName = $packageName; 31 | $this->checks = $checks; 32 | } 33 | 34 | public static function fromPackageAndSignatureChecks( 35 | PackageInterface $package, 36 | GitSignatureCheck $firstCheck, 37 | GitSignatureCheck ...$checks 38 | ) : self { 39 | return new self($package->getName(), ...array_merge([$firstCheck], $checks)); 40 | } 41 | 42 | public function packageName() : string 43 | { 44 | return $this->packageName; 45 | } 46 | 47 | public function isVerified() : bool 48 | { 49 | return (bool) $this->passedChecks(); 50 | } 51 | 52 | public function printReason() : string 53 | { 54 | if ($this->isVerified()) { 55 | return sprintf('The following GIT GPG signature checks passed for package "%s":', $this->packageName()) 56 | . "\n\n" 57 | . implode( 58 | "\n\n", 59 | array_map( 60 | function (GitSignatureCheck $check) : string { 61 | return $check->asHumanReadableString(); 62 | }, 63 | $this->passedChecks() 64 | ) 65 | ); 66 | } 67 | 68 | return sprintf('The following GIT GPG signature checks have failed for package "%s":', $this->packageName()) 69 | . "\n\n" 70 | . implode( 71 | "\n\n", 72 | array_map( 73 | function (GitSignatureCheck $check) : string { 74 | return $check->asHumanReadableString(); 75 | }, 76 | $this->failedChecks() 77 | ) 78 | ); 79 | } 80 | 81 | /** 82 | * @return GitSignatureCheck[] 83 | */ 84 | private function passedChecks() : array 85 | { 86 | return array_values(array_filter( 87 | $this->checks, 88 | function (GitSignatureCheck $check) { 89 | return $check->canBeTrusted(); 90 | } 91 | )); 92 | } 93 | 94 | /** 95 | * @return GitSignatureCheck[] 96 | */ 97 | private function failedChecks() : array 98 | { 99 | return array_values(array_filter( 100 | $this->checks, 101 | function (GitSignatureCheck $check) { 102 | return ! $check->canBeTrusted(); 103 | } 104 | )); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Roave/ComposerGpgVerify/Package/PackageVerification.php: -------------------------------------------------------------------------------- 1 | packageName = $packageName; 25 | } 26 | 27 | public static function fromNonGitPackage(PackageInterface $package) : self 28 | { 29 | return new self($package->getName()); 30 | } 31 | 32 | public function packageName(): string 33 | { 34 | return $this->packageName; 35 | } 36 | 37 | public function isVerified(): bool 38 | { 39 | return false; 40 | } 41 | 42 | public function printReason(): string 43 | { 44 | return sprintf( 45 | 'Package "%s" is in a format that Roave\\ComposerGpgVerify cannot verify:' 46 | . ' try forcing it to be downloaded as GIT repository', 47 | $this->packageName() 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Roave/ComposerGpgVerify/Verify.php: -------------------------------------------------------------------------------- 1 | 'verify', 32 | ]; 33 | } 34 | 35 | /** 36 | * {@inheritDoc} 37 | * 38 | * @codeCoverageIgnore 39 | */ 40 | public function activate(Composer $composer, IOInterface $io) : void 41 | { 42 | // Nothing to do here, as all features are provided through event listeners 43 | } 44 | 45 | /** 46 | * @param Event $composerEvent 47 | * 48 | * @throws \Roave\ComposerGpgVerify\Exception\PackagesTrustCheckFailed 49 | * @throws \RuntimeException 50 | * @throws \Roave\ComposerGpgVerify\Exception\PreferredInstallIsNotSource 51 | */ 52 | public static function verify(Event $composerEvent) : void 53 | { 54 | $originalLanguage = getenv('LANGUAGE'); 55 | $io = $composerEvent->getIO(); 56 | $composer = $composerEvent->getComposer(); 57 | $config = $composer->getConfig(); 58 | 59 | $io->write('roave/composer-gpg-verify: Analysing downloaded packages...'); 60 | 61 | self::assertSourceInstallation($config); 62 | 63 | // prevent output changes caused by locale settings on the system where this script is running 64 | putenv(sprintf('LANGUAGE=%s', 'en_US')); 65 | 66 | $installationManager = $composer->getInstallationManager(); 67 | /* @var $checkedPackages PackageVerification[] */ 68 | $checkedPackages = array_map( 69 | function (PackageInterface $package) use ($installationManager) : PackageVerification { 70 | return self::verifyPackage($installationManager, $package); 71 | }, 72 | $composer->getRepositoryManager()->getLocalRepository()->getPackages() 73 | ); 74 | 75 | putenv(sprintf('LANGUAGE=%s', (string) $originalLanguage)); 76 | 77 | $escapes = array_filter( 78 | $checkedPackages, 79 | function (PackageVerification $verification) : bool { 80 | return ! $verification->isVerified(); 81 | } 82 | ); 83 | 84 | if (! $escapes) { 85 | $io->write('roave/composer-gpg-verify: All installed packages passed GPG validation!'); 86 | 87 | return; 88 | } 89 | 90 | throw PackagesTrustCheckFailed::fromFailedPackageVerifications(...$escapes); 91 | } 92 | 93 | private static function verifyPackage( 94 | InstallationManager $installationManager, 95 | PackageInterface $package 96 | ) : PackageVerification { 97 | $gitDirectory = $installationManager->getInstallPath($package) . '/.git'; 98 | 99 | if (! is_dir($gitDirectory)) { 100 | return UnknownPackageFormat::fromNonGitPackage($package); 101 | } 102 | 103 | return GitPackage::fromPackageAndSignatureChecks( 104 | $package, 105 | self::checkCurrentCommitSignature($gitDirectory, $package), 106 | ...self::checkTagSignatures($gitDirectory, $package, ...self::getTagsForCurrentCommit($gitDirectory)) 107 | ); 108 | } 109 | 110 | private static function checkCurrentCommitSignature( 111 | string $gitDirectory, 112 | PackageInterface $package 113 | ) : GitSignatureCheck { 114 | $command = sprintf( 115 | 'git --git-dir %s verify-commit --verbose HEAD 2>&1', 116 | escapeshellarg($gitDirectory) 117 | ); 118 | 119 | exec($command, $output, $exitCode); 120 | 121 | return GitSignatureCheck::fromGitCommitCheck($package, $command, $exitCode, implode("\n", $output)); 122 | } 123 | 124 | /** 125 | * @param string $gitDirectory 126 | * 127 | * @return string[] 128 | */ 129 | private static function getTagsForCurrentCommit(string $gitDirectory) : array 130 | { 131 | exec( 132 | sprintf( 133 | 'git --git-dir %s tag --points-at HEAD 2>&1', 134 | escapeshellarg($gitDirectory) 135 | ), 136 | $tags 137 | ); 138 | 139 | return array_values(array_filter($tags)); 140 | } 141 | 142 | 143 | /** 144 | * @param string $gitDirectory 145 | * @param PackageInterface $package 146 | * @param string[] $tags 147 | * 148 | * @return GitSignatureCheck[] 149 | */ 150 | private static function checkTagSignatures(string $gitDirectory, PackageInterface $package, string ...$tags) : array 151 | { 152 | return array_map( 153 | function (string $tag) use ($gitDirectory, $package) : GitSignatureCheck { 154 | $command = sprintf( 155 | 'git --git-dir %s tag -v %s 2>&1', 156 | escapeshellarg($gitDirectory), 157 | escapeshellarg($tag) 158 | ); 159 | 160 | exec($command, $tagSignatureOutput, $exitCode); 161 | 162 | return GitSignatureCheck::fromGitTagCheck( 163 | $package, 164 | $command, 165 | $exitCode, 166 | implode("\n", $tagSignatureOutput) 167 | ); 168 | }, 169 | $tags 170 | ); 171 | } 172 | 173 | /** 174 | * @param Config $config 175 | * 176 | * 177 | * @throws \RuntimeException 178 | * @throws \Roave\ComposerGpgVerify\Exception\PreferredInstallIsNotSource 179 | */ 180 | private static function assertSourceInstallation(Config $config) : void 181 | { 182 | $preferredInstall = $config->get('preferred-install'); 183 | 184 | if ('source' !== $preferredInstall) { 185 | throw PreferredInstallIsNotSource::fromPreferredInstall($preferredInstall); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /test/RoaveTest/ComposerGpgVerify/Exception/PackagesTrustCheckFailedTest.php: -------------------------------------------------------------------------------- 1 | createMock(PackageVerification::class); 21 | 22 | $verification->expects(self::any())->method('printReason')->willReturn('foo'); 23 | 24 | $exception = PackagesTrustCheckFailed::fromFailedPackageVerifications($verification); 25 | 26 | self::assertInstanceOf(PackagesTrustCheckFailed::class, $exception); 27 | self::assertInstanceOf(LogicException::class, $exception); 28 | self::assertSame( 29 | <<<'EXPECTED' 30 | The following packages need to be signed and verified, or added to exclusions: 31 | foo 32 | EXPECTED 33 | , 34 | $exception->getMessage() 35 | ); 36 | } 37 | 38 | public function testFromFailedPackageVerificationsWithMultiplePackages() : void 39 | { 40 | /* @var $verification1 PackageVerification|\PHPUnit_Framework_MockObject_MockObject */ 41 | $verification1 = $this->createMock(PackageVerification::class); 42 | /* @var $verification2 PackageVerification|\PHPUnit_Framework_MockObject_MockObject */ 43 | $verification2 = $this->createMock(PackageVerification::class); 44 | /* @var $verification3 PackageVerification|\PHPUnit_Framework_MockObject_MockObject */ 45 | $verification3 = $this->createMock(PackageVerification::class); 46 | 47 | $verification1->expects(self::any())->method('printReason')->willReturn('foo'); 48 | $verification2->expects(self::any())->method('printReason')->willReturn('bar'); 49 | $verification3->expects(self::any())->method('printReason')->willReturn('baz'); 50 | 51 | $exception = PackagesTrustCheckFailed::fromFailedPackageVerifications( 52 | $verification1, 53 | $verification2, 54 | $verification3 55 | ); 56 | 57 | self::assertInstanceOf(PackagesTrustCheckFailed::class, $exception); 58 | self::assertInstanceOf(LogicException::class, $exception); 59 | self::assertSame( 60 | <<<'EXPECTED' 61 | The following packages need to be signed and verified, or added to exclusions: 62 | foo 63 | 64 | bar 65 | 66 | baz 67 | EXPECTED 68 | , 69 | $exception->getMessage() 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/RoaveTest/ComposerGpgVerify/Exception/PreferredInstallIsNotSourceTest.php: -------------------------------------------------------------------------------- 1 | getMessage() 29 | ); 30 | } 31 | public function testFromArrayPreferredInstall() : void 32 | { 33 | $exception = PreferredInstallIsNotSource::fromPreferredInstall(['foo' => 'bar', 'baz' => 'tab']); 34 | 35 | self::assertInstanceOf(PreferredInstallIsNotSource::class, $exception); 36 | self::assertInstanceOf(InvalidArgumentException::class, $exception); 37 | self::assertSame( 38 | <<<'EXPECTED' 39 | The detected preferred install required for the git verification to work correctly is "source", but your composer.json configuration reported {"foo":"bar","baz":"tab"}. 40 | Please edit your composer.json to enforce "source" installation as described at https://getcomposer.org/doc/06-config.md#preferred-install 41 | EXPECTED 42 | , 43 | $exception->getMessage() 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/RoaveTest/ComposerGpgVerify/Package/Git/GitSignatureCheckTest.php: -------------------------------------------------------------------------------- 1 | canBeTrusted()); 30 | self::assertStringMatchesFormat($expectedHumanReadableStringFormat, $check->asHumanReadableString()); 31 | } 32 | 33 | public function commitDataProvider() : array 34 | { 35 | $packageName = uniqid('packageName', true); 36 | $package = $this->createMock(PackageInterface::class); 37 | 38 | $package->expects(self::any())->method('getName')->willReturn($packageName); 39 | 40 | return [ 41 | 'empty' => [ 42 | $package, 43 | '', 44 | 0, 45 | '', 46 | false, 47 | <<<'READABLE' 48 | [NOT SIGNED] [NOT VERIFIED] 49 | Command: 50 | Exit code: 0 51 | Output: 52 | READABLE 53 | ], 54 | 'not signed' => [ 55 | $package, 56 | 'git verify-commit --verbose HEAD', 57 | 1, 58 | '', 59 | false, 60 | <<<'READABLE' 61 | [NOT SIGNED] [NOT VERIFIED] 62 | Command: git verify-commit --verbose HEAD 63 | Exit code: 1 64 | Output: 65 | READABLE 66 | ], 67 | 'signed, no key' => [ 68 | $package, 69 | 'git verify-commit --verbose HEAD', 70 | 1, 71 | <<<'OUTPUT' 72 | tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 73 | author Mr. Magoo 1495040303 +0200 74 | committer Mr. Magoo 1495040303 +0200 75 | 76 | signed commit 77 | gpg: Signature made Mi 17 Mai 2017 18:58:23 CEST 78 | gpg: using RSA key ECFE352F73409A6E 79 | gpg: Can't check signature: No public key 80 | OUTPUT 81 | , 82 | false, 83 | <<<'READABLE' 84 | [SIGNED] [NOT VERIFIED] Commit #4b825dc642cb6eb9a060e54bf8d69288fbee4904 (Key ECFE352F73409A6E) 85 | Command: git verify-commit --verbose HEAD 86 | Exit code: 1 87 | Output: tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 88 | author Mr. Magoo 1495040303 +0200 89 | committer Mr. Magoo 1495040303 +0200 90 | 91 | signed commit 92 | gpg: Signature made Mi 17 Mai 2017 18:58:23 CEST 93 | gpg: using RSA key ECFE352F73409A6E 94 | gpg: Can't check signature: No public key 95 | READABLE 96 | ], 97 | 'signed, key not trusted' => [ 98 | $package, 99 | 'git verify-commit --verbose HEAD', 100 | 0, 101 | <<<'OUTPUT' 102 | tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 103 | author Mr. Magoo 1495041438 +0200 104 | committer Mr. Magoo 1495041438 +0200 105 | 106 | signed commit 107 | gpg: Signature made Mi 17 Mai 2017 19:17:18 CEST 108 | gpg: using RSA key 3CD2E574BC4207C7 109 | gpg: Good signature from "Mr. Magoo " [unknown] 110 | gpg: WARNING: This key is not certified with a trusted signature! 111 | gpg: There is no indication that the signature belongs to the owner. 112 | Primary key fingerprint: AA0E 63DC BC06 F864 F53E F630 3CD2 E574 BC42 07C7 113 | OUTPUT 114 | , 115 | false, 116 | <<<'READABLE' 117 | [SIGNED] [NOT VERIFIED] Commit #4b825dc642cb6eb9a060e54bf8d69288fbee4904 By "Mr. Magoo " (Key 3CD2E574BC4207C7) 118 | Command: git verify-commit --verbose HEAD 119 | Exit code: 0 120 | Output: tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 121 | author Mr. Magoo 1495041438 +0200 122 | committer Mr. Magoo 1495041438 +0200 123 | 124 | signed commit 125 | gpg: Signature made Mi 17 Mai 2017 19:17:18 CEST 126 | gpg: using RSA key 3CD2E574BC4207C7 127 | gpg: Good signature from "Mr. Magoo " [unknown] 128 | gpg: WARNING: This key is not certified with a trusted signature! 129 | gpg: There is no indication that the signature belongs to the owner. 130 | Primary key fingerprint: AA0E 63DC BC06 F864 F53E F630 3CD2 E574 BC42 07C7 131 | READABLE 132 | ], 133 | [ 134 | $package, 135 | 'git verify-commit --verbose HEAD', 136 | 0, 137 | <<<'OUTPUT' 138 | tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 139 | author Mr. Magoo 1495041602 +0200 140 | committer Mr. Magoo 1495041602 +0200 141 | 142 | signed commit 143 | gpg: Signature made Mi 17 Mai 2017 19:20:02 CEST 144 | gpg: using RSA key 4889C20D148231DC 145 | gpg: Good signature from "Mr. Magoo " [full] 146 | OUTPUT 147 | , 148 | true, 149 | <<<'READABLE' 150 | [SIGNED] [VERIFIED] Commit #4b825dc642cb6eb9a060e54bf8d69288fbee4904 By "Mr. Magoo " (Key 4889C20D148231DC) 151 | Command: git verify-commit --verbose HEAD 152 | Exit code: 0 153 | Output: tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 154 | author Mr. Magoo 1495041602 +0200 155 | committer Mr. Magoo 1495041602 +0200 156 | 157 | signed commit 158 | gpg: Signature made Mi 17 Mai 2017 19:20:02 CEST 159 | gpg: using RSA key 4889C20D148231DC 160 | gpg: Good signature from "Mr. Magoo " [full] 161 | READABLE 162 | ], 163 | ]; 164 | } 165 | 166 | /** 167 | * @dataProvider tagDataProvider 168 | */ 169 | public function testFromGitTagCheck( 170 | PackageInterface $package, 171 | string $command, 172 | int $exitCode, 173 | string $commandOutput, 174 | bool $expectedTrust, 175 | string $expectedHumanReadableStringFormat 176 | ) : void { 177 | $check = GitSignatureCheck::fromGitTagCheck($package, $command, $exitCode, $commandOutput); 178 | 179 | self::assertSame($expectedTrust, $check->canBeTrusted()); 180 | self::assertStringMatchesFormat($expectedHumanReadableStringFormat, $check->asHumanReadableString()); 181 | } 182 | 183 | public function tagDataProvider() : array 184 | { 185 | $packageName = uniqid('packageName', true); 186 | $package = $this->createMock(PackageInterface::class); 187 | 188 | $package->expects(self::any())->method('getName')->willReturn($packageName); 189 | 190 | return [ 191 | 'empty' => [ 192 | $package, 193 | '', 194 | 0, 195 | '', 196 | false, 197 | <<<'READABLE' 198 | [NOT SIGNED] [NOT VERIFIED] 199 | Command: 200 | Exit code: 0 201 | Output: 202 | READABLE 203 | ], 204 | 'failed verification - no signed tag' => [ 205 | $package, 206 | 'git tag -v --verbose unsigned-tag', 207 | 1, 208 | '', 209 | false, 210 | <<<'READABLE' 211 | [NOT SIGNED] [NOT VERIFIED] 212 | Command: git tag -v --verbose unsigned-tag 213 | Exit code: 1 214 | Output: 215 | READABLE 216 | ], 217 | 'failed verification - signed tag, unknown key' => [ 218 | $package, 219 | 'git tag -v tag-name', 220 | 1, 221 | <<<'OUTPUT' 222 | object bf2fabeabe00f14f0ce0090adc7a2b9b770edbe3 223 | type commit 224 | tag tag-name 225 | tagger Mr. Magoo 1495094925 +0200 226 | 227 | signed tag 228 | gpg: keybox '/tmp/gpg-verification-test591d568c5d0947.51554486//pubring.kbx' created 229 | gpg: Signature made Do 18 Mai 2017 10:08:45 CEST 230 | gpg: using RSA key 4B95C0CE4DE340CC 231 | gpg: Can't check signature: No public key 232 | OUTPUT 233 | , 234 | false, 235 | <<<'READABLE' 236 | [SIGNED] [NOT VERIFIED] Commit #bf2fabeabe00f14f0ce0090adc7a2b9b770edbe3 Tag tag-name (Key 4B95C0CE4DE340CC) 237 | Command: git tag -v tag-name 238 | Exit code: 1 239 | Output: object bf2fabeabe00f14f0ce0090adc7a2b9b770edbe3 240 | type commit 241 | tag tag-name 242 | tagger Mr. Magoo 1495094925 +0200 243 | 244 | signed tag 245 | gpg: keybox '/tmp/gpg-verification-test591d568c5d0947.51554486//pubring.kbx' created 246 | gpg: Signature made Do 18 Mai 2017 10:08:45 CEST 247 | gpg: using RSA key 4B95C0CE4DE340CC 248 | gpg: Can't check signature: No public key 249 | READABLE 250 | ], 251 | 'failed verification - signed tag, untrusted key' => [ 252 | $package, 253 | 'git tag -v tag-name', 254 | 0, 255 | <<<'OUTPUT' 256 | object be3b7a6f0ee4d90a72e1e1a19f89d8eeef746200 257 | type commit 258 | tag tag-name 259 | tagger Mr. Magoo 1495095245 +0200 260 | 261 | signed tag 262 | gpg: Signature made Do 18 Mai 2017 10:14:05 CEST 263 | gpg: using RSA key 865E20A60B500B00 264 | gpg: checking the trustdb 265 | gpg: marginals needed: 3 completes needed: 1 trust model: pgp 266 | gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u 267 | gpg: Good signature from "Mr. Magoo " [unknown] 268 | gpg: WARNING: This key is not certified with a trusted signature! 269 | gpg: There is no indication that the signature belongs to the owner. 270 | Primary key fingerprint: D8BE 3E96 4271 2378 9551 AF87 865E 20A6 0B50 0B00 271 | OUTPUT 272 | , 273 | false, 274 | <<<'READABLE' 275 | [SIGNED] [NOT VERIFIED] Commit #be3b7a6f0ee4d90a72e1e1a19f89d8eeef746200 Tag tag-name By "Mr. Magoo " (Key 865E20A60B500B00) 276 | Command: git tag -v tag-name 277 | Exit code: 0 278 | Output: object be3b7a6f0ee4d90a72e1e1a19f89d8eeef746200 279 | type commit 280 | tag tag-name 281 | tagger Mr. Magoo 1495095245 +0200 282 | 283 | signed tag 284 | gpg: Signature made Do 18 Mai 2017 10:14:05 CEST 285 | gpg: using RSA key 865E20A60B500B00 286 | gpg: checking the trustdb 287 | gpg: marginals needed: 3 completes needed: 1 trust model: pgp 288 | gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u 289 | gpg: Good signature from "Mr. Magoo " [unknown] 290 | gpg: WARNING: This key is not certified with a trusted signature! 291 | gpg: There is no indication that the signature belongs to the owner. 292 | Primary key fingerprint: D8BE 3E96 4271 2378 9551 AF87 865E 20A6 0B50 0B00 293 | READABLE 294 | ], 295 | 'successful verification' => [ 296 | $package, 297 | 'git tag -v tag-name', 298 | 0, 299 | <<<'OUTPUT' 300 | object 99498872c90de4d40b2fcafad7bd1bb2cbd0433a 301 | type commit 302 | tag tag-name 303 | tagger Mr. Magoo 1495095451 +0200 304 | 305 | signed tag 306 | gpg: Signature made Do 18 Mai 2017 10:17:31 CEST 307 | gpg: using RSA key E9AE0662BC840E1F 308 | gpg: Good signature from "Mr. Magoo " [full] 309 | OUTPUT 310 | , 311 | true, 312 | <<<'READABLE' 313 | [SIGNED] [VERIFIED] Commit #99498872c90de4d40b2fcafad7bd1bb2cbd0433a Tag tag-name By "Mr. Magoo " (Key E9AE0662BC840E1F) 314 | Command: git tag -v tag-name 315 | Exit code: 0 316 | Output: object 99498872c90de4d40b2fcafad7bd1bb2cbd0433a 317 | type commit 318 | tag tag-name 319 | tagger Mr. Magoo 1495095451 +0200 320 | 321 | signed tag 322 | gpg: Signature made Do 18 Mai 2017 10:17:31 CEST 323 | gpg: using RSA key E9AE0662BC840E1F 324 | gpg: Good signature from "Mr. Magoo " [full] 325 | READABLE 326 | ], 327 | ]; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /test/RoaveTest/ComposerGpgVerify/Package/GitPackageTest.php: -------------------------------------------------------------------------------- 1 | packageName = uniqid('packageName', true); 32 | 33 | /* @var $package PackageInterface|\PHPUnit_Framework_MockObject_MockObject */ 34 | $package = $this->createMock(PackageInterface::class); 35 | 36 | $package->expects(self::any())->method('getName')->willReturn($this->packageName); 37 | 38 | $this->package = $package; 39 | } 40 | 41 | public function testWillReportSuccessfulVerification() : void 42 | { 43 | $verification = GitPackage::fromPackageAndSignatureChecks( 44 | $this->package, 45 | $this->makeSignatureCheck(true, 'yadda') 46 | ); 47 | 48 | self::assertInstanceOf(GitPackage::class, $verification); 49 | self::assertSame($this->packageName, $verification->packageName()); 50 | self::assertTrue($verification->isVerified()); 51 | self::assertSame( 52 | <<packageName}": 54 | 55 | yadda 56 | REASON 57 | , 58 | $verification->printReason() 59 | ); 60 | } 61 | 62 | public function testWillReportSuccessfulVerificationWithMultipleChecks() : void 63 | { 64 | $verification = GitPackage::fromPackageAndSignatureChecks( 65 | $this->package, 66 | $this->makeSignatureCheck(true, 'yadda'), 67 | $this->makeSignatureCheck(true, 'dadda'), 68 | $this->makeSignatureCheck(true, 'wadda'), 69 | $this->makeSignatureCheck(false, 'nope') 70 | ); 71 | 72 | self::assertInstanceOf(GitPackage::class, $verification); 73 | self::assertSame($this->packageName, $verification->packageName()); 74 | self::assertTrue($verification->isVerified()); 75 | self::assertSame( 76 | <<packageName}": 78 | 79 | yadda 80 | 81 | dadda 82 | 83 | wadda 84 | REASON 85 | , 86 | $verification->printReason() 87 | ); 88 | } 89 | 90 | public function testWillReportFailedVerification() : void 91 | { 92 | $verification = GitPackage::fromPackageAndSignatureChecks( 93 | $this->package, 94 | $this->makeSignatureCheck(false, 'yadda') 95 | ); 96 | 97 | self::assertInstanceOf(GitPackage::class, $verification); 98 | self::assertSame($this->packageName, $verification->packageName()); 99 | self::assertFalse($verification->isVerified()); 100 | self::assertSame( 101 | <<packageName}": 103 | 104 | yadda 105 | REASON 106 | , 107 | $verification->printReason() 108 | ); 109 | } 110 | 111 | public function testWillReportFailedVerificationWithMultipleChecks() : void 112 | { 113 | $verification = GitPackage::fromPackageAndSignatureChecks( 114 | $this->package, 115 | $this->makeSignatureCheck(false, 'yadda'), 116 | $this->makeSignatureCheck(false, 'dadda'), 117 | $this->makeSignatureCheck(false, 'wadda'), 118 | $this->makeSignatureCheck(false, 'nope') 119 | ); 120 | 121 | self::assertInstanceOf(GitPackage::class, $verification); 122 | self::assertSame($this->packageName, $verification->packageName()); 123 | self::assertFalse($verification->isVerified()); 124 | self::assertSame( 125 | <<packageName}": 127 | 128 | yadda 129 | 130 | dadda 131 | 132 | wadda 133 | 134 | nope 135 | REASON 136 | , 137 | $verification->printReason() 138 | ); 139 | } 140 | 141 | public function testWillReportSuccessfulVerificationWithMultipleChecksAndSuccessfulOneNotFirst() : void 142 | { 143 | $verification = GitPackage::fromPackageAndSignatureChecks( 144 | $this->package, 145 | $this->makeSignatureCheck(false, 'yadda'), 146 | $this->makeSignatureCheck(false, 'dadda'), 147 | $this->makeSignatureCheck(false, 'wadda'), 148 | $this->makeSignatureCheck(true, 'yarp') 149 | ); 150 | 151 | self::assertInstanceOf(GitPackage::class, $verification); 152 | self::assertSame($this->packageName, $verification->packageName()); 153 | self::assertTrue($verification->isVerified()); 154 | self::assertSame( 155 | <<packageName}": 157 | 158 | yarp 159 | REASON 160 | , 161 | $verification->printReason() 162 | ); 163 | } 164 | 165 | private function makeSignatureCheck(bool $passed, string $reasoning) : GitSignatureCheck 166 | { 167 | /* @var $verification GitSignatureCheck|\PHPUnit_Framework_MockObject_MockObject */ 168 | $verification = $this->createMock(GitSignatureCheck::class); 169 | 170 | $verification->expects(self::any())->method('asHumanReadableString')->willReturn($reasoning); 171 | $verification->expects(self::any())->method('canBeTrusted')->willReturn($passed); 172 | 173 | return $verification; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/RoaveTest/ComposerGpgVerify/Package/UnknownPackageFormatTest.php: -------------------------------------------------------------------------------- 1 | createMock(PackageInterface::class); 20 | $packageName = uniqid('packageName', true); 21 | 22 | $package->expects(self::any())->method('getName')->willReturn($packageName); 23 | 24 | $verification = UnknownPackageFormat::fromNonGitPackage($package); 25 | 26 | self::assertInstanceOf(UnknownPackageFormat::class, $verification); 27 | self::assertSame($packageName, $verification->packageName()); 28 | self::assertFalse($verification->isVerified(), 'Unknown package types cannot be verified'); 29 | self::assertSame( 30 | 'Package "' 31 | . $packageName 32 | . '" is in a format that Roave\ComposerGpgVerify cannot verify:' 33 | . ' try forcing it to be downloaded as GIT repository', 34 | $verification->printReason() 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/RoaveTest/ComposerGpgVerify/VerifyTest.php: -------------------------------------------------------------------------------- 1 | installedPackages = []; 82 | $this->originalGpgHome = (string) getenv('GNUPGHOME'); 83 | $this->originalLanguage = (string) getenv('LANGUAGE'); 84 | 85 | $this->event = $this->createMock(Event::class); 86 | $this->composer = $this->createMock(Composer::class); 87 | $this->io = $this->createMock(IOInterface::class); 88 | $this->config = $this->createMock(Config::class); 89 | $this->repositoryManager = $this->createMock(RepositoryManager::class); 90 | $this->installationManager = $this->createMock(InstallationManager::class); 91 | $this->localRepository = $this->createMock(RepositoryInterface::class); 92 | 93 | $this->event->expects(self::any())->method('getComposer')->willReturn($this->composer); 94 | $this->event->expects(self::any())->method('getIO')->willReturn($this->io); 95 | $this->composer->expects(self::any())->method('getConfig')->willReturn($this->config); 96 | $this 97 | ->composer 98 | ->expects(self::any()) 99 | ->method('getRepositoryManager') 100 | ->willReturn($this->repositoryManager); 101 | $this 102 | ->composer 103 | ->expects(self::any()) 104 | ->method('getInstallationManager') 105 | ->willReturn($this->installationManager); 106 | $this 107 | ->repositoryManager 108 | ->expects(self::any()) 109 | ->method('getLocalRepository') 110 | ->willReturn($this->localRepository); 111 | $this 112 | ->installationManager 113 | ->expects(self::any()) 114 | ->method('getInstallPath') 115 | ->willReturnCallback(function (PackageInterface $package) : string { 116 | return array_search($package, $this->installedPackages, true); 117 | }); 118 | $this 119 | ->localRepository 120 | ->expects(self::any()) 121 | ->method('getPackages') 122 | ->willReturnCallback(function () { 123 | return array_values($this->installedPackages); 124 | }); 125 | } 126 | 127 | protected function tearDown() : void 128 | { 129 | putenv(sprintf('GNUPGHOME=%s', $this->originalGpgHome)); 130 | putenv(sprintf('LANGUAGE=%s', $this->originalLanguage)); 131 | 132 | parent::tearDown(); 133 | } 134 | 135 | public function testWillDisallowInstallationOnNonSourceInstall() : void 136 | { 137 | $this 138 | ->config 139 | ->expects(self::any()) 140 | ->method('get') 141 | ->with('preferred-install') 142 | ->willReturn('foo'); 143 | 144 | $this->expectException(PreferredInstallIsNotSource::class); 145 | 146 | Verify::verify($this->event); 147 | } 148 | 149 | public function testWillRetrieveSubscribedEvents() : void 150 | { 151 | $events = Verify::getSubscribedEvents(); 152 | 153 | self::assertNotEmpty($events); 154 | 155 | $availableEvents = (new \ReflectionClass(ScriptEvents::class))->getConstants(); 156 | 157 | foreach ($events as $eventName => $callback) { 158 | self::assertContains($eventName, $availableEvents); 159 | self::assertInternalType('string', $callback); 160 | self::assertInternalType('callable', [Verify::class, $callback]); 161 | } 162 | } 163 | 164 | public function testWillRejectNonGitPackages() : void 165 | { 166 | $packageName = 'vendor1/package1'; 167 | $vendor1 = sys_get_temp_dir() . '/' . uniqid('vendor', true) . '/' . $packageName; 168 | 169 | /* @var $package PackageInterface|\PHPUnit_Framework_MockObject_MockObject */ 170 | $package = $this->createMock(PackageInterface::class); 171 | 172 | $package->expects(self::any())->method('getName')->willReturn($packageName); 173 | 174 | $this->installedPackages[$vendor1] = $package; 175 | 176 | self::assertTrue(mkdir($vendor1, 0700, true)); 177 | $this->configureCorrectComposerSetup(); 178 | 179 | $this->assertWillFailPackageVerification(); 180 | } 181 | 182 | public function testWillAcceptSignedAndTrustedPackages() : void 183 | { 184 | $gpgHomeDirectory = $this->makeGpgHomeDirectory(); 185 | 186 | $vendorName = 'Mr. Magoo'; 187 | $vendorEmail = 'magoo@example.com'; 188 | $vendorKey = $this->makeKey($gpgHomeDirectory, $vendorEmail, $vendorName); 189 | $vendorDir = $this->makeVendorDirectory(); 190 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 191 | 192 | $this->signDependency($vendor1, $gpgHomeDirectory, $vendorKey); 193 | 194 | $this->configureCorrectComposerSetup(); 195 | 196 | putenv('GNUPGHOME=' . $gpgHomeDirectory); 197 | 198 | $this->assertWillSucceedPackageVerification(); 199 | } 200 | 201 | public function testWillRejectPackageSignedWithImportedButUnTrustedKey() : void 202 | { 203 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 204 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 205 | 206 | $this->makeKey($personalGpgDirectory, 'me@example.com', 'Just Me'); 207 | 208 | $vendorName = 'Mr. Magoo'; 209 | $vendorEmail = 'magoo@example.com'; 210 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 211 | $vendorDir = $this->makeVendorDirectory(); 212 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 213 | 214 | $this->signDependency($vendor1, $foreignGpgDirectory, $vendorKey); 215 | 216 | $this->importForeignKeys($personalGpgDirectory, $foreignGpgDirectory, $vendorKey, false); 217 | 218 | $this->configureCorrectComposerSetup(); 219 | 220 | putenv('GNUPGHOME=' . $personalGpgDirectory); 221 | 222 | $this->assertWillFailPackageVerification(); 223 | } 224 | 225 | public function testWillRejectPackageSignedWithImportedButUnTrustedKeyWithDifferentLocaleSettings() : void 226 | { 227 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 228 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 229 | 230 | $this->makeKey($personalGpgDirectory, 'me@example.com', 'Just Me'); 231 | 232 | $vendorName = 'Mr. Magoo'; 233 | $vendorEmail = 'magoo@example.com'; 234 | 235 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 236 | $vendorDir = $this->makeVendorDirectory(); 237 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 238 | 239 | $this->signDependency($vendor1, $foreignGpgDirectory, $vendorKey); 240 | 241 | $this->importForeignKeys($personalGpgDirectory, $foreignGpgDirectory, $vendorKey, false); 242 | 243 | $this->configureCorrectComposerSetup(); 244 | 245 | putenv('GNUPGHOME=' . $personalGpgDirectory); 246 | putenv('LANGUAGE=de_DE'); 247 | 248 | try { 249 | Verify::verify($this->event); 250 | } catch (PackagesTrustCheckFailed $failure) { 251 | self::assertSame('de_DE', getenv('LANGUAGE')); 252 | 253 | return; 254 | } 255 | 256 | self::fail('Exception was not thrown'); 257 | } 258 | 259 | public function testWillAcceptPackageSignedWithImportedAndTrustedKey() : void 260 | { 261 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 262 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 263 | 264 | $this->makeKey($personalGpgDirectory, 'me@example.com', 'Just Me'); 265 | 266 | $vendorName = 'Mr. Magoo'; 267 | $vendorEmail = 'magoo@example.com'; 268 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 269 | $vendorDir = $this->makeVendorDirectory(); 270 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 271 | 272 | $this->signDependency($vendor1, $foreignGpgDirectory, $vendorKey); 273 | 274 | $this->importForeignKeys($personalGpgDirectory, $foreignGpgDirectory, $vendorKey, true); 275 | 276 | $this->configureCorrectComposerSetup(); 277 | 278 | putenv('GNUPGHOME=' . $personalGpgDirectory); 279 | 280 | $this->assertWillSucceedPackageVerification(); 281 | } 282 | 283 | public function testWillRejectPackageTaggedAndSignedWithImportedButUnTrustedKey() : void 284 | { 285 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 286 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 287 | 288 | $this->makeKey($personalGpgDirectory, 'me@example.com', 'Just Me'); 289 | 290 | $vendorName = 'Mr. Magoo'; 291 | $vendorEmail = 'magoo@example.com'; 292 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 293 | $vendorDir = $this->makeVendorDirectory(); 294 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 295 | 296 | $this->createDependencySignedTag($vendor1, $foreignGpgDirectory, $vendorKey); 297 | 298 | $this->importForeignKeys($personalGpgDirectory, $foreignGpgDirectory, $vendorKey, false); 299 | 300 | $this->configureCorrectComposerSetup(); 301 | 302 | putenv('GNUPGHOME=' . $personalGpgDirectory); 303 | 304 | $this->assertWillFailPackageVerification(); 305 | } 306 | 307 | public function testWillAcceptPackageTaggedAndSignedWithImportedAndTrustedKey() : void 308 | { 309 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 310 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 311 | 312 | $this->makeKey($personalGpgDirectory, 'me@example.com', 'Just Me'); 313 | 314 | $vendorName = 'Mr. Magoo'; 315 | $vendorEmail = 'magoo@example.com'; 316 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 317 | $vendorDir = $this->makeVendorDirectory(); 318 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 319 | 320 | $this->createDependencySignedTag($vendor1, $foreignGpgDirectory, $vendorKey); 321 | 322 | $this->importForeignKeys($personalGpgDirectory, $foreignGpgDirectory, $vendorKey, true); 323 | 324 | $this->configureCorrectComposerSetup(); 325 | 326 | putenv('GNUPGHOME=' . $personalGpgDirectory); 327 | 328 | $this->assertWillSucceedPackageVerification(); 329 | } 330 | 331 | public function testWillAcceptSignedAndTrustedTaggedPackages() : void 332 | { 333 | $gpgHomeDirectory = $this->makeGpgHomeDirectory(); 334 | 335 | $vendorName = 'Mr. Magoo'; 336 | $vendorEmail = 'magoo@example.com'; 337 | $vendorKey = $this->makeKey($gpgHomeDirectory, $vendorEmail, $vendorName); 338 | $vendorDir = $this->makeVendorDirectory(); 339 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 340 | 341 | $this->createDependencySignedTag($vendor1, $gpgHomeDirectory, $vendorKey); 342 | 343 | $this->configureCorrectComposerSetup(); 344 | 345 | putenv('GNUPGHOME=' . $gpgHomeDirectory); 346 | 347 | $this->assertWillSucceedPackageVerification(); 348 | } 349 | 350 | public function testWillRejectUnSignedCommits() : void 351 | { 352 | $vendorName = 'Mr. Magoo'; 353 | $vendorEmail = 'magoo@example.com'; 354 | $vendorDir = $this->makeVendorDirectory(); 355 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 356 | 357 | (new Process('git commit --allow-empty -m "unsigned commit"', $vendor1)) 358 | ->setTimeout(30) 359 | ->mustRun(); 360 | 361 | $this->configureCorrectComposerSetup(); 362 | 363 | putenv('GNUPGHOME=' . $this->makeGpgHomeDirectory()); 364 | 365 | $this->assertWillFailPackageVerification(); 366 | } 367 | 368 | public function testWillRejectUnSignedTags() : void 369 | { 370 | $vendorName = 'Mr. Magoo'; 371 | $vendorEmail = 'magoo@example.com'; 372 | $vendorDir = $this->makeVendorDirectory(); 373 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 374 | 375 | (new Process('git commit --allow-empty -m "unsigned commit"', $vendor1)) 376 | ->setTimeout(30) 377 | ->mustRun(); 378 | 379 | (new Process('git tag unsigned-tag -m "unsigned tag"', $vendor1)) 380 | ->setTimeout(30) 381 | ->mustRun(); 382 | 383 | $this->configureCorrectComposerSetup(); 384 | 385 | putenv('GNUPGHOME=' . $this->makeGpgHomeDirectory()); 386 | 387 | $this->assertWillFailPackageVerification(); 388 | } 389 | 390 | public function testWillRejectSignedTagsFromUnknownKey() : void 391 | { 392 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 393 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 394 | $vendorName = 'Mr. Magoo'; 395 | $vendorEmail = 'magoo@example.com'; 396 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 397 | $vendorDir = $this->makeVendorDirectory(); 398 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 399 | 400 | $this->createDependencySignedTag($vendor1, $foreignGpgDirectory, $vendorKey); 401 | 402 | $this->configureCorrectComposerSetup(); 403 | 404 | putenv('GNUPGHOME=' . $personalGpgDirectory); 405 | 406 | $this->assertWillFailPackageVerification(); 407 | } 408 | 409 | public function testWillRejectSignedTagsFromNonHeadCommit() : void 410 | { 411 | $gpgHome = $this->makeGpgHomeDirectory(); 412 | $vendorName = 'Mr. Magoo'; 413 | $vendorEmail = 'magoo@example.com'; 414 | $vendorKey = $this->makeKey($gpgHome, $vendorEmail, $vendorName); 415 | $vendorDir = $this->makeVendorDirectory(); 416 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 417 | 418 | $this->createDependencySignedTag($vendor1, $gpgHome, $vendorKey); 419 | 420 | (new Process('git commit --allow-empty -m "unsigned commit"', $vendor1)) 421 | ->setTimeout(30) 422 | ->mustRun(); 423 | 424 | $this->configureCorrectComposerSetup(); 425 | 426 | putenv('GNUPGHOME=' . $gpgHome); 427 | 428 | $this->assertWillFailPackageVerification(); 429 | } 430 | 431 | public function testWillOnlyConsiderTheHeadCommitForValidation() : void 432 | { 433 | $gpgHome = $this->makeGpgHomeDirectory(); 434 | $vendorName = 'Mr. Magoo'; 435 | $vendorEmail = 'magoo@example.com'; 436 | $vendorKey = $this->makeKey($gpgHome, $vendorEmail, $vendorName); 437 | $vendorDir = $this->makeVendorDirectory(); 438 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 439 | 440 | $this->signDependency($vendor1, $gpgHome, $vendorKey); 441 | 442 | (new Process('git commit --allow-empty -m "unsigned commit"', $vendor1)) 443 | ->setTimeout(30) 444 | ->mustRun(); 445 | 446 | $this->configureCorrectComposerSetup(); 447 | 448 | putenv('GNUPGHOME=' . $gpgHome); 449 | 450 | $this->assertWillFailPackageVerification(); 451 | } 452 | 453 | public function testWillRejectSignedCommitsFromUnknownKeys() : void 454 | { 455 | $personalGpgDirectory = $this->makeGpgHomeDirectory(); 456 | $foreignGpgDirectory = $this->makeGpgHomeDirectory(); 457 | 458 | $vendorName = 'Mr. Magoo'; 459 | $vendorEmail = 'magoo@example.com'; 460 | $vendorKey = $this->makeKey($foreignGpgDirectory, $vendorEmail, $vendorName); 461 | $vendorDir = $this->makeVendorDirectory(); 462 | $vendor1 = $this->makeDependencyGitRepository($vendorDir, 'vendor1/package1', $vendorEmail, $vendorName); 463 | 464 | $this->signDependency($vendor1, $foreignGpgDirectory, $vendorKey); 465 | 466 | $this->configureCorrectComposerSetup(); 467 | 468 | putenv('GNUPGHOME=' . $personalGpgDirectory); 469 | 470 | $this->assertWillFailPackageVerification(); 471 | } 472 | 473 | private function makeVendorDirectory() : string 474 | { 475 | $vendorDirectory = sys_get_temp_dir() . '/' . uniqid('vendor', true); 476 | 477 | self::assertTrue(mkdir($vendorDirectory)); 478 | 479 | return $vendorDirectory; 480 | } 481 | 482 | private function signDependency( 483 | string $dependencyDirectory, 484 | string $gpgHomeDirectory, 485 | string $signingKey 486 | ) : void { 487 | (new Process(sprintf('git config --local --add user.signingkey %s', escapeshellarg($signingKey)), $dependencyDirectory)) 488 | ->setTimeout(30) 489 | ->mustRun(); 490 | 491 | (new Process( 492 | 'git commit --allow-empty -m "signed commit" -S', 493 | $dependencyDirectory, 494 | ['GNUPGHOME' => $gpgHomeDirectory, 'GIT_TRACE' => '2'] 495 | )) 496 | ->setTimeout(30) 497 | ->mustRun(); 498 | } 499 | 500 | private function createDependencySignedTag( 501 | string $dependencyDirectory, 502 | string $gpgHomeDirectory, 503 | string $signingKey 504 | ) : void { 505 | (new Process(sprintf('git config --local --add user.signingkey %s', escapeshellarg($signingKey)), $dependencyDirectory)) 506 | ->setTimeout(30) 507 | ->mustRun(); 508 | 509 | (new Process('git commit --allow-empty -m "unsigned commit"', $dependencyDirectory)) 510 | ->setTimeout(30) 511 | ->mustRun(); 512 | 513 | (new Process( 514 | 'git tag -s "tag-name" -m "signed tag"', 515 | $dependencyDirectory, 516 | ['GNUPGHOME' => $gpgHomeDirectory, 'GIT_TRACE' => '2'] 517 | )) 518 | ->setTimeout(30) 519 | ->mustRun(); 520 | } 521 | 522 | private function makeDependencyGitRepository( 523 | string $vendorDirectory, 524 | string $packageName, 525 | string $email, 526 | string $name 527 | ) : string { 528 | $dependencyRepository = $vendorDirectory . '/' . $packageName; 529 | 530 | self::assertTrue(mkdir($dependencyRepository, 0777, true)); 531 | 532 | (new Process('git init', $dependencyRepository)) 533 | ->setTimeout(30) 534 | ->mustRun(); 535 | 536 | (new Process(sprintf('git config --local --add user.email %s', escapeshellarg($email)), $dependencyRepository)) 537 | ->setTimeout(30) 538 | ->mustRun(); 539 | 540 | (new Process(sprintf('git config --local --add user.name %s', escapeshellarg($name)), $dependencyRepository)) 541 | ->setTimeout(30) 542 | ->mustRun(); 543 | 544 | /* @var $package PackageInterface|\PHPUnit_Framework_MockObject_MockObject */ 545 | $package = $this->createMock(PackageInterface::class); 546 | 547 | $package->expects(self::any())->method('getName')->willReturn($packageName); 548 | 549 | $this->installedPackages[$dependencyRepository] = $package; 550 | 551 | return $dependencyRepository; 552 | } 553 | 554 | private function makeGpgHomeDirectory() : string 555 | { 556 | $homeDirectory = sys_get_temp_dir() . '/' . uniqid('gpg-verification-test', true); 557 | 558 | self::assertTrue(mkdir($homeDirectory, 0700)); 559 | 560 | return $homeDirectory; 561 | } 562 | 563 | private function makeKey(string $gpgHomeDirectory, string $emailAddress, string $name) : string 564 | { 565 | $input = <<<'KEY' 566 | %echo Generating a standard key 567 | Key-Type: RSA 568 | Key-Length: 128 569 | Name-Real: <<>> 570 | Name-Email: <<>> 571 | Expire-Date: 0 572 | %no-protection 573 | %no-ask-passphrase 574 | %commit 575 | %echo done 576 | 577 | KEY; 578 | self::assertGreaterThan( 579 | 0, 580 | file_put_contents( 581 | $gpgHomeDirectory . '/key-info.txt', 582 | str_replace(['<<>>', '<<>>'], [$name, $emailAddress], $input) 583 | ) 584 | ); 585 | 586 | $keyOutput = (new Process( 587 | 'gpg --batch --gen-key -a key-info.txt', 588 | $gpgHomeDirectory, 589 | ['GNUPGHOME' => $gpgHomeDirectory] 590 | )) 591 | ->setTimeout(30) 592 | ->mustRun() 593 | ->getErrorOutput(); 594 | 595 | self::assertRegExp('/key [0-9A-F]+ marked as ultimately trusted/i', $keyOutput); 596 | 597 | preg_match('/key ([0-9A-F]+) marked as ultimately trusted/i', $keyOutput, $matches); 598 | 599 | return $matches[1]; 600 | } 601 | 602 | private function configureCorrectComposerSetup() : void 603 | { 604 | $this 605 | ->config 606 | ->expects(self::any()) 607 | ->method('get') 608 | ->with('preferred-install') 609 | ->willReturn('source'); 610 | } 611 | 612 | private function assertWillSucceedPackageVerification() : void 613 | { 614 | $this 615 | ->io 616 | ->expects(self::exactly(2)) 617 | ->method('write') 618 | ->with(self::logicalOr( 619 | 'roave/composer-gpg-verify: Analysing downloaded packages...', 620 | 'roave/composer-gpg-verify: All installed packages passed GPG validation!' 621 | )); 622 | 623 | Verify::verify($this->event); 624 | } 625 | 626 | private function assertWillFailPackageVerification() : void 627 | { 628 | $this 629 | ->io 630 | ->expects(self::once()) 631 | ->method('write') 632 | ->with(self::logicalOr('roave/composer-gpg-verify: Analysing downloaded packages...')); 633 | 634 | $this->expectException(PackagesTrustCheckFailed::class); 635 | 636 | Verify::verify($this->event); 637 | } 638 | 639 | private function importForeignKeys( 640 | string $localGpgHome, 641 | string $foreignGpgHome, 642 | string $foreignKey, 643 | bool $sign 644 | ) : void { 645 | $exportPath = sys_get_temp_dir() . '/' . uniqid('exportedKey', true); 646 | 647 | (new Process( 648 | sprintf('gpg --export --armor > %s', escapeshellarg($exportPath)), 649 | null, 650 | ['GNUPGHOME' => $foreignGpgHome] 651 | )) 652 | ->setTimeout(30) 653 | ->mustRun(); 654 | 655 | self::assertFileExists($exportPath); 656 | 657 | (new Process( 658 | sprintf('gpg --import < %s', escapeshellarg($exportPath)), 659 | null, 660 | ['GNUPGHOME' => $localGpgHome] 661 | )) 662 | ->setTimeout(30) 663 | ->mustRun(); 664 | 665 | if (! $sign) { 666 | return; 667 | } 668 | 669 | (new Process( 670 | sprintf('gpg --batch --yes --sign-key %s', escapeshellarg($foreignKey)), 671 | null, 672 | ['GNUPGHOME' => $localGpgHome] 673 | )) 674 | ->setTimeout(30) 675 | ->mustRun(); 676 | } 677 | } 678 | --------------------------------------------------------------------------------