├── .github
└── workflows
│ ├── continuous-integration.yml
│ └── release-on-milestone-closed-triggering-release-event.yml
├── .gitignore
├── .laminas-ci.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── composer.json
├── composer.lock
├── infection.json.dist
├── phpcs.xml.dist
├── phpunit.xml.dist
├── psalm.xml.dist
├── renovate.json
├── src
├── CheckerInterface.php
├── Encoder
│ ├── Base64Encoder.php
│ ├── EncoderInterface.php
│ ├── HmacEncoder.php
│ └── Sha1SumEncoder.php
├── FileContentChecker.php
├── FileContentSigner.php
└── SignerInterface.php
└── test
├── fixture
├── UserClass.php
└── UserClassSignedByFileContent.php
└── unit
└── src
├── Encoder
├── Base64EncoderTest.php
├── HmacEncoderTest.php
└── Sha1SumEncoderTest.php
├── FileContentCheckerTest.php
└── FileContentSignerTest.php
/.github/workflows/continuous-integration.yml:
--------------------------------------------------------------------------------
1 | # See https://github.com/laminas/laminas-continuous-integration-action
2 | # Generates a job matrix based on current dependencies and supported version
3 | # ranges, then runs all those jobs
4 | name: "Continuous Integration"
5 |
6 | on:
7 | pull_request:
8 | push:
9 |
10 | jobs:
11 | matrix:
12 | name: Generate job matrix
13 | runs-on: ubuntu-latest
14 | outputs:
15 | matrix: ${{ steps.matrix.outputs.matrix }}
16 | steps:
17 | - name: Gather CI configuration
18 | id: matrix
19 | uses: laminas/laminas-ci-matrix-action@1.29.0
20 |
21 | qa:
22 | name: QA Checks
23 | needs: [ matrix ]
24 | runs-on: ${{ matrix.operatingSystem }}
25 | strategy:
26 | fail-fast: false
27 | matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
28 | steps:
29 | - name: ${{ matrix.name }}
30 | uses: laminas/laminas-continuous-integration-action@1.41.0
31 | env:
32 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
33 | "INFECTION_DASHBOARD_API_KEY": ${{ secrets.INFECTION_DASHBOARD_API_KEY }}
34 | "STRYKER_DASHBOARD_API_KEY": ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
35 | with:
36 | job: ${{ matrix.job }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/release-on-milestone-closed-triggering-release-event.yml:
--------------------------------------------------------------------------------
1 | # Alternate workflow example.
2 | # This one is identical to the one in release-on-milestone.yml, with one change:
3 | # the Release step uses the ORGANIZATION_ADMIN_TOKEN instead, to allow it to
4 | # trigger a release workflow event. This is useful if you have other actions
5 | # that intercept that event.
6 |
7 | name: "Automatic Releases"
8 |
9 | on:
10 | milestone:
11 | types:
12 | - "closed"
13 |
14 | jobs:
15 | release:
16 | name: "GIT tag, release & create merge-up PR"
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: "Checkout"
21 | uses: "actions/checkout@v4"
22 |
23 | - name: "Release"
24 | uses: "laminas/automatic-releases@v1"
25 | with:
26 | command-name: "laminas:automatic-releases:release"
27 | env:
28 | "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
29 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
30 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
31 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
32 |
33 | - name: "Create Merge-Up Pull Request"
34 | uses: "laminas/automatic-releases@v1"
35 | with:
36 | command-name: "laminas:automatic-releases:create-merge-up-pull-request"
37 | env:
38 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
39 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
40 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
41 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
42 |
43 | - name: "Create and/or Switch to new Release Branch"
44 | uses: "laminas/automatic-releases@v1"
45 | with:
46 | command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
47 | env:
48 | "GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
49 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
50 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
51 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
52 |
53 | - name: "Bump Changelog Version On Originating Release Branch"
54 | uses: "laminas/automatic-releases@v1"
55 | with:
56 | command-name: "laminas:automatic-releases:bump-changelog"
57 | env:
58 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
59 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
60 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
61 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
62 |
63 | - name: "Create new milestones"
64 | uses: "laminas/automatic-releases@v1"
65 | with:
66 | command-name: "laminas:automatic-releases:create-milestones"
67 | env:
68 | "GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
69 | "SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
70 | "GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
71 | "GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | phpunit.xml
2 | infection-log.txt
3 | .idea
4 | vendor
5 | .phpunit.result.cache
6 |
--------------------------------------------------------------------------------
/.laminas-ci.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensions": [
3 | "pcov"
4 | ],
5 | "ini": [
6 | "memory_limit=-1"
7 | ],
8 | "exclude": [
9 | {"name": "Infection [8.2, locked]"}
10 | ],
11 | "additional_checks": [
12 | {
13 | "name": "Infection (with PCOV)",
14 | "job": {
15 | "php": "8.4",
16 | "dependencies": "locked",
17 | "command": "./vendor/bin/roave-infection-static-analysis-plugin"
18 | }
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Contributing
3 | ---
4 |
5 | # Contributing
6 |
7 | * Coding standard for the project is [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
8 | * The project aims to follow most [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php)
9 | * Any contribution must provide tests for additional introduced conditions
10 | * Any un-confirmed issue needs a failing test case before being accepted
11 | * Pull requests must be sent from a new hotfix/feature branch, not from `master`.
12 |
13 | ## Installation
14 |
15 | To install the project and run the tests, you need to clone it first:
16 |
17 | ```sh
18 | $ git clone git://github.com/Roave/Signature.git
19 | ```
20 |
21 | You will then need to run a [Composer](https://getcomposer.org/) installation:
22 |
23 | ```sh
24 | $ cd Signature
25 | $ curl -s https://getcomposer.org/installer | php
26 | $ php composer.phar update
27 | ```
28 |
29 | ## Testing
30 |
31 | The PHPUnit version to be used is the one installed as a dev- dependency via composer:
32 |
33 | ```sh
34 | $ vendor/bin/phpunit
35 | ```
36 |
37 | Please ensure all new features or conditions are covered by unit tests.
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Roave, LLC.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :black_nib: Roave\Signature
2 |
3 | Sign and validate signed files made easy.
4 |
5 | **Note: this is not a cryptographic signing library.**
6 |
7 | ## Installation
8 |
9 | The suggested installation method is via [composer](https://getcomposer.org/):
10 |
11 | ```bash
12 | $ composer require roave/signature
13 | ```
14 |
15 | ## Usage examples
16 |
17 | ### Signing a file
18 |
19 | ```php
20 | // Creating a signer
21 | $signer = new \Roave\Signature\FileContentSigner(
22 | new \Roave\Signature\Encoder\Sha1SumEncoder()
23 | );
24 |
25 | // It'll give you a signature to the provided code content
26 | $signature = $signer->sign(file_get_contents('/var/tmp/file.php'));
27 | ```
28 |
29 | ### Validating a signed file
30 |
31 | ```php
32 | // Creating a signer checker
33 | $signer = new \Roave\Signature\FileContentChecker(
34 | new \Roave\Signature\Encoder\Sha1SumEncoder()
35 | );
36 |
37 | // It'll validate the signature on file content
38 | $signer->check(file_get_contents('/var/tmp/signed-file.php'));
39 | ```
40 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "roave/signature",
3 | "description": "Sign and verify stuff",
4 | "license": "MIT",
5 | "require": {
6 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0"
7 | },
8 | "require-dev": {
9 | "phpunit/phpunit": "^11.5.7",
10 | "roave/infection-static-analysis-plugin": "^1.36.0",
11 | "doctrine/coding-standard": "^12.0.0",
12 | "vimeo/psalm": "^6.6.1",
13 | "psalm/plugin-phpunit": "^0.19.2"
14 | },
15 | "autoload": {
16 | "psr-4": {
17 | "Roave\\Signature\\": "src"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Roave\\SignatureTest\\": "test/unit/src",
23 | "Roave\\SignatureTestFixture\\": "test/fixture"
24 | }
25 | },
26 | "minimum-stability": "dev",
27 | "prefer-stable": true,
28 | "config": {
29 | "allow-plugins": {
30 | "dealerdirect/phpcodesniffer-composer-installer": true,
31 | "infection/extension-installer": true
32 | },
33 | "platform": {
34 | "php": "8.2.99"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 10,
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | "logs": {
9 | "text": "php://stderr",
10 | "stryker": {
11 | "report": "/^\\d\\.\\d\\.x$/"
12 | }
13 | },
14 | "minMsi": 100,
15 | "minCoveredMsi": 100
16 | }
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ./src
18 | ./test/unit
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 | ./test/unit
19 |
20 |
21 |
22 |
23 |
24 | src
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/psalm.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "local>Ocramius/.github:renovate-config"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/CheckerInterface.php:
--------------------------------------------------------------------------------
1 | encode($codeWithoutSignature), $signature);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Encoder/EncoderInterface.php:
--------------------------------------------------------------------------------
1 | hmacKey = $hmacKey;
17 | }
18 |
19 | public function encode(string $codeWithoutSignature): string
20 | {
21 | return hash_hmac('sha256', $codeWithoutSignature, $this->hmacKey);
22 | }
23 |
24 | public function verify(string $codeWithoutSignature, string $signature): bool
25 | {
26 | return hash_equals($this->encode($codeWithoutSignature), $signature);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Encoder/Sha1SumEncoder.php:
--------------------------------------------------------------------------------
1 | encode($codeWithoutSignature), $signature);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/FileContentChecker.php:
--------------------------------------------------------------------------------
1 | encoder = $encoder;
24 | }
25 |
26 | public function check(string $phpCode): bool
27 | {
28 | if (! preg_match('{Roave/Signature:\s+([a-zA-Z0-9\/=]+)}', $phpCode, $matches)) {
29 | return false;
30 | }
31 |
32 | return $this->encoder->verify($this->stripCodeSignature($phpCode), $matches[1]);
33 | }
34 |
35 | private function stripCodeSignature(string $phpCode): string
36 | {
37 | $replaced = preg_replace('{[\/\*\s]+Roave/Signature:\s+([a-zA-Z0-9\/\*\/ =]+)}', '', $phpCode);
38 |
39 | assert(is_string($replaced));
40 |
41 | return $replaced;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/FileContentSigner.php:
--------------------------------------------------------------------------------
1 | encoder = $encoder;
19 | }
20 |
21 | public function sign(string $phpCode): string
22 | {
23 | return 'Roave/Signature: ' . $this->encoder->encode($phpCode);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/SignerInterface.php:
--------------------------------------------------------------------------------
1 | encode(' '));
22 | self::assertSame('PD9waHA=', $encoder->encode('verify($value, base64_encode($value)));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/test/unit/src/Encoder/HmacEncoderTest.php:
--------------------------------------------------------------------------------
1 | encode($value),
25 | );
26 | }
27 |
28 | public function testVerify(): void
29 | {
30 | $hmacKey = random_bytes(64);
31 | $value = uniqid('values', true);
32 | self::assertTrue((new HmacEncoder($hmacKey))->verify($value, hash_hmac('sha256', $value, $hmacKey)));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/unit/src/Encoder/Sha1SumEncoderTest.php:
--------------------------------------------------------------------------------
1 | encode($value));
21 | }
22 |
23 | public function testVerify(): void
24 | {
25 | $value = uniqid('values', true);
26 | self::assertTrue((new Sha1SumEncoder())->verify($value, sha1($value)));
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/unit/src/FileContentCheckerTest.php:
--------------------------------------------------------------------------------
1 | encoder = $this->createMock(EncoderInterface::class);
30 | }
31 |
32 | public function testShouldCheckClassFileContent(): void
33 | {
34 | $classFilePath = __DIR__ . '/../../fixture/UserClassSignedByFileContent.php';
35 |
36 | self::assertFileExists($classFilePath);
37 |
38 | $checker = new FileContentChecker(new Base64Encoder());
39 |
40 | $checker->check(self::readFile($classFilePath));
41 | }
42 |
43 | public function testShouldReturnFalseIfSignatureDoesNotMatch(): void
44 | {
45 | $classFilePath = __DIR__ . '/../../fixture/UserClassSignedByFileContent.php';
46 |
47 | self::assertFileExists($classFilePath);
48 |
49 | $expectedSignature = 'YToxOntpOjA7czoxNDE6Ijw/cGhwCgpuYW1lc3BhY2UgU2lnbmF0dXJlVGVzdEZpeHR1cmU7' .
50 | 'CgpjbGFzcyBVc2VyQ2xhc3NTaWduZWRCeUZpbGVDb250ZW50CnsKICAgIHB1YmxpYyAkbmFtZTsKCiAgICBwcm90ZW' .
51 | 'N0ZWQgJHN1cm5hbWU7CgogICAgcHJpdmF0ZSAkYWdlOwp9CiI7fQ==';
52 |
53 | $this->encoder->expects(self::once())->method('verify')->with(
54 | str_replace(
55 | '/** Roave/Signature: ' . $expectedSignature . ' */' . "\n",
56 | '',
57 | self::readFile($classFilePath),
58 | ),
59 | $expectedSignature,
60 | );
61 |
62 | $checker = new FileContentChecker($this->encoder);
63 |
64 | self::assertFalse($checker->check(self::readFile($classFilePath)));
65 | }
66 |
67 | public function testShouldReturnFalseIfClassIsNotSigned(): void
68 | {
69 | $classFilePath = __DIR__ . '/../../fixture/UserClass.php';
70 |
71 | self::assertFileExists($classFilePath);
72 |
73 | $checker = new FileContentChecker($this->encoder);
74 |
75 | self::assertFalse($checker->check(self::readFile($classFilePath)));
76 | }
77 |
78 | private static function readFile(string $path): string
79 | {
80 | $contents = file_get_contents($path);
81 |
82 | assert(is_string($contents));
83 |
84 | return $contents;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/unit/src/FileContentSignerTest.php:
--------------------------------------------------------------------------------
1 | */
17 | public static function signProvider(): array
18 | {
19 | return [
20 | ['Roave/Signature: PD9waHA=', ''],
23 | ['Roave/Signature: cGxhaW4gdGV4dA==', 'plain text'],
24 | ];
25 | }
26 |
27 | /** @dataProvider signProvider */
28 | #[DataProvider('signProvider')]
29 | public function testSign(string $expected, string $inputString): void
30 | {
31 | $signer = new FileContentSigner(new Base64Encoder());
32 |
33 | self::assertSame($expected, $signer->sign($inputString));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------