0
0
Fork 0
mirror of https://github.com/nextcloud/server.git synced 2025-02-24 17:06:50 +00:00
nextcloud_server/lib/private/Security/Signature/Model/IncomingSignedRequest.php
Maxence Lange 15b72281df fix(signatory): details on interfaces
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
2024-12-04 09:30:55 -01:00

268 lines
7.7 KiB
PHP

<?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\Model;
use JsonSerializable;
use NCU\Security\Signature\Enum\DigestAlgorithm;
use NCU\Security\Signature\Enum\SignatureAlgorithm;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\InvalidSignatureException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\IIncomingSignedRequest;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\Signatory;
use OC\Security\Signature\SignatureManager;
use OCP\IRequest;
use ValueError;
/**
* @inheritDoc
*
* @see ISignatureManager for details on signature
* @since 31.0.0
*/
class IncomingSignedRequest extends SignedRequest implements
IIncomingSignedRequest,
JsonSerializable {
private string $origin = '';
/**
* @param string $body
* @param IRequest $request
* @param array $options
*
* @throws IncomingRequestException if incoming request is wrongly signed
* @throws SignatureException if signature is faulty
* @throws SignatureNotFoundException if signature is not implemented
*/
public function __construct(
string $body,
private readonly IRequest $request,
private readonly array $options = [],
) {
parent::__construct($body);
$this->verifyHeaders();
$this->extractSignatureHeader();
$this->reconstructSignatureData();
try {
// we set origin based on the keyId defined in the Signature header of the request
$this->setOrigin(Signatory::extractIdentityFromUri($this->getSigningElement('keyId')));
} catch (IdentityNotFoundException $e) {
throw new IncomingRequestException($e->getMessage());
}
}
/**
* confirm that:
*
* - date is available in the header and its value is less than 5 minutes old
* - content-length is available and is the same as the payload size
* - digest is available and fit the checksum of the payload
*
* @throws IncomingRequestException
* @throws SignatureNotFoundException
*/
private function verifyHeaders(): void {
if ($this->request->getHeader('Signature') === '') {
throw new SignatureNotFoundException('missing Signature in header');
}
// confirm presence of date, content-length, digest and Signature
$date = $this->request->getHeader('date');
if ($date === '') {
throw new IncomingRequestException('missing date in header');
}
$contentLength = $this->request->getHeader('content-length');
if ($contentLength === '') {
throw new IncomingRequestException('missing content-length in header');
}
$digest = $this->request->getHeader('digest');
if ($digest === '') {
throw new IncomingRequestException('missing digest in header');
}
// confirm date
try {
$dTime = new \DateTime($date);
$requestTime = $dTime->getTimestamp();
} catch (\Exception) {
throw new IncomingRequestException('datetime exception');
}
if ($requestTime < (time() - ($this->options['ttl'] ?? SignatureManager::DATE_TTL))) {
throw new IncomingRequestException('object is too old');
}
// confirm validity of content-length
if (strlen($this->getBody()) !== (int)$contentLength) {
throw new IncomingRequestException('inexact content-length in header');
}
// confirm digest value, based on body
[$algo, ] = explode('=', $digest);
try {
$this->setDigestAlgorithm(DigestAlgorithm::from($algo));
} catch (ValueError) {
throw new IncomingRequestException('unknown digest algorithm');
}
if ($digest !== $this->getDigest()) {
throw new IncomingRequestException('invalid value for digest in header');
}
}
/**
* extract data from the header entry 'Signature' and convert its content from string to an array
* also confirm that it contains the minimum mandatory information
*
* @throws IncomingRequestException
*/
private function extractSignatureHeader(): void {
$details = [];
foreach (explode(',', $this->request->getHeader('Signature')) as $entry) {
if ($entry === '' || !strpos($entry, '=')) {
continue;
}
[$k, $v] = explode('=', $entry, 2);
preg_match('/^"([^"]+)"$/', $v, $var);
if ($var[0] !== '') {
$v = trim($var[0], '"');
}
$details[$k] = $v;
}
$this->setSigningElements($details);
try {
// confirm keys are in the Signature header
$this->getSigningElement('keyId');
$this->getSigningElement('headers');
$this->setSignature($this->getSigningElement('signature'));
} catch (SignatureElementNotFoundException $e) {
throw new IncomingRequestException($e->getMessage());
}
}
/**
* reconstruct signature data based on signature's metadata stored in the 'Signature' header
*
* @throws SignatureException
* @throws SignatureElementNotFoundException
*/
private function reconstructSignatureData(): void {
$usedHeaders = explode(' ', $this->getSigningElement('headers'));
$neededHeaders = array_merge(['date', 'host', 'content-length', 'digest'],
array_keys($this->options['extraSignatureHeaders'] ?? []));
$missingHeaders = array_diff($neededHeaders, $usedHeaders);
if ($missingHeaders !== []) {
throw new SignatureException('missing entries in Signature.headers: ' . json_encode($missingHeaders));
}
$estimated = ['(request-target): ' . strtolower($this->request->getMethod()) . ' ' . $this->request->getRequestUri()];
foreach ($usedHeaders as $key) {
if ($key === '(request-target)') {
continue;
}
$value = (strtolower($key) === 'host') ? $this->request->getServerHost() : $this->request->getHeader($key);
if ($value === '') {
throw new SignatureException('missing header ' . $key . ' in request');
}
$estimated[] = $key . ': ' . $value;
}
$this->setSignatureData($estimated);
}
/**
* @inheritDoc
*
* @return IRequest
* @since 31.0.0
*/
public function getRequest(): IRequest {
return $this->request;
}
/**
* set the hostname at the source of the request,
* based on the keyId defined in the signature header.
*
* @param string $origin
* @since 31.0.0
*/
private function setOrigin(string $origin): void {
$this->origin = $origin;
}
/**
* @inheritDoc
*
* @return string
* @throws IncomingRequestException
* @since 31.0.0
*/
public function getOrigin(): string {
if ($this->origin === '') {
throw new IncomingRequestException('empty origin');
}
return $this->origin;
}
/**
* returns the keyId extracted from the signature headers.
* keyId is a mandatory entry in the headers of a signed request.
*
* @return string
* @throws SignatureElementNotFoundException
* @since 31.0.0
*/
public function getKeyId(): string {
return $this->getSigningElement('keyId');
}
/**
* @inheritDoc
*
* @throws SignatureException
* @throws SignatoryNotFoundException
* @since 31.0.0
*/
public function verify(): void {
$publicKey = $this->getSignatory()->getPublicKey();
if ($publicKey === '') {
throw new SignatoryNotFoundException('empty public key');
}
$algorithm = SignatureAlgorithm::tryFrom($this->getSigningElement('algorithm')) ?? SignatureAlgorithm::RSA_SHA256;
if (openssl_verify(
implode("\n", $this->getSignatureData()),
base64_decode($this->getSignature()),
$publicKey,
$algorithm->value
) !== 1) {
throw new InvalidSignatureException('signature issue');
}
}
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'options' => $this->options,
'origin' => $this->origin,
]
);
}
}