diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index 32068efe3eb16..fb13b7c0f9356 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -183,7 +183,9 @@ public function import(array $data): static { $this->setResourceTypes($resources); // import details about the remote request signing public key, if available - $signatory = new Signatory($data['publicKey']['keyId'] ?? '', $data['publicKey']['publicKeyPem'] ?? ''); + $signatory = new Signatory(); + $signatory->setKeyId($data['publicKey']['keyId'] ?? ''); + $signatory->setPublicKey($data['publicKey']['publicKeyPem'] ?? ''); if ($signatory->getKeyId() !== '' && $signatory->getPublicKey() !== '') { $this->setSignatory($signatory); } diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php index 909952a6b3738..3e5202e61656e 100644 --- a/lib/private/OCM/OCMSignatoryManager.php +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -9,7 +9,9 @@ namespace OC\OCM; +use NCU\Security\Signature\Enum\DigestAlgorithm; use NCU\Security\Signature\Enum\SignatoryType; +use NCU\Security\Signature\Enum\SignatureAlgorithm; use NCU\Security\Signature\Exceptions\IdentityNotFoundException; use NCU\Security\Signature\ISignatoryManager; use NCU\Security\Signature\ISignatureManager; @@ -61,7 +63,15 @@ public function getProviderId(): string { * @since 31.0.0 */ public function getOptions(): array { - return []; + return [ + 'algorithm' => SignatureAlgorithm::RSA_SHA512, + 'digestAlgorithm' => DigestAlgorithm::SHA512, + 'extraSignatureHeaders' => [], + 'ttl' => 300, + 'dateHeader' => 'D, d M Y H:i:s T', + 'ttlSignatory' => 86400 * 3, + 'bodyMaxSize' => 50000, + ]; } /** @@ -92,7 +102,12 @@ public function getLocalSignatory(): Signatory { } $keyPair = $this->identityProofManager->getAppKey('core', 'ocm_external'); - return new Signatory($keyId, $keyPair->getPublic(), $keyPair->getPrivate(), local: true); + $signatory = new Signatory(true); + $signatory->setKeyId($keyId); + $signatory->setPublicKey($keyPair->getPublic()); + $signatory->setPrivateKey($keyPair->getPrivate()); + return $signatory; + } /** diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php index fae8b897d5b77..2f4590b53d40f 100644 --- a/lib/private/Security/Signature/Model/IncomingSignedRequest.php +++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php @@ -36,8 +36,13 @@ class IncomingSignedRequest extends SignedRequest implements private string $origin = ''; /** + * @param string $body + * @param IRequest $request + * @param array $options + * * @throws IncomingRequestException if incoming request is wrongly signed - * @throws SignatureNotFoundException if signature is not fully implemented + * @throws SignatureException if signature is faulty + * @throws SignatureNotFoundException if signature is not implemented */ public function __construct( string $body, @@ -45,8 +50,9 @@ public function __construct( private readonly array $options = [], ) { parent::__construct($body); - $this->verifyHeadersFromRequest(); - $this->extractSignatureHeaderFromRequest(); + $this->verifyHeaders(); + $this->extractSignatureHeader(); + $this->reconstructSignatureData(); } /** @@ -59,7 +65,7 @@ public function __construct( * @throws IncomingRequestException * @throws SignatureNotFoundException */ - private function verifyHeadersFromRequest(): void { + private function verifyHeaders(): void { // confirm presence of date, content-length, digest and Signature $date = $this->getRequest()->getHeader('date'); if ($date === '') { @@ -105,7 +111,7 @@ private function verifyHeadersFromRequest(): void { * * @throws IncomingRequestException */ - private function extractSignatureHeaderFromRequest(): void { + private function extractSignatureHeader(): void { $details = []; foreach (explode(',', $this->getRequest()->getHeader('Signature')) as $entry) { if ($entry === '' || !strpos($entry, '=')) { @@ -132,6 +138,36 @@ private function extractSignatureHeaderFromRequest(): void { } } + /** + * @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 * @@ -214,7 +250,7 @@ public function verify(): void { throw new SignatoryNotFoundException('empty public key'); } - $algorithm = SignatureAlgorithm::tryFrom($this->getSigningElement('algorithm')) ?? SignatureAlgorithm::SHA256; + $algorithm = SignatureAlgorithm::tryFrom($this->getSigningElement('algorithm')) ?? SignatureAlgorithm::RSA_SHA256; if (openssl_verify( implode("\n", $this->getSignatureData()), base64_decode($this->getSignature()), diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php index 8879821a029d3..dbfac3bfd34e1 100644 --- a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php +++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php @@ -9,6 +9,7 @@ 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; @@ -42,8 +43,9 @@ public function __construct( $options = $signatoryManager->getOptions(); $this->setHost($identity) - ->setAlgorithm(SignatureAlgorithm::from($options['algorithm'] ?? 'sha256')) - ->setSignatory($signatoryManager->getLocalSignatory()); + ->setAlgorithm($options['algorithm'] ?? SignatureAlgorithm::RSA_SHA256) + ->setSignatory($signatoryManager->getLocalSignatory()) + ->setDigestAlgorithm($options['digestAlgorithm'] ?? DigestAlgorithm::SHA256); $headers = array_merge([ '(request-target)' => strtolower($method) . ' ' . $path, diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php index dd3c1de431dbf..214e43e8cb343 100644 --- a/lib/private/Security/Signature/Model/SignedRequest.php +++ b/lib/private/Security/Signature/Model/SignedRequest.php @@ -9,6 +9,7 @@ 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; @@ -20,7 +21,8 @@ * @since 31.0.0 */ class SignedRequest implements ISignedRequest, JsonSerializable { - private string $digest; + private string $digest = ''; + private DigestAlgorithm $digestAlgorithm = DigestAlgorithm::SHA256; private array $signingElements = []; private array $signatureData = []; private string $signature = ''; @@ -29,8 +31,6 @@ class SignedRequest implements ISignedRequest, JsonSerializable { public function __construct( private readonly string $body, ) { - // digest is created on the fly using $body - $this->digest = 'SHA-256=' . base64_encode(hash('sha256', mb_convert_encoding($body, 'UTF-8', mb_detect_encoding($body)), true)); } /** @@ -43,6 +43,28 @@ public function getBody(): string { return $this->body; } + /** + * @inheritDoc + * + * @param DigestAlgorithm $algorithm + * + * @return self + * @since 31.0.0 + */ + public function setDigestAlgorithm(DigestAlgorithm $algorithm): self { + return $this; + } + + /** + * @inheritDoc + * + * @return DigestAlgorithm + * @since 31.0.0 + */ + public function getDigestAlgorithm(): DigestAlgorithm { + return $this->digestAlgorithm; + } + /** * @inheritDoc * @@ -50,6 +72,10 @@ public function getBody(): 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; } @@ -178,10 +204,11 @@ public function hasSignatory(): bool { public function jsonSerialize(): array { return [ 'body' => $this->body, - 'digest' => $this->digest, - 'signatureElements' => $this->signingElements, - 'clearSignature' => $this->signatureData, - 'signedSignature' => $this->signature, + 'digest' => $this->getDigest(), + 'digestAlgorithm' => $this->getDigestAlgorithm()->value, + 'signingElements' => $this->signingElements, + 'signatureData' => $this->signatureData, + 'signature' => $this->signature, 'signatory' => $this->signatory ?? false, ]; } diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php index 6247b7901fa4d..4fb4f37116625 100644 --- a/lib/private/Security/Signature/SignatureManager.php +++ b/lib/private/Security/Signature/SignatureManager.php @@ -111,7 +111,6 @@ public function getIncomingSignedRequest( try { // confirm the validity of content and identity of the incoming request - $this->generateExpectedClearSignatureFromRequest($signedRequest, $options['extraSignatureHeaders'] ?? []); $this->confirmIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL); } catch (SignatureException $e) { $this->logger->warning( @@ -127,44 +126,6 @@ public function getIncomingSignedRequest( return $signedRequest; } - /** - * generating the expected signature (clear version) sent by the remote instance - * based on the data available in the Signature header. - * - * @param IIncomingSignedRequest $signedRequest - * @param array $extraSignatureHeaders - * - * @throws SignatureException - */ - private function generateExpectedClearSignatureFromRequest( - IIncomingSignedRequest $signedRequest, - array $extraSignatureHeaders = [], - ): void { - $request = $signedRequest->getRequest(); - $usedHeaders = explode(' ', $signedRequest->getSigningElement('headers')); - $neededHeaders = array_merge(['date', 'host', 'content-length', 'digest'], array_keys($extraSignatureHeaders)); - - $missingHeaders = array_diff($neededHeaders, $usedHeaders); - if ($missingHeaders !== []) { - throw new SignatureException('missing entries in Signature.headers: ' . json_encode($missingHeaders)); - } - - $estimated = ['(request-target): ' . strtolower($request->getMethod()) . ' ' . $request->getRequestUri()]; - foreach ($usedHeaders as $key) { - if ($key === '(request-target)') { - continue; - } - $value = (strtolower($key) === 'host') ? $request->getServerHost() : $request->getHeader($key); - if ($value === '') { - throw new SignatureException('missing header ' . $key . ' in request'); - } - - $estimated[] = $key . ': ' . $value; - } - - $signedRequest->setSignatureData($estimated); - } - /** * 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 @@ -403,9 +364,11 @@ private function storeSignatory(Signatory $signatory): void { /** * @param Signatory $signatory - * @throws DBException */ private function insertSignatory(Signatory $signatory): void { + $time = time(); + $signatory->setCreation($time); + $signatory->setLastUpdated($time); $this->mapper->insert($signatory); } diff --git a/lib/unstable/Security/Signature/Enum/DigestAlgorithm.php b/lib/unstable/Security/Signature/Enum/DigestAlgorithm.php new file mode 100644 index 0000000000000..20208dce05c10 --- /dev/null +++ b/lib/unstable/Security/Signature/Enum/DigestAlgorithm.php @@ -0,0 +1,29 @@ + 'sha256', + self::SHA512 => 'sha512', + }; + } +} diff --git a/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php b/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php index 94996d17bd5ed..67a1df02d21f1 100644 --- a/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php +++ b/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php @@ -16,7 +16,7 @@ */ enum SignatureAlgorithm: string { /** @since 31.0.0 */ - case SHA256 = 'sha256'; + case RSA_SHA256 = 'rsa-sha256'; /** @since 31.0.0 */ - case SHA512 = 'sha512'; + case RSA_SHA512 = 'rsa-sha512'; } diff --git a/lib/unstable/Security/Signature/ISignedRequest.php b/lib/unstable/Security/Signature/ISignedRequest.php index 6f9e143c579f5..3c38018127d1e 100644 --- a/lib/unstable/Security/Signature/ISignedRequest.php +++ b/lib/unstable/Security/Signature/ISignedRequest.php @@ -8,6 +8,7 @@ */ namespace NCU\Security\Signature; +use NCU\Security\Signature\Enum\DigestAlgorithm; use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; use NCU\Security\Signature\Model\Signatory; @@ -30,6 +31,24 @@ interface ISignedRequest { */ public function getBody(): string; + /** + * set algorithm used to generate digest + * + * @param DigestAlgorithm $algorithm + * + * @return self + * @since 31.0.0 + */ + public function setDigestAlgorithm(DigestAlgorithm $algorithm): self; + + /** + * get algorithm used to generate digest + * + * @return DigestAlgorithm + * @since 31.0.0 + */ + public function getDigestAlgorithm(): DigestAlgorithm; + /** * checksum of the payload of the request * diff --git a/lib/unstable/Security/Signature/Model/Signatory.php b/lib/unstable/Security/Signature/Model/Signatory.php index 621cd5ac7ee63..787d6b9307376 100644 --- a/lib/unstable/Security/Signature/Model/Signatory.php +++ b/lib/unstable/Security/Signature/Model/Signatory.php @@ -27,12 +27,18 @@ * @method void setProviderId(string $providerId) * @method string getProviderId() * @method string getKeyId() + * @method void setKeyIdSum(string $keyIdSum) + * @method string getKeyIdSum() * @method void setPublicKey(string $publicKey) * @method string getPublicKey() * @method void setPrivateKey(string $privateKey) * @method string getPrivateKey() * @method void setHost(string $host) * @method string getHost() + * @method int getType() + * @method void setType(int $type) + * @method int getStatus() + * @method void setStatus(int $status) * @method void setAccount(string $account) * @method string getAccount() * @method void setMetadata(array $metadata) @@ -47,6 +53,8 @@ class Signatory extends Entity implements JsonSerializable { protected string $keyIdSum = ''; protected string $providerId = ''; protected string $host = ''; + protected string $publicKey = ''; + protected string $privateKey = ''; protected string $account = ''; protected int $type = 9; protected int $status = 1; @@ -62,12 +70,7 @@ class Signatory extends Entity implements JsonSerializable { * * @since 31.0.0 */ - public function __construct( - string $keyId = '', - protected string $publicKey = '', - protected string $privateKey = '', - private readonly bool $local = false, - ) { + public function __construct(private readonly bool $local = false) { $this->addType('providerId', 'string'); $this->addType('host', 'string'); $this->addType('account', 'string'); @@ -79,8 +82,6 @@ public function __construct( $this->addType('status', 'integer'); $this->addType('creation', 'integer'); $this->addType('lastUpdated', 'integer'); - - $this->setKeyId($keyId); } /** @@ -105,40 +106,40 @@ public function setKeyId(string $keyId): void { } } } - $this->keyId = $keyId; - $this->keyIdSum = hash('sha256', $keyId); + $this->setter('keyId', [$keyId]); // needed to trigger the update in database + $this->setKeyIdSum(hash('sha256', $keyId)); } /** * @param SignatoryType $type * @since 31.0.0 */ - public function setType(SignatoryType $type): void { - $this->type = $type->value; + public function setSignatoryType(SignatoryType $type): void { + $this->setType($type->value); } /** * @return SignatoryType * @since 31.0.0 */ - public function getType(): SignatoryType { - return SignatoryType::from($this->type); + public function getSignatoryType(): SignatoryType { + return SignatoryType::from($this->getType()); } /** * @param SignatoryStatus $status * @since 31.0.0 */ - public function setStatus(SignatoryStatus $status): void { - $this->status = $status->value; + public function setSignatoryStatus(SignatoryStatus $status): void { + $this->setStatus($status->value); } /** * @return SignatoryStatus * @since 31.0.0 */ - public function getStatus(): SignatoryStatus { - return SignatoryStatus::from($this->status); + public function getSignatoryStatus(): SignatoryStatus { + return SignatoryStatus::from($this->getStatus()); } /**