Add signing key management functionality

This commit is contained in:
netkas 2025-01-03 18:30:50 -05:00
parent d732c89632
commit e4b9a08972
7 changed files with 693 additions and 1 deletions

View file

@ -0,0 +1,62 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use Exception;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Configuration;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\StandardException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\SigningKeysManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class SettingsAddSigningKey extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->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);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\StandardException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\SigningKeysManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class SettingsGetSigningKeys extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
$keys = SigningKeysManager::getSigningKeys($request->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));
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Socialbox\Enums;
enum SigningKeyState : string
{
case ACTIVE = 'active';
case EXPIRED = 'expired';
case NOT_FOUND = 'not_found';
}

View file

@ -10,6 +10,8 @@
use Socialbox\Classes\StandardMethods\GetSessionState;
use Socialbox\Classes\StandardMethods\GetTermsOfService;
use Socialbox\Classes\StandardMethods\Ping;
use Socialbox\Classes\StandardMethods\SettingsAddSigningKey;
use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys;
use Socialbox\Classes\StandardMethods\SettingsSetDisplayName;
use Socialbox\Classes\StandardMethods\SettingsSetPassword;
use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha;
@ -58,6 +60,9 @@
case SETTINGS_SET_PHONE = 'settingsSetPhone';
case SETTINGS_SET_BIRTHDAY = 'settingsSetBirthday';
case SETTINGS_ADD_SIGNING_KEY = 'settingsAddSigningKey';
case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys';
/**
* Executes the appropriate operation based on the current context and requests provided.
*
@ -86,6 +91,9 @@
self::SETTINGS_SET_PASSWORD => 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()))
{

View file

@ -0,0 +1,224 @@
<?php
namespace Socialbox\Managers;
use DateTime;
use InvalidArgumentException;
use ncc\ThirdParty\Symfony\Uid\UuidV4;
use PDOException;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Database;
use Socialbox\Enums\SigningKeyState;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Objects\Database\SigningKeyRecord;
class SigningKeysManager
{
/**
* Retrieves the state of a signing key identified by its UUID.
*
* @param string $uuid The UUID of the signing key whose state is to be retrieved.
* @return SigningKeyState The state of the signing key. Returns SigningKeyState::EXPIRED if the key is expired,
* SigningKeyState::NOT_FOUND if the key does not exist, or the state as defined in the database.
* @throws DatabaseOperationException If an error occurs during the database operation.
*/
public static function getSigningKeyState(string $uuid): SigningKeyState
{
try
{
$statement = Database::getConnection()->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());
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace Socialbox\Objects\Database;
use DateTime;
use InvalidArgumentException;
use Socialbox\Enums\SigningKeyState;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\Standard\SigningKey;
class SigningKeyRecord implements SerializableInterface
{
private string $peerUuid;
private string $uuid;
private ?string $name;
private string $publicKey;
private SigningKeyState $state;
private int $expires;
private int $created;
/**
* Constructs a new instance of this class and initializes its properties with the provided data.
*
* @param array $data An associative array containing initialization data. Expected keys:
* - 'peer_uuid' (string): The peer UUID.
* - 'uuid' (string): The unique identifier.
* - 'name' (string|null): The name, which is optional.
* - 'public_key' (string): The associated public key.
* - 'state' (string|null): The state, which will be cast to a SigningKeyState instance.
* - 'expires' (int|DateTime): The expiration time as a timestamp or DateTime object.
* - 'created' (int|DateTime): The creation time as a timestamp or DateTime object.
* @return void
*/
public function __construct(array $data)
{
$this->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);
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace Socialbox\Objects\Standard;
use DateTime;
use InvalidArgumentException;
use Socialbox\Enums\SigningKeyState;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\Database\SigningKeyRecord;
class SigningKey implements SerializableInterface
{
private string $uuid;
private ?string $name;
private string $publicKey;
private SigningKeyState $state;
private int $expires;
private int $created;
/**
* Constructs a new instance of the class, initializing its properties using the provided data.
*
* @param array $data An associative array containing initialization data.
* Expected keys are:
* - 'uuid' (string): The unique identifier for the instance.
* - 'name' (string|null): The optional name for the instance.
* - 'public_key' (string): The public key associated with the instance.
* - 'state' (string): The state of the signing key.
* - 'expires' (int|DateTime): The expiration timestamp or DateTime object.
* - 'created' (int|DateTime): The creation timestamp or DateTime object.
*
* @return void
* @throws InvalidArgumentException If 'expires' or 'created' are not valid integer timestamps or DateTime instances.
*/
public function __construct(array $data)
{
$this->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
];
}
}