Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/phar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ jobs:
- name: Test PHAR
run: php bin/sentry-agent help

- name: Test PHAR signature mismatch
run: |
cp bin/sentry-agent /tmp/sentry-agent
echo "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" > /tmp/sentry-agent.sig
! php /tmp/sentry-agent help

- name: Commit PHAR
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
with:
Expand Down
86 changes: 86 additions & 0 deletions agent/src/PharSignatureVerifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Sentry\Agent;

/**
* Verifies the packaged PHAR against its adjacent Box-generated signature file.
*/
final class PharSignatureVerifier
{
private const PHAR_SHA512_SIGNATURE_LENGTH = 64;
private const PHAR_SHA512_SIGNATURE_HEX_LENGTH = 128;
private const PHAR_SHA512_TRAILER = "\x04\x00\x00\x00GBMB";

public static function verifyRunningPhar(): void
{
if (!class_exists('Phar') || \Phar::running(false) === '') {
return;
}

$pharPath = \Phar::running(false);

self::verify($pharPath, $pharPath . '.sig');
}

public static function verify(string $pharPath, string $signaturePath): void
{
$expectedSignature = self::readExpectedSignature($signaturePath);
$actualSignature = self::readEmbeddedSha512Signature($pharPath);

if (!hash_equals($expectedSignature, $actualSignature)) {
throw new \RuntimeException('PHAR signature mismatch.');
}
}

private static function readExpectedSignature(string $signaturePath): string
{
if (!is_file($signaturePath) || !is_readable($signaturePath)) {
throw new \RuntimeException(\sprintf('PHAR signature file "%s" was not found or is not readable.', $signaturePath));
}

$contents = @file_get_contents($signaturePath);

if ($contents === false) {
throw new \RuntimeException(\sprintf('Failed to read PHAR signature file "%s".', $signaturePath));
}

$signature = strtoupper(trim($contents));

if (preg_match('/\A[0-9A-F]+\z/', $signature) !== 1 || \strlen($signature) !== self::PHAR_SHA512_SIGNATURE_HEX_LENGTH) {
throw new \RuntimeException(\sprintf('PHAR signature file "%s" is malformed.', $signaturePath));
}

return $signature;
}

private static function readEmbeddedSha512Signature(string $pharPath): string
{
if (!is_file($pharPath) || !is_readable($pharPath)) {
throw new \RuntimeException(\sprintf('PHAR "%s" was not found or is not readable.', $pharPath));
}

$contents = @file_get_contents($pharPath);

if ($contents === false) {
throw new \RuntimeException(\sprintf('Failed to read PHAR "%s".', $pharPath));
}

$signatureTrailerLength = self::PHAR_SHA512_SIGNATURE_LENGTH + \strlen(self::PHAR_SHA512_TRAILER);

if (\strlen($contents) < $signatureTrailerLength) {
throw new \RuntimeException(\sprintf('Failed to read PHAR signature from "%s".', $pharPath));
}

$trailer = substr($contents, -\strlen(self::PHAR_SHA512_TRAILER));

if ($trailer !== self::PHAR_SHA512_TRAILER) {
throw new \RuntimeException(\sprintf('PHAR "%s" is not signed with the expected SHA-512 signature trailer.', $pharPath));
}

$signature = substr($contents, -$signatureTrailerLength, self::PHAR_SHA512_SIGNATURE_LENGTH);

return strtoupper(bin2hex($signature));
}
}
10 changes: 10 additions & 0 deletions agent/src/sentry-agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@
use Sentry\Agent\Envelope;
use Sentry\Agent\EnvelopeForwarder;
use Sentry\Agent\EnvelopeQueue;
use Sentry\Agent\PharSignatureVerifier;
use Sentry\Agent\Server;

require_once __DIR__ . '/PharSignatureVerifier.php';

try {
PharSignatureVerifier::verifyRunningPhar();
} catch (RuntimeException $e) {
fwrite(\STDERR, "sentry-agent [ERROR] {$e->getMessage()}\n");
exit(1);
}

$vendorPath = __DIR__ . '/../vendor';

if (class_exists('Phar') && Phar::running(false) !== '') {
Expand Down
135 changes: 135 additions & 0 deletions agent/tests/PharSignatureVerifierTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace Sentry\Agent\Tests;

use PHPUnit\Framework\TestCase;
use Sentry\Agent\PharSignatureVerifier;

class PharSignatureVerifierTest extends TestCase
{
private const PHAR_SHA512_TRAILER = "\x04\x00\x00\x00GBMB";

/**
* @var string[]
*/
private $temporaryFiles = [];

protected function tearDown(): void
{
foreach ($this->temporaryFiles as $temporaryFile) {
if (\is_file($temporaryFile)) {
\unlink($temporaryFile);
}
}

$this->temporaryFiles = [];
}

public function testVerifySucceedsWhenSignatureMatches(): void
{
$fixture = $this->createSha512SignedPharFixture();

PharSignatureVerifier::verify($fixture['phar'], $fixture['signature']);

$this->addToAssertionCount(1);
}

public function testVerifyFailsWhenSignatureDoesNotMatch(): void
{
$fixture = $this->createSha512SignedPharFixture();

$this->writeFile($fixture['signature'], \str_repeat('B2', 64) . "\n");

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('PHAR signature mismatch.');

PharSignatureVerifier::verify($fixture['phar'], $fixture['signature']);
}

public function testVerifyFailsWhenSignatureFileIsMissing(): void
{
$fixture = $this->createSha512SignedPharFixture();

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('was not found or is not readable');

PharSignatureVerifier::verify($fixture['phar'], $fixture['signature'] . '.missing');
}

public function testVerifyFailsWhenSignatureFileIsMalformed(): void
{
$fixture = $this->createSha512SignedPharFixture();

$this->writeFile($fixture['signature'], 'not-a-signature');

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('is malformed');

PharSignatureVerifier::verify($fixture['phar'], $fixture['signature']);
}

public function testVerifyFailsWhenPharDoesNotUseSha512Trailer(): void
{
$fixture = $this->createPharFixture("\x03\x00\x00\x00GBMB");

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('is not signed with the expected SHA-512 signature trailer');

PharSignatureVerifier::verify($fixture['phar'], $fixture['signature']);
}

public function testVerifyRunningPharSkipsSourceMode(): void
{
PharSignatureVerifier::verifyRunningPhar();

$this->addToAssertionCount(1);
}

/**
* @return array{phar: string, signature: string}
*/
private function createSha512SignedPharFixture(): array
{
return $this->createPharFixture(self::PHAR_SHA512_TRAILER);
}

/**
* @return array{phar: string, signature: string}
*/
private function createPharFixture(string $trailer): array
{
$signature = \str_repeat("\xA1", 64);
$phar = $this->createTemporaryFile();
$signatureFile = $this->createTemporaryFile();

$this->writeFile($phar, 'phar contents' . $signature . $trailer);
$this->writeFile($signatureFile, \strtoupper(\bin2hex($signature)) . "\n");

return [
'phar' => $phar,
'signature' => $signatureFile,
];
}

private function createTemporaryFile(): string
{
$temporaryFile = \tempnam(\sys_get_temp_dir(), 'sentry-agent-phar-');

if ($temporaryFile === false) {
throw new \RuntimeException('Failed to create temporary file.');
}

$this->temporaryFiles[] = $temporaryFile;

return $temporaryFile;
}

private function writeFile(string $path, string $contents): void
{
if (\file_put_contents($path, $contents) === false) {
throw new \RuntimeException(\sprintf('Failed to write temporary file "%s".', $path));
}
}
}
Binary file modified bin/sentry-agent
Binary file not shown.
2 changes: 1 addition & 1 deletion bin/sentry-agent.sig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
C00F482330557B994A19052C273479E02BB5FE510ED165678DF52F97E60975B31A113CDC7273414A2CAE6E364AF3466A2B32AE16F010CE5C69C88BCA848CCF01
C5A1B020D608BDEF9E7D690A7DA18DF1BE0F3FF534304B3CED4734096A415F63B448F8156224E80F530CF203582C124CE04F4BABF968F5A905DA83199B5F6EF9
Loading