https?:\/\/[\w\-\.]+[\w]+(:[\d]+)?[\w\-\.#\/@]+)")) (?=.*(signature="(?P[\w+\/]+={0,2})")) (?=.*(headers="\(request-target\)(?P[\w\\-\s]+)"))? (?=.*(algorithm="(?P[\w\-]+)"))? /x'; protected IncomingRequest $request; public function __construct(IncomingRequest $request = null) { if (! $request instanceof IncomingRequest) { $request = Services::request(); } $this->request = $request; } /** * Verify an incoming message based upon its HTTP signature * * @return bool True if signature has been verified. Otherwise false */ public function verify(): bool { if (! ($dateHeader = $this->request->header('date'))) { throw new Exception('Request must include a date header.'); } // verify that request has been made within the last hour $currentTime = Time::now(); $requestTime = Time::createFromFormat('D, d M Y H:i:s T', $dateHeader->getValue()); $diff = $requestTime->difference($currentTime); $diffSeconds = $diff->getSeconds(); if ($diffSeconds > 3600 || $diffSeconds < 0) { throw new Exception('Request must be made within the last hour.'); } // check that digest header is set if (! ($digestHeader = $this->request->header('digest'))) { throw new Exception('Request must include a digest header'); } // compute body digest and compare with header digest $bodyDigest = hash('sha256', $this->request->getBody(), true); $digest = 'SHA-256=' . base64_encode($bodyDigest); if ($digest !== $digestHeader->getValue()) { throw new Exception('Request digest is incorrect.'); } // read the Signature header if (($signature = $this->request->getHeaderLine('signature')) === '') { // Signature header not found throw new Exception('Request must include a signature header'); } // Split it into its parts (keyId, headers and signature) if (! ($parts = $this->splitSignature($signature))) { throw new Exception('Malformed signature string.'); } // set $keyId, $headers and $signature variables $keyId = $parts['keyId']; $algorithm = $parts['algorithm']; $headers = $parts['headers'] ?? 'date'; $signature = $parts['signature']; // Fetch the public key linked from keyId $actorRequest = new ActivityRequest($keyId); $actorResponse = $actorRequest->get(); $actor = json_decode($actorResponse->getBody(), false, 512, JSON_THROW_ON_ERROR); $publicKeyPem = (string) $actor->publicKey->publicKeyPem; // Create a comparison string from the plaintext headers we got // in the same order as was given in the signature header, $data = $this->getPlainText(explode(' ', trim($headers))); // Verify the data string using the public key and the original signature. return $this->verifySignature($publicKeyPem, $data, $signature, $algorithm); } /** * Split HTTP signature into its parts (keyId, headers and signature) * * @return array|false */ private function splitSignature(string $signature): bool|array { if (! preg_match(self::SIGNATURE_PATTERN, $signature, $matches, PREG_UNMATCHED_AS_NULL)) { // Signature pattern failed return false; } // Headers are optional if (! isset($matches['headers']) || $matches['headers'] === '') { $matches['headers'] = 'date'; } return $matches; } /** * Get plain text that has been originally signed * * @param string[] $headers HTTP header keys */ private function getPlainText(array $headers): string { $strings = []; $strings[] = sprintf( '(request-target): %s %s%s', $this->request->getMethod(), '/' . $this->request->uri->getPath(), $this->request->uri->getQuery() !== '' ? '?' . $this->request->uri->getQuery() : '', ); foreach ($headers as $key) { if ($this->request->hasHeader($key)) { $strings[] = "{$key}: {$this->request->getHeaderLine($key)}"; } } return implode("\n", $strings); } /** * Verifies the signature depending on the algorithm sent */ private function verifySignature( string $publicKeyPem, string $data, string $signature, string $algorithm = 'rsa-sha256' ): bool { if ($algorithm === 'rsa-sha512' || $algorithm === 'rsa-sha256') { $hash = substr($algorithm, strpos($algorithm, '-') + 1); $rsa = new RSA(); $rsa->setHash($hash); $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); $rsa->loadKey($publicKeyPem); return $rsa->verify($data, (string) base64_decode($signature, true)); } // not implemented return false; } }