getOptions(); if (strlen($body) > ($options['bodyMaxSize'] ?? self::BODY_MAXSIZE)) { throw new IncomingRequestException('content of request is too big'); } // generate IncomingSignedRequest based on body and request $signedRequest = new IncomingSignedRequest($body, $this->request, $options); try { // confirm the validity of content and identity of the incoming request $this->confirmIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL); } catch (SignatureException $e) { $this->logger->warning( 'signature could not be verified', [ 'exception' => $e, 'signedRequest' => $signedRequest, 'signatoryManager' => get_class($signatoryManager) ] ); throw $e; } return $signedRequest; } /** * confirm that the Signature is signed using the correct private key, using * clear version of the Signature and the public key linked to the keyId * * @param IIncomingSignedRequest $signedRequest * @param ISignatoryManager $signatoryManager * * @throws SignatoryNotFoundException * @throws SignatureException */ private function confirmIncomingRequestSignature( IIncomingSignedRequest $signedRequest, ISignatoryManager $signatoryManager, int $ttlSignatory, ): void { $knownSignatory = null; try { $knownSignatory = $this->getStoredSignatory($signedRequest->getKeyId()); // refreshing ttl and compare with previous public key if ($ttlSignatory > 0 && $knownSignatory->getLastUpdated() < (time() - $ttlSignatory)) { $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest); $this->updateSignatoryMetadata($signatory); $knownSignatory->setMetadata($signatory->getMetadata() ?? []); } $signedRequest->setSignatory($knownSignatory); $signedRequest->verify(); } catch (InvalidKeyOriginException $e) { throw $e; // issue while requesting remote instance also means there is no 2nd try } catch (SignatoryNotFoundException) { // if no signatory in cache, we retrieve the one from the remote instance (using // $signatoryManager), check its validity with current signature and store it $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest); $signedRequest->setSignatory($signatory); $signedRequest->verify(); $this->storeSignatory($signatory); } catch (SignatureException) { // if public key (from cache) is not valid, we try to refresh it (based on SignatoryType) try { $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest); } catch (SignatoryNotFoundException $e) { $this->manageDeprecatedSignatory($knownSignatory); throw $e; } $signedRequest->setSignatory($signatory); try { $signedRequest->verify(); } catch (InvalidSignatureException $e) { $this->logger->debug('signature issue', ['signed' => $signedRequest, 'exception' => $e]); throw $e; } $this->storeSignatory($signatory); } } /** * @inheritDoc * * @param ISignatoryManager $signatoryManager * @param string $content body to be signed * @param string $method needed in the signature * @param string $uri needed in the signature * * @return IOutgoingSignedRequest * @throws IdentityNotFoundException * @throws SignatoryException * @throws SignatoryNotFoundException * @since 31.0.0 */ public function getOutgoingSignedRequest( ISignatoryManager $signatoryManager, string $content, string $method, string $uri, ): IOutgoingSignedRequest { $signedRequest = new OutgoingSignedRequest( $content, $signatoryManager, $this->extractIdentityFromUri($uri), $method, parse_url($uri, PHP_URL_PATH) ?? '/' ); $signedRequest->sign(); return $signedRequest; } /** * @inheritDoc * * @param ISignatoryManager $signatoryManager * @param array $payload original payload, will be used to sign and completed with new headers with * signature elements * @param string $method needed in the signature * @param string $uri needed in the signature * * @return array new payload to be sent, including original payload and signature elements in headers * @since 31.0.0 */ public function signOutgoingRequestIClientPayload( ISignatoryManager $signatoryManager, array $payload, string $method, string $uri, ): array { $signedRequest = $this->getOutgoingSignedRequest($signatoryManager, $payload['body'], $method, $uri); $payload['headers'] = array_merge($payload['headers'], $signedRequest->getHeaders()); return $payload; } /** * @inheritDoc * * @param string $host remote host * @param string $account linked account, should be used when multiple signature can exist for the same * host * * @return Signatory * @throws SignatoryNotFoundException if entry does not exist in local database * @since 31.0.0 */ public function getSignatory(string $host, string $account = ''): Signatory { return $this->mapper->getByHost($host, $account); } /** * @inheritDoc * * keyId is set using app config 'core/security.signature.identity' * * @param string $path * * @return string * @throws IdentityNotFoundException is identity is not set in app config * @since 31.0.0 */ public function generateKeyIdFromConfig(string $path): string { if (!$this->appConfig->hasKey('core', self::APPCONFIG_IDENTITY, true)) { throw new IdentityNotFoundException(self::APPCONFIG_IDENTITY . ' not set'); } $identity = trim($this->appConfig->getValueString('core', self::APPCONFIG_IDENTITY, lazy: true), '/'); return 'https://' . $identity . '/' . ltrim($path, '/'); } /** * @inheritDoc * * @param string $uri * * @return string * @throws IdentityNotFoundException if identity cannot be extracted * @since 31.0.0 */ public function extractIdentityFromUri(string $uri): string { return Signatory::extractIdentityFromUri($uri); } /** * get remote signatory using the ISignatoryManager * and confirm the validity of the keyId * * @param ISignatoryManager $signatoryManager * @param IIncomingSignedRequest $signedRequest * * @return Signatory * @throws InvalidKeyOriginException * @throws SignatoryNotFoundException * @see ISignatoryManager::getRemoteSignatory */ private function getSaneRemoteSignatory( ISignatoryManager $signatoryManager, IIncomingSignedRequest $signedRequest, ): Signatory { $signatory = $signatoryManager->getRemoteSignatory($signedRequest->getOrigin()); if ($signatory === null) { throw new SignatoryNotFoundException('empty result from getRemoteSignatory'); } try { if ($signatory->getKeyId() !== $signedRequest->getKeyId()) { throw new InvalidKeyOriginException('keyId from signatory not related to the one from request'); } } catch (SignatureElementNotFoundException) { throw new InvalidKeyOriginException('missing keyId'); } $signatory->setProviderId($signatoryManager->getProviderId()); return $signatory; } /** * @param string $keyId * * @return Signatory * @throws SignatoryNotFoundException */ private function getStoredSignatory(string $keyId): Signatory { return $this->mapper->getByKeyId($keyId); } /** * @param Signatory $signatory */ private function storeSignatory(Signatory $signatory): void { try { $this->insertSignatory($signatory); } catch (DBException $e) { if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { $this->logger->warning('exception while storing signature', ['exception' => $e]); throw $e; } try { $this->updateKnownSignatory($signatory); } catch (SignatoryNotFoundException $e) { $this->logger->warning('strange behavior, signatory not found ?', ['exception' => $e]); } } } /** * @param Signatory $signatory */ private function insertSignatory(Signatory $signatory): void { $time = time(); $signatory->setCreation($time); $signatory->setLastUpdated($time); $signatory->setMetadata($signatory->getMetadata() ?? []); // trigger insert on field metadata using current or default value $this->mapper->insert($signatory); } /** * @param Signatory $signatory * * @throws SignatoryNotFoundException * @throws SignatoryConflictException */ private function updateKnownSignatory(Signatory $signatory): void { $knownSignatory = $this->getStoredSignatory($signatory->getKeyId()); switch ($signatory->getType()) { case SignatoryType::FORGIVABLE: $this->deleteSignatory($knownSignatory->getKeyId()); $this->insertSignatory($signatory); return; case SignatoryType::REFRESHABLE: $this->updateSignatoryPublicKey($signatory); $this->updateSignatoryMetadata($signatory); break; case SignatoryType::TRUSTED: // TODO: send notice to admin throw new SignatoryConflictException(); case SignatoryType::STATIC: // TODO: send warning to admin throw new SignatoryConflictException(); } } /** * This is called when a remote signatory does not exist anymore * * @param Signatory|null $knownSignatory NULL is not known * * @throws SignatoryConflictException * @throws SignatoryNotFoundException */ private function manageDeprecatedSignatory(?Signatory $knownSignatory): void { switch ($knownSignatory?->getType()) { case null: // unknown in local database case SignatoryType::FORGIVABLE: // who cares ? throw new SignatoryNotFoundException(); // meaning we just return the correct exception case SignatoryType::REFRESHABLE: // TODO: send notice to admin throw new SignatoryConflictException(); // while it can be refreshed, it must exist case SignatoryType::TRUSTED: case SignatoryType::STATIC: // TODO: send warning to admin throw new SignatoryConflictException(); // no way. } } private function updateSignatoryPublicKey(Signatory $signatory): void { $this->mapper->updatePublicKey($signatory); } private function updateSignatoryMetadata(Signatory $signatory): void { $this->mapper->updateMetadata($signatory); } private function deleteSignatory(string $keyId): void { $this->mapper->deleteByKeyId($keyId); } }