aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Security/Signature/Model
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Security/Signature/Model')
-rw-r--r--lib/private/Security/Signature/Model/IncomingSignedRequest.php268
-rw-r--r--lib/private/Security/Signature/Model/OutgoingSignedRequest.php229
-rw-r--r--lib/private/Security/Signature/Model/SignedRequest.php216
3 files changed, 713 insertions, 0 deletions
diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php
new file mode 100644
index 00000000000..0f7dc7cb771
--- /dev/null
+++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php
@@ -0,0 +1,268 @@
+<?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,
+ ]
+ );
+ }
+}
diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
new file mode 100644
index 00000000000..dbfac3bfd34
--- /dev/null
+++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
@@ -0,0 +1,229 @@
+<?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\SignatoryException;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\IOutgoingSignedRequest;
+use NCU\Security\Signature\ISignatoryManager;
+use NCU\Security\Signature\ISignatureManager;
+use OC\Security\Signature\SignatureManager;
+
+/**
+ * extends ISignedRequest to add info requested at the generation of the signature
+ *
+ * @see ISignatureManager for details on signature
+ * @since 31.0.0
+ */
+class OutgoingSignedRequest extends SignedRequest implements
+ IOutgoingSignedRequest,
+ JsonSerializable {
+ private string $host = '';
+ private array $headers = [];
+ /** @var list<string> $headerList */
+ private array $headerList = [];
+ private SignatureAlgorithm $algorithm;
+ public function __construct(
+ string $body,
+ ISignatoryManager $signatoryManager,
+ private readonly string $identity,
+ private readonly string $method,
+ private readonly string $path,
+ ) {
+ parent::__construct($body);
+
+ $options = $signatoryManager->getOptions();
+ $this->setHost($identity)
+ ->setAlgorithm($options['algorithm'] ?? SignatureAlgorithm::RSA_SHA256)
+ ->setSignatory($signatoryManager->getLocalSignatory())
+ ->setDigestAlgorithm($options['digestAlgorithm'] ?? DigestAlgorithm::SHA256);
+
+ $headers = array_merge([
+ '(request-target)' => strtolower($method) . ' ' . $path,
+ 'content-length' => strlen($this->getBody()),
+ 'date' => gmdate($options['dateHeader'] ?? SignatureManager::DATE_HEADER),
+ 'digest' => $this->getDigest(),
+ 'host' => $this->getHost()
+ ], $options['extraSignatureHeaders'] ?? []);
+
+ $signing = $headerList = [];
+ foreach ($headers as $element => $value) {
+ $signing[] = $element . ': ' . $value;
+ $headerList[] = $element;
+ if ($element !== '(request-target)') {
+ $this->addHeader($element, $value);
+ }
+ }
+
+ $this->setHeaderList($headerList)
+ ->setSignatureData($signing);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $host
+ * @return $this
+ * @since 31.0.0
+ */
+ public function setHost(string $host): self {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getHost(): string {
+ return $this->host;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $key
+ * @param string|int|float $value
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function addHeader(string $key, string|int|float $value): self {
+ $this->headers[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return array
+ * @since 31.0.0
+ */
+ public function getHeaders(): array {
+ return $this->headers;
+ }
+
+ /**
+ * set the ordered list of used headers in the Signature
+ *
+ * @param list<string> $list
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function setHeaderList(array $list): self {
+ $this->headerList = $list;
+ return $this;
+ }
+
+ /**
+ * returns ordered list of used headers in the Signature
+ *
+ * @return list<string>
+ * @since 31.0.0
+ */
+ public function getHeaderList(): array {
+ return $this->headerList;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param SignatureAlgorithm $algorithm
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function setAlgorithm(SignatureAlgorithm $algorithm): self {
+ $this->algorithm = $algorithm;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return SignatureAlgorithm
+ * @since 31.0.0
+ */
+ public function getAlgorithm(): SignatureAlgorithm {
+ return $this->algorithm;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return self
+ * @throws SignatoryException
+ * @throws SignatoryNotFoundException
+ * @since 31.0.0
+ */
+ public function sign(): self {
+ $privateKey = $this->getSignatory()->getPrivateKey();
+ if ($privateKey === '') {
+ throw new SignatoryException('empty private key');
+ }
+
+ openssl_sign(
+ implode("\n", $this->getSignatureData()),
+ $signed,
+ $privateKey,
+ $this->getAlgorithm()->value
+ );
+
+ $this->setSignature(base64_encode($signed));
+ $this->setSigningElements(
+ [
+ 'keyId="' . $this->getSignatory()->getKeyId() . '"',
+ 'algorithm="' . $this->getAlgorithm()->value . '"',
+ 'headers="' . implode(' ', $this->getHeaderList()) . '"',
+ 'signature="' . $this->getSignature() . '"'
+ ]
+ );
+ $this->addHeader('Signature', implode(',', $this->getSigningElements()));
+
+ return $this;
+ }
+
+ /**
+ * @param string $clear
+ * @param string $privateKey
+ * @param SignatureAlgorithm $algorithm
+ *
+ * @return string
+ * @throws SignatoryException
+ */
+ private function signString(string $clear, string $privateKey, SignatureAlgorithm $algorithm): string {
+ if ($privateKey === '') {
+ throw new SignatoryException('empty private key');
+ }
+
+ openssl_sign($clear, $signed, $privateKey, $algorithm->value);
+
+ return base64_encode($signed);
+ }
+
+ public function jsonSerialize(): array {
+ return array_merge(
+ parent::jsonSerialize(),
+ [
+ 'host' => $this->host,
+ 'headers' => $this->headers,
+ 'algorithm' => $this->algorithm->value,
+ 'method' => $this->method,
+ 'identity' => $this->identity,
+ 'path' => $this->path,
+ ]
+ );
+ }
+}
diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php
new file mode 100644
index 00000000000..12a43f32bcc
--- /dev/null
+++ b/lib/private/Security/Signature/Model/SignedRequest.php
@@ -0,0 +1,216 @@
+<?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\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
+use NCU\Security\Signature\ISignedRequest;
+use NCU\Security\Signature\Model\Signatory;
+
+/**
+ * @inheritDoc
+ *
+ * @since 31.0.0
+ */
+class SignedRequest implements ISignedRequest, JsonSerializable {
+ private string $digest = '';
+ private DigestAlgorithm $digestAlgorithm = DigestAlgorithm::SHA256;
+ private array $signingElements = [];
+ private array $signatureData = [];
+ private string $signature = '';
+ private ?Signatory $signatory = null;
+
+ public function __construct(
+ private readonly string $body,
+ ) {
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getBody(): string {
+ return $this->body;
+ }
+
+ /**
+ * set algorithm used to generate digest
+ *
+ * @param DigestAlgorithm $algorithm
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ protected function setDigestAlgorithm(DigestAlgorithm $algorithm): self {
+ $this->digestAlgorithm = $algorithm;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return DigestAlgorithm
+ * @since 31.0.0
+ */
+ public function getDigestAlgorithm(): DigestAlgorithm {
+ return $this->digestAlgorithm;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getDigest(): string {
+ if ($this->digest === '') {
+ $this->digest = $this->digestAlgorithm->value . '='
+ . base64_encode(hash($this->digestAlgorithm->getHashingAlgorithm(), $this->body, true));
+ }
+ return $this->digest;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param array $elements
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function setSigningElements(array $elements): self {
+ $this->signingElements = $elements;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return array
+ * @since 31.0.0
+ */
+ public function getSigningElements(): array {
+ return $this->signingElements;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ * @throws SignatureElementNotFoundException
+ * @since 31.0.0
+ *
+ */
+ public function getSigningElement(string $key): string { // getSignatureDetail / getSignatureEntry() ?
+ if (!array_key_exists($key, $this->signingElements)) {
+ throw new SignatureElementNotFoundException('missing element ' . $key . ' in Signature header');
+ }
+
+ return $this->signingElements[$key];
+ }
+
+ /**
+ * store data used to generate signature
+ *
+ * @param array $data
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ protected function setSignatureData(array $data): self {
+ $this->signatureData = $data;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return array
+ * @since 31.0.0
+ */
+ public function getSignatureData(): array {
+ return $this->signatureData;
+ }
+
+ /**
+ * set the signed version of the signature
+ *
+ * @param string $signature
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ protected function setSignature(string $signature): self {
+ $this->signature = $signature;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getSignature(): string {
+ return $this->signature;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param Signatory $signatory
+ * @return self
+ * @since 31.0.0
+ */
+ public function setSignatory(Signatory $signatory): self {
+ $this->signatory = $signatory;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return Signatory
+ * @throws SignatoryNotFoundException
+ * @since 31.0.0
+ */
+ public function getSignatory(): Signatory {
+ if ($this->signatory === null) {
+ throw new SignatoryNotFoundException();
+ }
+
+ return $this->signatory;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return bool
+ * @since 31.0.0
+ */
+ public function hasSignatory(): bool {
+ return ($this->signatory !== null);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'body' => $this->body,
+ 'digest' => $this->getDigest(),
+ 'digestAlgorithm' => $this->getDigestAlgorithm()->value,
+ 'signingElements' => $this->signingElements,
+ 'signatureData' => $this->signatureData,
+ 'signature' => $this->signature,
+ 'signatory' => $this->signatory ?? false,
+ ];
+ }
+}