- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
151 lines
4.7 KiB
PHP
151 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Security\RequestSigning;
|
|
|
|
use App\Framework\Http\Request;
|
|
|
|
/**
|
|
* Verifies HTTP request signatures
|
|
*/
|
|
final readonly class RequestVerifier
|
|
{
|
|
public function __construct(
|
|
private SigningKeyRepository $keyRepository,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Verify a request signature
|
|
*/
|
|
public function verify(Request $request): VerificationResult
|
|
{
|
|
try {
|
|
// Extract signature from header
|
|
$signatureHeader = $request->headers->getFirst('Signature');
|
|
if ($signatureHeader === null) {
|
|
return VerificationResult::failure('Missing Signature header');
|
|
}
|
|
|
|
$signature = RequestSignature::fromHttpSignatureHeader($signatureHeader);
|
|
|
|
// Check if signature is expired
|
|
if ($signature->isExpired()) {
|
|
return VerificationResult::failure('Signature has expired');
|
|
}
|
|
|
|
// Get signing key
|
|
$key = $this->keyRepository->findByKeyId($signature->keyId);
|
|
if ($key === null) {
|
|
return VerificationResult::failure('Unknown key ID: ' . $signature->keyId);
|
|
}
|
|
|
|
if (! $key->isValid()) {
|
|
return VerificationResult::failure('Signing key is not valid');
|
|
}
|
|
|
|
// Verify algorithm matches
|
|
if ($key->algorithm !== $signature->algorithm) {
|
|
return VerificationResult::failure('Algorithm mismatch');
|
|
}
|
|
|
|
// Build signing string
|
|
$signingString = new SigningString($request);
|
|
$stringToSign = $signingString->build($signature->headers);
|
|
|
|
// Verify signature
|
|
$isValid = $this->verifySignature($stringToSign, $signature->signature, $key);
|
|
|
|
if ($isValid) {
|
|
return VerificationResult::success($signature, $key);
|
|
} else {
|
|
return VerificationResult::failure('Invalid signature');
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
return VerificationResult::failure('Signature verification error: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify signature using the appropriate algorithm
|
|
*/
|
|
private function verifySignature(string $data, string $signature, SigningKey $key): bool
|
|
{
|
|
return match ($key->algorithm) {
|
|
SigningAlgorithm::HMAC_SHA256 => $this->verifyHmacSignature($data, $signature, $key, 'sha256'),
|
|
SigningAlgorithm::HMAC_SHA512 => $this->verifyHmacSignature($data, $signature, $key, 'sha512'),
|
|
SigningAlgorithm::RSA_SHA256 => $this->verifyRsaSignature($data, $signature, $key),
|
|
SigningAlgorithm::ED25519 => throw new \InvalidArgumentException('ED25519 not yet implemented'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify HMAC signature
|
|
*/
|
|
private function verifyHmacSignature(string $data, string $signature, SigningKey $key, string $algorithm): bool
|
|
{
|
|
$expectedSignature = hash_hmac($algorithm, $data, $key->keyMaterial, true);
|
|
$expectedSignatureBase64 = base64_encode($expectedSignature);
|
|
|
|
return hash_equals($expectedSignatureBase64, $signature);
|
|
}
|
|
|
|
/**
|
|
* Verify RSA signature
|
|
*/
|
|
private function verifyRsaSignature(string $data, string $signature, SigningKey $key): bool
|
|
{
|
|
// Extract public key from private key
|
|
$privateKey = openssl_pkey_get_private($key->keyMaterial);
|
|
if ($privateKey === false) {
|
|
return false;
|
|
}
|
|
|
|
$keyDetails = openssl_pkey_get_details($privateKey);
|
|
if ($keyDetails === false || ! isset($keyDetails['key'])) {
|
|
return false;
|
|
}
|
|
|
|
$publicKey = openssl_pkey_get_public($keyDetails['key']);
|
|
if ($publicKey === false) {
|
|
return false;
|
|
}
|
|
|
|
$signatureBinary = base64_decode($signature);
|
|
if ($signatureBinary === false) {
|
|
return false;
|
|
}
|
|
|
|
$result = openssl_verify($data, $signatureBinary, $publicKey, $key->algorithm->getOpenSSLSignatureAlgorithm());
|
|
|
|
return $result === 1;
|
|
}
|
|
|
|
/**
|
|
* Verify digest header if present
|
|
*/
|
|
public function verifyDigest(Request $request): bool
|
|
{
|
|
$digestHeader = $request->headers->getFirst('Digest');
|
|
if ($digestHeader === null) {
|
|
return true; // No digest to verify
|
|
}
|
|
|
|
$body = $request->body;
|
|
|
|
// Parse digest header: "SHA256=base64hash"
|
|
if (! preg_match('/^([A-Z0-9]+)=(.+)$/', $digestHeader, $matches)) {
|
|
return false;
|
|
}
|
|
|
|
$algorithm = strtolower($matches[1]);
|
|
$expectedHash = $matches[2];
|
|
|
|
$actualHash = base64_encode(hash($algorithm, $body, true));
|
|
|
|
return hash_equals($expectedHash, $actualHash);
|
|
}
|
|
}
|