Add controllers and packages for security keys

This commit is contained in:
Matthew Penner 2022-10-24 09:44:16 -06:00
parent f8ec8b4d5a
commit 06f692e649
No known key found for this signature in database
29 changed files with 2398 additions and 383 deletions

View file

@ -0,0 +1,68 @@
<?php
namespace Pterodactyl\Repositories\SecurityKeys;
use Pterodactyl\Models\User;
use Illuminate\Container\Container;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialUserEntity;
use Pterodactyl\Models\SecurityKey;
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyRepositoryInterface;
class PublicKeyCredentialSourceRepository implements PublicKeyRepositoryInterface
{
protected User $user;
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Find a single hardware security token for a user by using the credential ID.
*/
public function findOneByCredentialId(string $id): ?PublicKeyCredentialSource
{
/** @var \Pterodactyl\Models\SecurityKey $key */
$key = $this->user->securityKeys()
->where('public_key_id', base64_encode($id))
->first();
return optional($key)->getPublicKeyCredentialSource();
}
/**
* Find all the hardware tokens that exist for the user using the given
* entity handle.
*/
public function findAllForUserEntity(PublicKeyCredentialUserEntity $entity): array
{
$results = $this->user->securityKeys()
->where('user_handle', $entity->getId())
->get();
return $results->map(function (SecurityKey $key) {
return $key->getPublicKeyCredentialSource();
})->values()->toArray();
}
/**
* Save a credential to the database and link it with the user.
*
* @throws \Throwable
*/
public function saveCredentialSource(PublicKeyCredentialSource $source): void
{
// no-op — we handle creation of the keys in StoreSecurityKeyService
//
// If you put logic in here it is triggered on each login.
}
/**
* Returns a new instance of the repository with the provided user attached.
*/
public static function factory(User $user): self
{
return Container::getInstance()->make(static::class, ['user' => $user]);
}
}

View file

@ -0,0 +1,161 @@
<?php
namespace Pterodactyl\Repositories\SecurityKeys;
use Cose\Algorithms;
use Illuminate\Support\Str;
use Pterodactyl\Models\User;
use Pterodactyl\Models\SecurityKey;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialParameters;
use Psr\Http\Message\ServerRequestInterface;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\AuthenticatorAttestationResponse;
use Cose\Algorithm\Manager as AlgorithmManager;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
final class WebauthnServerRepository
{
private PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository;
private PublicKeyCredentialRpEntity $rpEntity;
private PublicKeyCredentialLoader $credentialLoader;
private AuthenticatorAssertionResponseValidator $assertionValidator;
private AuthenticatorAttestationResponseValidator $attestationValidator;
public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository)
{
$url = str_replace(['http://', 'https://'], '', config('app.url'));
$this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
$this->rpEntity = new PublicKeyCredentialRpEntity(config('app.name'), trim($url, '/'));
$this->credentialLoader = new PublicKeyCredentialLoader(new AttestationObjectLoader(new AttestationStatementSupportManager()));
$this->assertionValidator = new AuthenticatorAssertionResponseValidator(
$this->publicKeyCredentialSourceRepository,
null,
ExtensionOutputCheckerHandler::create(),
AlgorithmManager::create(),
);
$this->attestationValidator = new AuthenticatorAttestationResponseValidator(
new AttestationStatementSupportManager(),
$this->publicKeyCredentialSourceRepository,
null,
new ExtensionOutputCheckerHandler(),
);
}
/**
* @throws \Webauthn\Exception\InvalidDataException
*/
public function getPublicKeyCredentialCreationOptions(User $user): PublicKeyCredentialCreationOptions
{
$excluded = $user->securityKeys->map(function (SecurityKey $key) {
return $key->getPublicKeyCredentialDescriptor();
})->values()->toArray();
$challenge = Str::random(16);
return (new PublicKeyCredentialCreationOptions(
$this->rpEntity,
$user->toPublicKeyCredentialEntity(),
$challenge,
[
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256K),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES384),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES512),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS384),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS512),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS384),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS512),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED256),
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED512),
],
))
->setTimeout(30_000)
->excludeCredentials(...$excluded)
->setAttestation(PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE)
->setAuthenticatorSelection(AuthenticatorSelectionCriteria::create());
}
/**
* @throws \Webauthn\Exception\InvalidDataException
*/
public function generatePublicKeyCredentialRequestOptions(User $user): PublicKeyCredentialRequestOptions
{
$allowedCredentials = $user->securityKeys->map(function (SecurityKey $key) {
return $key->getPublicKeyCredentialDescriptor();
})->values()->toArray();
return (new PublicKeyCredentialRequestOptions(Str::random(32)))
->allowCredentials(...$allowedCredentials)
->setUserVerification(PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED);
}
/**
* @throws \Throwable
* @throws \JsonException
*/
public function loadAndCheckAssertionResponse(
User $user,
array $data,
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
ServerRequestInterface $request
): PublicKeyCredentialSource {
$credential = $this->credentialLoader->loadArray($data);
$authenticatorAssertionResponse = $credential->getResponse();
if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
// TODO
throw new \Exception('');
}
return $this->assertionValidator->check(
$credential->getRawId(),
$authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions,
$request,
null, // TODO: use handle?
// $user->toPublicKeyCredentialEntity()
);
}
/**
* Register a new security key for a user.
*
* @throws \Throwable
* @throws \JsonException
*/
public function loadAndCheckAttestationResponse(
User $user,
array $data,
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
ServerRequestInterface $request
): PublicKeyCredentialSource {
$credential = $this->credentialLoader->loadArray($data);
$authenticatorAttestationResponse = $credential->getResponse();
if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) {
// TODO
throw new \Exception('');
}
return $this->attestationValidator->check(
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions,
$request,
);
}
}