You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
nextcloud-server/lib/private/Security/Signature/SignatureManager.php

828 lines
26 KiB

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Signature;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\InvalidKeyOriginException;
use NCU\Security\Signature\Exceptions\InvalidSignatureException;
use NCU\Security\Signature\Exceptions\SignatoryConflictException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\ISignatoryManager;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\IOutgoingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\SignatoryType;
use NCU\Security\Signature\SignatureAlgorithm;
use OC\Security\Signature\Model\IncomingSignedRequest;
use OC\Security\Signature\Model\OutgoingSignedRequest;
use OC\Security\Signature\Model\Signatory;
use OCP\DB\Exception as DBException;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
/**
* ISignatureManager is a service integrated to core that provide tools
* to set/get authenticity of/from outgoing/incoming request.
*
* Quick description of the signature, added to the headers
* {
* "(request-target)": "post /path",
* "content-length": 385,
* "date": "Mon, 08 Jul 2024 14:16:20 GMT",
* "digest": "SHA-256=U7gNVUQiixe5BRbp4Tg0xCZMTcSWXXUZI2\\/xtHM40S0=",
* "host": "hostname.of.the.recipient",
* "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"ras-sha256\",headers=\"content-length
* date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
* }
*
* 'content-length' is the total length of the data/content
* 'date' is the datetime the request have been initiated
* 'digest' is a checksum of the data/content
* 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on
* incoming request)
* 'Signature' contains the signature generated using the private key, and metadata:
* - 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom
* discovery
* - 'algorithm' define the algorithm used to generate signature
* - 'headers' contains a list of element used during the generation of the signature
* - 'signature' is the encrypted string, using local private key, of an array containing elements
* listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
* to ensure authenticity override protection.
*
* @since 31.0.0
*/
class SignatureManager implements ISignatureManager {
private const DATE_HEADER = 'D, d M Y H:i:s T';
private const DATE_TTL = 300;
private const SIGNATORY_TTL = 86400 * 3;
private const TABLE_SIGNATORIES = 'sec_signatory';
private const BODY_MAXSIZE = 50000; // max size of the payload of the request
public const APPCONFIG_IDENTITY = 'security.signature.identity';
public function __construct(
private readonly IRequest $request,
private readonly IDBConnection $connection,
private readonly IAppConfig $appConfig,
private readonly LoggerInterface $logger,
) {
}
/**
* @inheritDoc
*
* @param ISignatoryManager $signatoryManager used to get details about remote instance
* @param string|null $body if NULL, body will be extracted from php://input
*
* @return IIncomingSignedRequest
* @throws IncomingRequestException if anything looks wrong with the incoming request
* @throws SignatureNotFoundException if incoming request is not signed
* @throws SignatureException if signature could not be confirmed
* @since 31.0.0
*/
public function getIncomingSignedRequest(
ISignatoryManager $signatoryManager,
?string $body = null,
): IIncomingSignedRequest {
$body = $body ?? file_get_contents('php://input');
if (strlen($body) > self::BODY_MAXSIZE) {
throw new IncomingRequestException('content of request is too big');
}
$signedRequest = new IncomingSignedRequest($body);
$signedRequest->setRequest($this->request);
$options = $signatoryManager->getOptions();
try {
$this->verifyIncomingRequestTime($signedRequest, $options['ttl'] ?? self::DATE_TTL);
$this->verifyIncomingRequestContent($signedRequest);
$this->prepIncomingSignatureHeader($signedRequest);
$this->verifyIncomingSignatureHeader($signedRequest);
$this->prepEstimatedSignature($signedRequest, $options['extraSignatureHeaders'] ?? []);
$this->verifyIncomingRequestSignature(
$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;
}
/**
* @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
* @since 31.0.0
*/
public function getOutgoingSignedRequest(
ISignatoryManager $signatoryManager,
string $content,
string $method,
string $uri,
): IOutgoingSignedRequest {
$signedRequest = new OutgoingSignedRequest($content);
$options = $signatoryManager->getOptions();
$signedRequest->setHost($this->getHostFromUri($uri))
->setAlgorithm($options['algorithm'] ?? 'sha256')
->setSignatory($signatoryManager->getLocalSignatory());
$this->setOutgoingSignatureHeader(
$signedRequest,
strtolower($method),
parse_url($uri, PHP_URL_PATH) ?? '/',
$options['dateHeader'] ?? self::DATE_HEADER
);
$this->setOutgoingClearSignature($signedRequest);
$this->setOutgoingSignedSignature($signedRequest);
$this->signingOutgoingRequest($signedRequest);
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 ISignatory
* @throws SignatoryNotFoundException if entry does not exist in local database
* @since 31.0.0
*/
public function searchSignatory(string $host, string $account = ''): ISignatory {
$qb = $this->connection->getQueryBuilder();
$qb->select(
'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type',
'status', 'creation', 'last_updated'
);
$qb->from(self::TABLE_SIGNATORIES);
$qb->where($qb->expr()->eq('host', $qb->createNamedParameter($host)));
$qb->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account)));
$result = $qb->executeQuery();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new SignatoryNotFoundException('no signatory found');
}
$signature = new Signatory($row['key_id'], $row['public_key']);
return $signature->importFromDatabase($row);
}
/**
* @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 {
$identity = parse_url($uri, PHP_URL_HOST);
$port = parse_url($uri, PHP_URL_PORT);
if ($identity === null || $identity === false) {
throw new IdentityNotFoundException('cannot extract identity from ' . $uri);
}
if ($port !== null && $port !== false) {
$identity .= ':' . $port;
}
return $identity;
}
/**
* using the requested 'date' entry from header to confirm request is not older than ttl
*
* @param IIncomingSignedRequest $signedRequest
* @param int $ttl
*
* @throws IncomingRequestException
* @throws SignatureNotFoundException
*/
private function verifyIncomingRequestTime(IIncomingSignedRequest $signedRequest, int $ttl): void {
$request = $signedRequest->getRequest();
$date = $request->getHeader('date');
if ($date === '') {
throw new SignatureNotFoundException('missing date in header');
}
try {
$dTime = new \DateTime($date);
$signedRequest->setTime($dTime->getTimestamp());
} catch (\Exception $e) {
$this->logger->warning(
'datetime exception', ['exception' => $e, 'header' => $request->getHeader('date')]
);
throw new IncomingRequestException('datetime exception');
}
if ($signedRequest->getTime() < (time() - $ttl)) {
throw new IncomingRequestException('object is too old');
}
}
/**
* confirm the values of 'content-length' and 'digest' from header
* is related to request content
*
* @param IIncomingSignedRequest $signedRequest
*
* @throws IncomingRequestException
* @throws SignatureNotFoundException
*/
private function verifyIncomingRequestContent(IIncomingSignedRequest $signedRequest): void {
$request = $signedRequest->getRequest();
$contentLength = $request->getHeader('content-length');
if ($contentLength === '') {
throw new SignatureNotFoundException('missing content-length in header');
}
if (strlen($signedRequest->getBody()) !== (int)$request->getHeader('content-length')) {
throw new IncomingRequestException(
'inexact content-length in header: ' . strlen($signedRequest->getBody()) . ' vs '
. (int)$request->getHeader('content-length')
);
}
$digest = $request->getHeader('digest');
if ($digest === '') {
throw new SignatureNotFoundException('missing digest in header');
}
if ($digest !== $signedRequest->getDigest()) {
throw new IncomingRequestException('invalid value for digest in header');
}
}
/**
* preparing a clear version of the signature based on list of metadata from the
* Signature entry in header
*
* @param IIncomingSignedRequest $signedRequest
*
* @throws SignatureNotFoundException
*/
private function prepIncomingSignatureHeader(IIncomingSignedRequest $signedRequest): void {
$sign = [];
$request = $signedRequest->getRequest();
$signature = $request->getHeader('Signature');
if ($signature === '') {
throw new SignatureNotFoundException('missing Signature in header');
}
foreach (explode(',', $signature) as $entry) {
if ($entry === '' || !strpos($entry, '=')) {
continue;
}
[$k, $v] = explode('=', $entry, 2);
preg_match('/"([^"]+)"/', $v, $var);
if ($var[0] !== '') {
$v = trim($var[0], '"');
}
$sign[$k] = $v;
}
$signedRequest->setSignatureHeader($sign);
}
/**
* @param IIncomingSignedRequest $signedRequest
*
* @throws IncomingRequestException
* @throws InvalidKeyOriginException
*/
private function verifyIncomingSignatureHeader(IIncomingSignedRequest $signedRequest): void {
$data = $signedRequest->getSignatureHeader();
if (!array_key_exists('keyId', $data) || !array_key_exists('headers', $data)
|| !array_key_exists('signature', $data)) {
throw new IncomingRequestException('missing keys in signature headers: ' . json_encode($data));
}
try {
$signedRequest->setOrigin($this->getHostFromUri($data['keyId']));
} catch (\Exception) {
throw new InvalidKeyOriginException('cannot retrieve origin from ' . $data['keyId']);
}
$signedRequest->setSignedSignature($data['signature']);
}
/**
* @param IIncomingSignedRequest $signedRequest
* @param array $extraSignatureHeaders
*
* @throws IncomingRequestException
*/
private function prepEstimatedSignature(
IIncomingSignedRequest $signedRequest,
array $extraSignatureHeaders = [],
): void {
$request = $signedRequest->getRequest();
$headers = explode(' ', $signedRequest->getSignatureHeader()['headers'] ?? []);
$enforceHeaders = array_merge(
['date', 'host', 'content-length', 'digest'],
$extraSignatureHeaders
);
$missingHeaders = array_diff($enforceHeaders, $headers);
if ($missingHeaders !== []) {
throw new IncomingRequestException(
'missing elements in headers: ' . json_encode($missingHeaders)
);
}
$target = strtolower($request->getMethod()) . ' ' . $request->getRequestUri();
$estimated = ['(request-target): ' . $target];
foreach ($headers as $key) {
$value = $request->getHeader($key);
if (strtolower($key) === 'host') {
$value = $request->getServerHost();
}
if ($value === '') {
throw new IncomingRequestException('empty elements in header ' . $key);
}
$estimated[] = $key . ': ' . $value;
}
$signedRequest->setEstimatedSignature(implode("\n", $estimated));
}
/**
* @param IIncomingSignedRequest $signedRequest
* @param ISignatoryManager $signatoryManager
*
* @throws SignatoryNotFoundException
* @throws SignatureException
*/
private function verifyIncomingRequestSignature(
IIncomingSignedRequest $signedRequest,
ISignatoryManager $signatoryManager,
int $ttlSignatory,
): void {
$knownSignatory = null;
try {
$knownSignatory = $this->getStoredSignatory($signedRequest->getKeyId());
if ($ttlSignatory > 0 && $knownSignatory->getLastUpdated() < (time() - $ttlSignatory)) {
$signatory = $this->getSafeRemoteSignatory($signatoryManager, $signedRequest);
$this->updateSignatoryMetadata($signatory);
$knownSignatory->setMetadata($signatory->getMetadata());
}
$signedRequest->setSignatory($knownSignatory);
$this->verifySignedRequest($signedRequest);
} catch (InvalidKeyOriginException $e) {
throw $e; // issue while requesting remote instance also means there is no 2nd try
} catch (SignatoryNotFoundException|SignatureException) {
try {
$signatory = $this->getSafeRemoteSignatory($signatoryManager, $signedRequest);
} catch (SignatoryNotFoundException $e) {
$this->manageDeprecatedSignatory($knownSignatory);
throw $e;
}
$signedRequest->setSignatory($signatory);
$this->storeSignatory($signatory);
$this->verifySignedRequest($signedRequest);
}
}
/**
* @param ISignatoryManager $signatoryManager
* @param IIncomingSignedRequest $signedRequest
*
* @return ISignatory
* @throws InvalidKeyOriginException
* @throws SignatoryNotFoundException
*/
private function getSafeRemoteSignatory(
ISignatoryManager $signatoryManager,
IIncomingSignedRequest $signedRequest,
): ISignatory {
$signatory = $signatoryManager->getRemoteSignatory($signedRequest);
if ($signatory === null) {
throw new SignatoryNotFoundException('empty result from getRemoteSignatory');
}
if ($signatory->getKeyId() !== $signedRequest->getKeyId()) {
throw new InvalidKeyOriginException('keyId from signatory not related to the one from request');
}
return $signatory->setProviderId($signatoryManager->getProviderId());
}
private function setOutgoingSignatureHeader(
IOutgoingSignedRequest $signedRequest,
string $method,
string $path,
string $dateHeader,
): void {
$header = [
'(request-target)' => $method . ' ' . $path,
'content-length' => strlen($signedRequest->getBody()),
'date' => gmdate($dateHeader),
'digest' => $signedRequest->getDigest(),
'host' => $signedRequest->getHost()
];
$signedRequest->setSignatureHeader($header);
}
/**
* @param IOutgoingSignedRequest $signedRequest
*/
private function setOutgoingClearSignature(IOutgoingSignedRequest $signedRequest): void {
$signing = [];
$header = $signedRequest->getSignatureHeader();
foreach (array_keys($header) as $element) {
$value = $header[$element];
$signing[] = $element . ': ' . $value;
if ($element !== '(request-target)') {
$signedRequest->addHeader($element, $value);
}
}
$signedRequest->setClearSignature(implode("\n", $signing));
}
private function setOutgoingSignedSignature(IOutgoingSignedRequest $signedRequest): void {
$clear = $signedRequest->getClearSignature();
$signed = $this->signString(
$clear, $signedRequest->getSignatory()->getPrivateKey(), $signedRequest->getAlgorithm()
);
$signedRequest->setSignedSignature($signed);
}
private function signingOutgoingRequest(IOutgoingSignedRequest $signedRequest): void {
$signatureHeader = $signedRequest->getSignatureHeader();
$headers = array_diff(array_keys($signatureHeader), ['(request-target)']);
$signatory = $signedRequest->getSignatory();
$signatureElements = [
'keyId="' . $signatory->getKeyId() . '"',
'algorithm="' . $this->getChosenEncryption($signedRequest->getAlgorithm()) . '"',
'headers="' . implode(' ', $headers) . '"',
'signature="' . $signedRequest->getSignedSignature() . '"'
];
$signedRequest->addHeader('Signature', implode(',', $signatureElements));
}
/**
* @param IIncomingSignedRequest $signedRequest
*
* @return void
* @throws SignatureException
* @throws SignatoryNotFoundException
*/
private function verifySignedRequest(IIncomingSignedRequest $signedRequest): void {
$publicKey = $signedRequest->getSignatory()->getPublicKey();
if ($publicKey === '') {
throw new SignatoryNotFoundException('empty public key');
}
try {
$this->verifyString(
$signedRequest->getEstimatedSignature(),
$signedRequest->getSignedSignature(),
$publicKey,
$this->getUsedEncryption($signedRequest)
);
} catch (InvalidSignatureException $e) {
$this->logger->debug('signature issue', ['signed' => $signedRequest, 'exception' => $e]);
throw $e;
}
}
private function getUsedEncryption(IIncomingSignedRequest $signedRequest): SignatureAlgorithm {
$data = $signedRequest->getSignatureHeader();
return match ($data['algorithm']) {
'rsa-sha512' => SignatureAlgorithm::SHA512,
default => SignatureAlgorithm::SHA256,
};
}
private function getChosenEncryption(string $algorithm): string {
return match ($algorithm) {
'sha512' => 'ras-sha512',
default => 'ras-sha256',
};
}
public function getOpenSSLAlgo(string $algorithm): int {
return match ($algorithm) {
'sha512' => OPENSSL_ALGO_SHA512,
default => OPENSSL_ALGO_SHA256,
};
}
/**
* @param string $clear
* @param string $privateKey
* @param string $algorithm
*
* @return string
* @throws SignatoryException
*/
private function signString(string $clear, string $privateKey, string $algorithm): string {
if ($privateKey === '') {
throw new SignatoryException('empty private key');
}
openssl_sign($clear, $signed, $privateKey, $this->getOpenSSLAlgo($algorithm));
return base64_encode($signed);
}
/**
* @param string $clear
* @param string $encoded
* @param string $publicKey
* @param SignatureAlgorithm $algo
*
* @return void
* @throws InvalidSignatureException
*/
private function verifyString(
string $clear,
string $encoded,
string $publicKey,
SignatureAlgorithm $algo = SignatureAlgorithm::SHA256,
): void {
$signed = base64_decode($encoded);
if (openssl_verify($clear, $signed, $publicKey, $algo->value) !== 1) {
throw new InvalidSignatureException('signature issue');
}
}
/**
* @param string $keyId
*
* @return ISignatory
* @throws SignatoryNotFoundException
*/
private function getStoredSignatory(string $keyId): ISignatory {
$qb = $this->connection->getQueryBuilder();
$qb->select(
'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type',
'status', 'creation', 'last_updated'
);
$qb->from(self::TABLE_SIGNATORIES);
$qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId))));
$result = $qb->executeQuery();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new SignatoryNotFoundException('no signatory found in local');
}
$signature = new Signatory($row['key_id'], $row['public_key']);
$signature->importFromDatabase($row);
return $signature;
}
/**
* @param ISignatory $signatory
*/
private function storeSignatory(ISignatory $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]);
}
}
}
private function insertSignatory(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->insert(self::TABLE_SIGNATORIES)
->setValue('provider_id', $qb->createNamedParameter($signatory->getProviderId()))
->setValue('host', $qb->createNamedParameter($this->getHostFromUri($signatory->getKeyId())))
->setValue('account', $qb->createNamedParameter($signatory->getAccount()))
->setValue('key_id', $qb->createNamedParameter($signatory->getKeyId()))
->setValue('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
->setValue('public_key', $qb->createNamedParameter($signatory->getPublicKey()))
->setValue('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata())))
->setValue('type', $qb->createNamedParameter($signatory->getType()->value))
->setValue('status', $qb->createNamedParameter($signatory->getStatus()->value))
->setValue('creation', $qb->createNamedParameter(time()))
->setValue('last_updated', $qb->createNamedParameter(time()));
$qb->executeStatement();
}
/**
* @param ISignatory $signatory
*
* @throws SignatoryNotFoundException
* @throws SignatoryConflictException
*/
private function updateKnownSignatory(ISignatory $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();
break;
case SignatoryType::STATIC:
// TODO: send warning to admin
throw new SignatoryConflictException();
break;
}
}
/**
* This is called when a remote signatory does not exist anymore
*
* @param ISignatory|null $knownSignatory NULL is not known
*
* @throws SignatoryConflictException
* @throws SignatoryNotFoundException
*/
private function manageDeprecatedSignatory(?ISignatory $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();
case SignatoryType::TRUSTED:
case SignatoryType::STATIC:
// TODO: send warning to admin
throw new SignatoryConflictException();
}
}
private function updateSignatoryPublicKey(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->update(self::TABLE_SIGNATORIES)
->set('signatory', $qb->createNamedParameter($signatory->getPublicKey()))
->set('last_updated', $qb->createNamedParameter(time()));
$qb->where(
$qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
);
$qb->executeStatement();
}
private function updateSignatoryMetadata(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->update(self::TABLE_SIGNATORIES)
->set('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata())))
->set('last_updated', $qb->createNamedParameter(time()));
$qb->where(
$qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
);
$qb->executeStatement();
}
private function deleteSignatory(string $keyId): void {
$qb = $this->connection->getQueryBuilder();
$qb->delete(self::TABLE_SIGNATORIES)
->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId))));
$qb->executeStatement();
}
/**
* @param string $uri
*
* @return string
* @throws InvalidKeyOriginException
*/
private function getHostFromUri(string $uri): string {
$host = parse_url($uri, PHP_URL_HOST);
$port = parse_url($uri, PHP_URL_PORT);
if ($port !== null && $port !== false) {
$host .= ':' . $port;
}
if (is_string($host) && $host !== '') {
return $host;
}
throw new \Exception('invalid/empty uri');
}
private function hashKeyId(string $keyId): string {
return hash('sha256', $keyId);
}
}