<?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'); } } }