From e4b9a08972086f19593a130a1538896e085d1745 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 18:30:50 -0500 Subject: [PATCH] Add signing key management functionality --- .../StandardMethods/SettingsAddSigningKey.php | 62 +++++ .../SettingsGetSigningKeys.php | 39 +++ src/Socialbox/Enums/SigningKeyState.php | 10 + src/Socialbox/Enums/StandardMethods.php | 14 +- src/Socialbox/Managers/SigningKeysManager.php | 224 ++++++++++++++++++ .../Objects/Database/SigningKeyRecord.php | 171 +++++++++++++ src/Socialbox/Objects/Standard/SigningKey.php | 174 ++++++++++++++ 7 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php create mode 100644 src/Socialbox/Enums/SigningKeyState.php create mode 100644 src/Socialbox/Managers/SigningKeysManager.php create mode 100644 src/Socialbox/Objects/Database/SigningKeyRecord.php create mode 100644 src/Socialbox/Objects/Standard/SigningKey.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php b/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php new file mode 100644 index 0000000..60b4f6a --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php @@ -0,0 +1,62 @@ +containsParameter('public_key')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'public_key' parameter"); + } + + $expires = null; + if($rpcRequest->containsParameter('expires')) + { + $expires = (int)$rpcRequest->getParameter('expires'); + } + + $name = null; + if($rpcRequest->containsParameter('name')) + { + $name = $rpcRequest->getParameter('name'); + } + + $peerUuid = $request->getPeer()->getUuid(); + + try + { + if(SigningKeysManager::getSigningKeyCount($peerUuid) >= Configuration::getPoliciesConfiguration()->getMaxSigningKeys()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The maximum number of signing keys has been reached'); + } + + $uuid = SigningKeysManager::addSigningKey($peerUuid, $rpcRequest->getParameter('public_key'), $expires, $name); + } + catch(InvalidArgumentException $e) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); + } + catch(Exception $e) + { + throw new StandardException('Failed to add the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse($uuid); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php b/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php new file mode 100644 index 0000000..615aac4 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php @@ -0,0 +1,39 @@ +getPeer()->getUuid()); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if(empty($keys)) + { + // Return an empty array if the results are empty + return $rpcRequest->produceResponse([]); + } + + // Return the signing keys as an array of standard objects + return $rpcRequest->produceResponse(array_map(fn($key) => $key->toStandard(), $keys)); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/SigningKeyState.php b/src/Socialbox/Enums/SigningKeyState.php new file mode 100644 index 0000000..403d930 --- /dev/null +++ b/src/Socialbox/Enums/SigningKeyState.php @@ -0,0 +1,10 @@ + SettingsSetPassword::execute($request, $rpcRequest), self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), + self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), + default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; } @@ -158,9 +166,13 @@ $methods[] = self::SETTINGS_SET_PASSWORD; } - // If the user is authenticated, then preform additional method calls + // If the user is authenticated, then allow additional method calls if($session->isAuthenticated()) { + // Authenticated users can always manage their signing keys + $methods[] = self::SETTINGS_ADD_SIGNING_KEY; + $methods[] = self::SETTINGS_GET_SIGNING_KEYS; + // Always allow the authenticated user to change their password if(!in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) { diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php new file mode 100644 index 0000000..c74be55 --- /dev/null +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -0,0 +1,224 @@ +prepare("SELECT state, expires FROM signing_keys WHERE uuid=:uuid"); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + + if($row = $statement->fetch()) + { + if(is_int($row['expires']) && $row['expires'] < time()) + { + return SigningKeyState::EXPIRED; + } + + if($row['expires'] instanceof DateTime && $row['expires'] < new DateTime()) + { + return SigningKeyState::EXPIRED; + } + + return SigningKeyState::tryFrom($row['state']) ?? SigningKeyState::NOT_FOUND; + } + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing key state from the database', $e); + } + + return SigningKeyState::NOT_FOUND; + } + + /** + * Retrieves the count of signing keys associated with a specific peer UUID. + * + * @param string $peerUuid The UUID of the peer for which to count the signing keys. + * @return int The number of signing keys associated with the given peer UUID. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function getSigningKeyCount(string $peerUuid): int + { + try + { + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM signing_keys WHERE peer_uuid=:peer_uuid"); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->execute(); + + return $statement->fetchColumn(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing key count from the database', $e); + } + } + + /** + * Adds a signing key to the database for a specific peer. + * + * @param string $peerUuid The unique identifier of the peer associated with the signing key. + * @param string $publicKey The public signing key to be added. Must be valid according to the Cryptography::validatePublicSigningKey method. + * @param int|null $expires Optional expiration timestamp for the signing key. Can be null if the key does not expire. + * @param string|null $name Optional name associated with the signing key. Must not exceed 64 characters in length. + * @throws DatabaseOperationException If the operation to add the signing key to the database fails. + * @return string The UUID of the newly added signing key. + */ + public static function addSigningKey(string $peerUuid, string $publicKey, ?int $expires=null, ?string $name=null): string + { + if(!Cryptography::validatePublicSigningKey($publicKey)) + { + throw new InvalidArgumentException('The public key is invalid'); + } + + if(strlen($name) > 64) + { + throw new InvalidArgumentException('The name is too long'); + } + + $uuid = UuidV4::v4()->toRfc4122(); + + try + { + $statement = Database::getConnection()->prepare("INSERT INTO signing_keys (uuid, peer_uuid, public_key, expires, name) VALUES (:uuid, :peer_uuid, :public_key, :expires, :name)"); + $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->bindParam(':public_key', $publicKey); + $statement->bindParam(':expires', $expires); + $statement->bindParam(':name', $name); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to add a signing key to the database', $e); + } + + return $uuid; + } + + /** + * Updates the state of a signing key in the database identified by its UUID. + * + * @param string $uuid The unique identifier of the signing key to update. + * @param SigningKeyState $state The new state to set for the signing key. + * @return void + * @throws DatabaseOperationException + */ + public static function updateSigningKeyState(string $uuid, SigningKeyState $state): void + { + $state = $state->value; + + try + { + $statement = Database::getConnection()->prepare("UPDATE signing_keys SET state=:state WHERE uuid=:uuid"); + $statement->bindParam(':state', $state); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to update the signing key state in the database', $e); + } + } + + /** + * Retrieves a signing key from the database using the provided UUID. + * + * @param string $uuid The UUID of the signing key to retrieve. + * @return SigningKeyRecord|null The signing key record if found, or null if no record exists. + * @throws DatabaseOperationException If a database error occurs during the operation. + */ + public static function getSigningKey(string $uuid): ?SigningKeyRecord + { + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM signing_keys WHERE uuid=:uuid"); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + + if($row = $statement->fetch()) + { + return SigningKeyRecord::fromArray($row); + } + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing key from the database', $e); + } + + return null; + } + + /** + * Retrieves the signing keys associated with a specific peer UUID. + * + * @param string $peerUuid The UUID of the peer whose signing keys are to be retrieved. + * @return SigningKeyRecord[] An array of SigningKeyRecord objects representing the signing keys. + * @throws DatabaseOperationException If an error occurs during the database operation. + */ + public static function getSigningKeys(string $peerUuid): array + { + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM signing_keys WHERE peer_uuid=:peer_uuid"); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->execute(); + + $signingKeys = []; + while($row = $statement->fetch()) + { + $signingKeys[] = SigningKeyRecord::fromArray($row); + } + + return $signingKeys; + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing keys from the database', $e); + } + } + + /** + * Verifies the digital signature of a message using the signing key associated with a specific UUID. + * + * @param string $message The message whose signature needs to be verified. + * @param string $signature The digital signature to be verified. + * @param string $uuid The UUID used to retrieve the corresponding signing key. + * @return bool True if the signature is valid, false otherwise. + * @throws CryptographyException If an error occurs during the cryptographic operation. + * @throws DatabaseOperationException If an error occurs during the database operation. + */ + public static function verifySignature(string $message, string $signature, string $uuid): bool + { + $signingKey = self::getSigningKey($uuid); + if($signingKey === null) + { + return false; + } + + return Cryptography::verifyMessage($message, $signature, $signingKey->getPublicKey()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SigningKeyRecord.php b/src/Socialbox/Objects/Database/SigningKeyRecord.php new file mode 100644 index 0000000..c00bc3e --- /dev/null +++ b/src/Socialbox/Objects/Database/SigningKeyRecord.php @@ -0,0 +1,171 @@ +peerUuid = $data['peer_uuid']; + $this->uuid = $data['uuid']; + $this->name = $data['name'] ?? null; + $this->publicKey = $data['public_key']; + $this->state = SigningKeyState::tryFrom($data['state']); + + if(is_int($data['expires'])) + { + $this->expires = $data['expires']; + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid expires value'); + } + + if(is_int($data['created'])) + { + $this->created = $data['created']; + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid created value'); + } + } + + /** + * Retrieves the UUID of the peer. + * + * @return string The UUID of the peer. + */ + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + /** + * Retrieves the UUID associated with this instance. + * + * @return string The UUID as a string. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the name. + * + * @return string|null The name, or null if not set. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Retrieves the public key. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the current state of the signing key. + * + * @return SigningKeyState The state of the signing key. + */ + public function getState(): SigningKeyState + { + return $this->state; + } + + /** + * Retrieves the expiration timestamp. + * + * @return int The expiration timestamp as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * + * @return int Returns the created timestamp as an integer. + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SigningKeyRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'peer_uuid' => $this->peerUuid, + 'uuid' => $this->uuid, + 'name' => $this->name, + 'public_key' => $this->publicKey, + 'state' => $this->state->value, + 'expires' => (new DateTime())->setTimestamp($this->expires), + 'created' => (new DateTime())->setTimestamp($this->created) + ]; + } + + /** + * Converts the current signing key record to its standard format. + * + * @return SigningKey The signing key in its standard format. + */ + public function toStandard(): SigningKey + { + return SigningKey::fromSigningKeyRecord($this); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/SigningKey.php b/src/Socialbox/Objects/Standard/SigningKey.php new file mode 100644 index 0000000..0dd7757 --- /dev/null +++ b/src/Socialbox/Objects/Standard/SigningKey.php @@ -0,0 +1,174 @@ +uuid = $data['uuid']; + $this->name = $data['name'] ?? null; + $this->publicKey = $data['public_key']; + $this->state = SigningKeyState::from($data['state']); + + if(is_int($data['expires'])) + { + $this->expires = $data['expires']; + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid expires value'); + } + + if(is_int($data['created'])) + { + $this->created = $data['created']; + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid created value'); + } + } + + /** + * Retrieves the UUID associated with this instance. + * + * @return string The UUID as a string. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the name associated with this instance. + * + * @return string|null The name as a string, or null if not set. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * + * Retrieves the public key. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the current state of the signing key. + * + * @return SigningKeyState The current state of the signing key. + */ + public function getState(): SigningKeyState + { + if(time() > $this->expires) + { + return SigningKeyState::EXPIRED; + } + + return $this->state; + } + + /** + * Retrieves the expiration time. + * + * @return int The expiration time as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * + * @return int The timestamp representing the creation time. + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * Creates a new SigningKey instance from a SigningKeyRecord. + * + * @param SigningKeyRecord $record The record containing the signing key data. + * @return SigningKey An instance of SigningKey populated with data from the provided record. + */ + public static function fromSigningKeyRecord(SigningKeyRecord $record): SigningKey + { + return new self([ + 'uuid' => $record->getUuid(), + 'name' => $record->getName(), + 'public_key' => $record->getPublicKey(), + 'state' => $record->getState(), + 'expires' => $record->getExpires(), + 'created' => $record->getCreated() + ]); + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SigningKey + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'name' => $this->name, + 'public_key' => $this->publicKey, + 'state' => $this->state->value, + 'expires' => $this->expires, + 'created' => $this->created + ]; + } + } \ No newline at end of file