182 lines
7.0 KiB
PHP
182 lines
7.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2014-2021 Spomky-Labs
|
|
*
|
|
* This software may be modified and distributed under the terms
|
|
* of the MIT license. See the LICENSE file for details.
|
|
*/
|
|
|
|
namespace Webauthn;
|
|
|
|
use function array_key_exists;
|
|
use Assert\Assertion;
|
|
use Base64Url\Base64Url;
|
|
use CBOR\Decoder;
|
|
use CBOR\MapObject;
|
|
use CBOR\OtherObject\OtherObjectManager;
|
|
use CBOR\Tag\TagObjectManager;
|
|
use InvalidArgumentException;
|
|
use function ord;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use Ramsey\Uuid\Uuid;
|
|
use function Safe\json_decode;
|
|
use function Safe\sprintf;
|
|
use function Safe\unpack;
|
|
use Throwable;
|
|
use Webauthn\AttestationStatement\AttestationObjectLoader;
|
|
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
|
|
|
|
class PublicKeyCredentialLoader
|
|
{
|
|
private const FLAG_AT = 0b01000000;
|
|
private const FLAG_ED = 0b10000000;
|
|
|
|
/**
|
|
* @var AttestationObjectLoader
|
|
*/
|
|
private $attestationObjectLoader;
|
|
|
|
/**
|
|
* @var Decoder
|
|
*/
|
|
private $decoder;
|
|
|
|
/**
|
|
* @var LoggerInterface
|
|
*/
|
|
private $logger;
|
|
|
|
public function __construct(AttestationObjectLoader $attestationObjectLoader, ?LoggerInterface $logger = null)
|
|
{
|
|
if (null !== $logger) {
|
|
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
|
|
}
|
|
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
|
|
$this->attestationObjectLoader = $attestationObjectLoader;
|
|
$this->logger = $logger ?? new NullLogger();
|
|
}
|
|
|
|
public static function create(AttestationObjectLoader $attestationObjectLoader): self
|
|
{
|
|
return new self($attestationObjectLoader);
|
|
}
|
|
|
|
public function setLogger(LoggerInterface $logger): self
|
|
{
|
|
$this->logger = $logger;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param mixed[] $json
|
|
*/
|
|
public function loadArray(array $json): PublicKeyCredential
|
|
{
|
|
$this->logger->info('Trying to load data from an array', ['data' => $json]);
|
|
try {
|
|
foreach (['id', 'rawId', 'type'] as $key) {
|
|
Assertion::keyExists($json, $key, sprintf('The parameter "%s" is missing', $key));
|
|
Assertion::string($json[$key], sprintf('The parameter "%s" shall be a string', $key));
|
|
}
|
|
Assertion::keyExists($json, 'response', 'The parameter "response" is missing');
|
|
Assertion::isArray($json['response'], 'The parameter "response" shall be an array');
|
|
Assertion::eq($json['type'], 'public-key', sprintf('Unsupported type "%s"', $json['type']));
|
|
|
|
$id = Base64Url::decode($json['id']);
|
|
$rawId = Base64Url::decode($json['rawId']);
|
|
Assertion::true(hash_equals($id, $rawId));
|
|
|
|
$publicKeyCredential = new PublicKeyCredential(
|
|
$json['id'],
|
|
$json['type'],
|
|
$rawId,
|
|
$this->createResponse($json['response'])
|
|
);
|
|
$this->logger->info('The data has been loaded');
|
|
$this->logger->debug('Public Key Credential', ['publicKeyCredential' => $publicKeyCredential]);
|
|
|
|
return $publicKeyCredential;
|
|
} catch (Throwable $throwable) {
|
|
$this->logger->error('An error occurred', [
|
|
'exception' => $throwable,
|
|
]);
|
|
throw $throwable;
|
|
}
|
|
}
|
|
|
|
public function load(string $data): PublicKeyCredential
|
|
{
|
|
$this->logger->info('Trying to load data from a string', ['data' => $data]);
|
|
try {
|
|
$json = json_decode($data, true);
|
|
|
|
return $this->loadArray($json);
|
|
} catch (Throwable $throwable) {
|
|
$this->logger->error('An error occurred', [
|
|
'exception' => $throwable,
|
|
]);
|
|
throw $throwable;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param mixed[] $response
|
|
*/
|
|
private function createResponse(array $response): AuthenticatorResponse
|
|
{
|
|
Assertion::keyExists($response, 'clientDataJSON', 'Invalid data. The parameter "clientDataJSON" is missing');
|
|
Assertion::string($response['clientDataJSON'], 'Invalid data. The parameter "clientDataJSON" is invalid');
|
|
switch (true) {
|
|
case array_key_exists('attestationObject', $response):
|
|
Assertion::string($response['attestationObject'], 'Invalid data. The parameter "attestationObject " is invalid');
|
|
$attestationObject = $this->attestationObjectLoader->load($response['attestationObject']);
|
|
|
|
return new AuthenticatorAttestationResponse(CollectedClientData::createFormJson($response['clientDataJSON']), $attestationObject);
|
|
case array_key_exists('authenticatorData', $response) && array_key_exists('signature', $response):
|
|
$authData = Base64Url::decode($response['authenticatorData']);
|
|
|
|
$authDataStream = new StringStream($authData);
|
|
$rp_id_hash = $authDataStream->read(32);
|
|
$flags = $authDataStream->read(1);
|
|
$signCount = $authDataStream->read(4);
|
|
$signCount = unpack('N', $signCount)[1];
|
|
|
|
$attestedCredentialData = null;
|
|
if (0 !== (ord($flags) & self::FLAG_AT)) {
|
|
$aaguid = Uuid::fromBytes($authDataStream->read(16));
|
|
$credentialLength = $authDataStream->read(2);
|
|
$credentialLength = unpack('n', $credentialLength)[1];
|
|
$credentialId = $authDataStream->read($credentialLength);
|
|
$credentialPublicKey = $this->decoder->decode($authDataStream);
|
|
Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.');
|
|
$attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey);
|
|
}
|
|
|
|
$extension = null;
|
|
if (0 !== (ord($flags) & self::FLAG_ED)) {
|
|
$extension = $this->decoder->decode($authDataStream);
|
|
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
|
|
}
|
|
Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.');
|
|
$authDataStream->close();
|
|
$authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
|
|
|
|
return new AuthenticatorAssertionResponse(
|
|
CollectedClientData::createFormJson($response['clientDataJSON']),
|
|
$authenticatorData,
|
|
Base64Url::decode($response['signature']),
|
|
$response['userHandle'] ?? null
|
|
);
|
|
default:
|
|
throw new InvalidArgumentException('Unable to create the response object');
|
|
}
|
|
}
|
|
}
|