File manager - Edit - /home/opticamezl/www/newok/webauthn-lib.tar
Back
LICENSE 0000644 00000002054 15173222011 0005544 0 ustar 00 MIT License Copyright (c) 2018 Spomky-Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. src/AuthenticatorAttestationResponse.php 0000644 00000001556 15173222011 0014616 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Webauthn\AttestationStatement\AttestationObject; /** * @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse */ class AuthenticatorAttestationResponse extends AuthenticatorResponse { /** * @var AttestationObject */ private $attestationObject; public function __construct(CollectedClientData $clientDataJSON, AttestationObject $attestationObject) { parent::__construct($clientDataJSON); $this->attestationObject = $attestationObject; } public function getAttestationObject(): AttestationObject { return $this->attestationObject; } } src/AuthenticatorData.php 0000644 00000005543 15173222011 0011451 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs; /** * @see https://www.w3.org/TR/webauthn/#sec-authenticator-data */ class AuthenticatorData { /** * @var string */ protected $authData; /** * @var string */ protected $rpIdHash; /** * @var string */ protected $flags; /** * @var int */ protected $signCount; /** * @var AttestedCredentialData|null */ protected $attestedCredentialData; /** * @var AuthenticationExtensionsClientOutputs|null */ protected $extensions; private const FLAG_UP = 0b00000001; private const FLAG_RFU1 = 0b00000010; private const FLAG_UV = 0b00000100; private const FLAG_RFU2 = 0b00111000; private const FLAG_AT = 0b01000000; private const FLAG_ED = 0b10000000; public function __construct(string $authData, string $rpIdHash, string $flags, int $signCount, ?AttestedCredentialData $attestedCredentialData, ?AuthenticationExtensionsClientOutputs $extensions) { $this->rpIdHash = $rpIdHash; $this->flags = $flags; $this->signCount = $signCount; $this->attestedCredentialData = $attestedCredentialData; $this->extensions = $extensions; $this->authData = $authData; } public function getAuthData(): string { return $this->authData; } public function getRpIdHash(): string { return $this->rpIdHash; } public function isUserPresent(): bool { return 0 !== (\ord($this->flags) & self::FLAG_UP) ? true : false; } public function isUserVerified(): bool { return 0 !== (\ord($this->flags) & self::FLAG_UV) ? true : false; } public function hasAttestedCredentialData(): bool { return 0 !== (\ord($this->flags) & self::FLAG_AT) ? true : false; } public function hasExtensions(): bool { return 0 !== (\ord($this->flags) & self::FLAG_ED) ? true : false; } public function getReservedForFutureUse1(): int { return \ord($this->flags) & self::FLAG_RFU1; } public function getReservedForFutureUse2(): int { return \ord($this->flags) & self::FLAG_RFU2; } public function getSignCount(): int { return $this->signCount; } public function getAttestedCredentialData(): ?AttestedCredentialData { return $this->attestedCredentialData; } public function getExtensions(): ?AuthenticationExtensionsClientOutputs { return null !== $this->extensions && $this->hasExtensions() ? $this->extensions : null; } } src/Server.php 0000644 00000026306 15173222011 0007313 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Cose\Algorithm\Algorithm; use Cose\Algorithm\ManagerFactory; use Cose\Algorithm\Signature\ECDSA; use Cose\Algorithm\Signature\EdDSA; use Cose\Algorithm\Signature\RSA; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ServerRequestInterface; use Webauthn\AttestationStatement\AndroidKeyAttestationStatementSupport; use Webauthn\AttestationStatement\AndroidSafetyNetAttestationStatementSupport; use Webauthn\AttestationStatement\AttestationObjectLoader; use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AttestationStatement\FidoU2FAttestationStatementSupport; use Webauthn\AttestationStatement\NoneAttestationStatementSupport; use Webauthn\AttestationStatement\PackedAttestationStatementSupport; use Webauthn\AttestationStatement\TPMAttestationStatementSupport; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\TokenBinding\TokenBindingNotSupportedHandler; class Server { /** * @var int */ public $timeout = 60000; /** * @var int */ public $challengeSize = 32; /** * @var PublicKeyCredentialRpEntity */ private $rpEntity; /** * @var ManagerFactory */ private $coseAlgorithmManagerFactory; /** * @var PublicKeyCredentialSourceRepository */ private $publicKeyCredentialSourceRepository; /** * @var TokenBindingNotSupportedHandler */ private $tokenBindingHandler; /** * @var ExtensionOutputCheckerHandler */ private $extensionOutputCheckerHandler; /** * @var string[] */ private $selectedAlgorithms; /** * @var MetadataStatementRepository|null */ private $metadataStatementRepository; /** * @var ClientInterface */ private $httpClient; /** * @var string */ private $googleApiKey; /** * @var RequestFactoryInterface */ private $requestFactory; public function __construct(PublicKeyCredentialRpEntity $relayingParty, PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, ?MetadataStatementRepository $metadataStatementRepository) { $this->rpEntity = $relayingParty; $this->coseAlgorithmManagerFactory = new ManagerFactory(); $this->coseAlgorithmManagerFactory->add('RS1', new RSA\RS1()); $this->coseAlgorithmManagerFactory->add('RS256', new RSA\RS256()); $this->coseAlgorithmManagerFactory->add('RS384', new RSA\RS384()); $this->coseAlgorithmManagerFactory->add('RS512', new RSA\RS512()); $this->coseAlgorithmManagerFactory->add('PS256', new RSA\PS256()); $this->coseAlgorithmManagerFactory->add('PS384', new RSA\PS384()); $this->coseAlgorithmManagerFactory->add('PS512', new RSA\PS512()); $this->coseAlgorithmManagerFactory->add('ES256', new ECDSA\ES256()); $this->coseAlgorithmManagerFactory->add('ES256K', new ECDSA\ES256K()); $this->coseAlgorithmManagerFactory->add('ES384', new ECDSA\ES384()); $this->coseAlgorithmManagerFactory->add('ES512', new ECDSA\ES512()); $this->coseAlgorithmManagerFactory->add('Ed25519', new EdDSA\Ed25519()); $this->selectedAlgorithms = ['RS256', 'RS512', 'PS256', 'PS512', 'ES256', 'ES512', 'Ed25519']; $this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository; $this->tokenBindingHandler = new TokenBindingNotSupportedHandler(); $this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler(); $this->metadataStatementRepository = $metadataStatementRepository; } /** * @param string[] $selectedAlgorithms */ public function setSelectedAlgorithms(array $selectedAlgorithms): void { $this->selectedAlgorithms = $selectedAlgorithms; } public function setTokenBindingHandler(TokenBindingNotSupportedHandler $tokenBindingHandler): void { $this->tokenBindingHandler = $tokenBindingHandler; } public function addAlgorithm(string $alias, Algorithm $algorithm): void { $this->coseAlgorithmManagerFactory->add($alias, $algorithm); $this->selectedAlgorithms[] = $alias; $this->selectedAlgorithms = array_unique($this->selectedAlgorithms); } public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void { $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; } /** * @param PublicKeyCredentialDescriptor[] $excludedPublicKeyDescriptors */ public function generatePublicKeyCredentialCreationOptions(PublicKeyCredentialUserEntity $userEntity, ?string $attestationMode = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, array $excludedPublicKeyDescriptors = [], ?AuthenticatorSelectionCriteria $criteria = null, ?AuthenticationExtensionsClientInputs $extensions = null): PublicKeyCredentialCreationOptions { $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms); $publicKeyCredentialParametersList = []; foreach ($coseAlgorithmManager->all() as $algorithm) { $publicKeyCredentialParametersList[] = new PublicKeyCredentialParameters( PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, $algorithm::identifier() ); } $criteria = $criteria ?? new AuthenticatorSelectionCriteria(); $extensions = $extensions ?? new AuthenticationExtensionsClientInputs(); $challenge = random_bytes($this->challengeSize); return new PublicKeyCredentialCreationOptions( $this->rpEntity, $userEntity, $challenge, $publicKeyCredentialParametersList, $this->timeout, $excludedPublicKeyDescriptors, $criteria, $attestationMode, $extensions ); } /** * @param PublicKeyCredentialDescriptor[] $allowedPublicKeyDescriptors */ public function generatePublicKeyCredentialRequestOptions(?string $userVerification = PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, array $allowedPublicKeyDescriptors = [], ?AuthenticationExtensionsClientInputs $extensions = null): PublicKeyCredentialRequestOptions { return new PublicKeyCredentialRequestOptions( random_bytes($this->challengeSize), $this->timeout, $this->rpEntity->getId(), $allowedPublicKeyDescriptors, $userVerification, $extensions ?? new AuthenticationExtensionsClientInputs() ); } public function loadAndCheckAttestationResponse(string $data, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $serverRequest): PublicKeyCredentialSource { $attestationStatementSupportManager = $this->getAttestationStatementSupportManager(); $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager); $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader); $publicKeyCredential = $publicKeyCredentialLoader->load($data); $authenticatorResponse = $publicKeyCredential->getResponse(); Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAttestationResponse::class, 'Not an authenticator attestation response'); $authenticatorAttestationResponseValidator = new AuthenticatorAttestationResponseValidator( $attestationStatementSupportManager, $this->publicKeyCredentialSourceRepository, $this->tokenBindingHandler, $this->extensionOutputCheckerHandler ); return $authenticatorAttestationResponseValidator->check($authenticatorResponse, $publicKeyCredentialCreationOptions, $serverRequest); } public function loadAndCheckAssertionResponse(string $data, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ?PublicKeyCredentialUserEntity $userEntity, ServerRequestInterface $serverRequest): PublicKeyCredentialSource { $attestationStatementSupportManager = $this->getAttestationStatementSupportManager(); $attestationObjectLoader = new AttestationObjectLoader($attestationStatementSupportManager); $publicKeyCredentialLoader = new PublicKeyCredentialLoader($attestationObjectLoader); $publicKeyCredential = $publicKeyCredentialLoader->load($data); $authenticatorResponse = $publicKeyCredential->getResponse(); Assertion::isInstanceOf($authenticatorResponse, AuthenticatorAssertionResponse::class, 'Not an authenticator assertion response'); $authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator( $this->publicKeyCredentialSourceRepository, null, $this->tokenBindingHandler, $this->extensionOutputCheckerHandler, $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms) ); return $authenticatorAssertionResponseValidator->check( $publicKeyCredential->getRawId(), $authenticatorResponse, $publicKeyCredentialRequestOptions, $serverRequest, null !== $userEntity ? $userEntity->getId() : null ); } public function enforceAndroidSafetyNetVerification(ClientInterface $client, string $apiKey, RequestFactoryInterface $requestFactory): void { $this->httpClient = $client; $this->googleApiKey = $apiKey; $this->requestFactory = $requestFactory; } private function getAttestationStatementSupportManager(): AttestationStatementSupportManager { $attestationStatementSupportManager = new AttestationStatementSupportManager(); $attestationStatementSupportManager->add(new NoneAttestationStatementSupport()); if (null !== $this->metadataStatementRepository) { $coseAlgorithmManager = $this->coseAlgorithmManagerFactory->create($this->selectedAlgorithms); $attestationStatementSupportManager->add(new FidoU2FAttestationStatementSupport(null, $this->metadataStatementRepository)); $attestationStatementSupportManager->add(new AndroidSafetyNetAttestationStatementSupport($this->httpClient, $this->googleApiKey, $this->requestFactory, 2000, 60000, $this->metadataStatementRepository)); $attestationStatementSupportManager->add(new AndroidKeyAttestationStatementSupport(null, $this->metadataStatementRepository)); $attestationStatementSupportManager->add(new TPMAttestationStatementSupport($this->metadataStatementRepository)); $attestationStatementSupportManager->add(new PackedAttestationStatementSupport(null, $coseAlgorithmManager, $this->metadataStatementRepository)); } return $attestationStatementSupportManager; } } src/Credential.php 0000644 00000001365 15173222011 0010115 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn; /** * @see https://w3c.github.io/webappsec-credential-management/#credential */ abstract class Credential { /** * @var string */ protected $id; /** * @var string */ protected $type; public function __construct(string $id, string $type) { $this->id = $id; $this->type = $type; } public function getId(): string { return $this->id; } public function getType(): string { return $this->type; } } src/StringStream.php 0000644 00000003226 15173222011 0010463 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use CBOR\Stream; final class StringStream implements Stream { /** * @var resource */ private $data; /** * @var int */ private $length; /** * @var int */ private $totalRead = 0; public function __construct(string $data) { $this->length = mb_strlen($data, '8bit'); $resource = fopen('php://memory', 'rb+'); Assertion::isResource($resource, 'Unable to open memory'); $result = fwrite($resource, $data); Assertion::integer($result, 'Unable to write memory'); $result = rewind($resource); Assertion::true($result, 'Unable to read memory'); $this->data = $resource; } public function read(int $length): string { if (0 === $length) { return ''; } $read = fread($this->data, $length); Assertion::string($read, 'Unable to read memory'); $bytesRead = mb_strlen($read, '8bit'); Assertion::length($read, $length, sprintf('Out of range. Expected: %d, read: %d.', $length, $bytesRead), null, '8bit'); $this->totalRead += $bytesRead; return $read; } public function close(): void { $result = fclose($this->data); Assertion::true($result, 'Unable to close the memory'); } public function isEOF(): bool { return $this->totalRead === $this->length; } } src/PublicKeyCredentialRpEntity.php 0000644 00000002167 15173222011 0013425 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; class PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity { /** * @var string|null */ protected $id; public function __construct(string $name, ?string $id = null, ?string $icon = null) { parent::__construct($name, $icon); $this->id = $id; } public function getId(): ?string { return $this->id; } public static function createFromArray(array $json): self { Assertion::keyExists($json, 'name', 'Invalid input. "name" is missing.'); return new self( $json['name'], $json['id'] ?? null, $json['icon'] ?? null ); } public function jsonSerialize(): array { $json = parent::jsonSerialize(); if (null !== $this->id) { $json['id'] = $this->id; } return $json; } } src/PublicKeyCredentialOptions.php 0000644 00000002556 15173222011 0013304 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 JsonSerializable; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; abstract class PublicKeyCredentialOptions implements JsonSerializable { /** * @var string */ protected $challenge; /** * @var int|null */ protected $timeout; /** * @var AuthenticationExtensionsClientInputs */ protected $extensions; public function __construct(string $challenge, ?int $timeout = null, ?AuthenticationExtensionsClientInputs $extensions = null) { $this->challenge = $challenge; $this->timeout = $timeout; $this->extensions = $extensions ?? new AuthenticationExtensionsClientInputs(); } public function getChallenge(): string { return $this->challenge; } public function getTimeout(): ?int { return $this->timeout; } public function getExtensions(): AuthenticationExtensionsClientInputs { return $this->extensions; } abstract public static function createFromString(string $data): self; abstract public static function createFromArray(array $json): self; } src/PublicKeyCredentialCreationOptions.php 0000644 00000013257 15173222011 0014771 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Base64Url\Base64Url; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; class PublicKeyCredentialCreationOptions extends PublicKeyCredentialOptions { public const ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 'none'; public const ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 'indirect'; public const ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 'direct'; /** * @var PublicKeyCredentialRpEntity */ private $rp; /** * @var PublicKeyCredentialUserEntity */ private $user; /** * @var PublicKeyCredentialParameters[] */ private $pubKeyCredParams; /** * @var PublicKeyCredentialDescriptor[] */ private $excludeCredentials; /** * @var AuthenticatorSelectionCriteria */ private $authenticatorSelection; /** * @var string */ private $attestation; /** * PublicKeyCredentialCreationOptions constructor. * * @param PublicKeyCredentialParameters[] $pubKeyCredParams * @param PublicKeyCredentialDescriptor[] $excludeCredentials */ public function __construct(PublicKeyCredentialRpEntity $rp, PublicKeyCredentialUserEntity $user, string $challenge, array $pubKeyCredParams, ?int $timeout, array $excludeCredentials, AuthenticatorSelectionCriteria $authenticatorSelection, string $attestation, ?AuthenticationExtensionsClientInputs $extensions) { parent::__construct($challenge, $timeout, $extensions); $this->rp = $rp; $this->user = $user; $this->pubKeyCredParams = array_values($pubKeyCredParams); $this->excludeCredentials = array_values($excludeCredentials); $this->authenticatorSelection = $authenticatorSelection; $this->attestation = $attestation; } public function getRp(): PublicKeyCredentialRpEntity { return $this->rp; } public function getUser(): PublicKeyCredentialUserEntity { return $this->user; } /** * @return PublicKeyCredentialParameters[] */ public function getPubKeyCredParams(): array { return $this->pubKeyCredParams; } /** * @return PublicKeyCredentialDescriptor[] */ public function getExcludeCredentials(): array { return $this->excludeCredentials; } public function getAuthenticatorSelection(): AuthenticatorSelectionCriteria { return $this->authenticatorSelection; } public function getAttestation(): string { return $this->attestation; } public static function createFromString(string $data): PublicKeyCredentialOptions { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): PublicKeyCredentialOptions { Assertion::keyExists($json, 'rp', 'Invalid input. "rp" is missing.'); Assertion::keyExists($json, 'pubKeyCredParams', 'Invalid input. "pubKeyCredParams" is missing.'); Assertion::isArray($json['pubKeyCredParams'], 'Invalid input. "pubKeyCredParams" is not an array.'); Assertion::keyExists($json, 'challenge', 'Invalid input. "challenge" is missing.'); Assertion::keyExists($json, 'attestation', 'Invalid input. "attestation" is missing.'); Assertion::keyExists($json, 'user', 'Invalid input. "user" is missing.'); Assertion::keyExists($json, 'authenticatorSelection', 'Invalid input. "authenticatorSelection" is missing.'); $pubKeyCredParams = []; foreach ($json['pubKeyCredParams'] as $pubKeyCredParam) { $pubKeyCredParams[] = PublicKeyCredentialParameters::createFromArray($pubKeyCredParam); } $excludeCredentials = []; if (isset($json['excludeCredentials'])) { foreach ($json['excludeCredentials'] as $excludeCredential) { $excludeCredentials[] = PublicKeyCredentialDescriptor::createFromArray($excludeCredential); } } return new self( PublicKeyCredentialRpEntity::createFromArray($json['rp']), PublicKeyCredentialUserEntity::createFromArray($json['user']), Base64Url::decode($json['challenge']), $pubKeyCredParams, $json['timeout'] ?? null, $excludeCredentials, AuthenticatorSelectionCriteria::createFromArray($json['authenticatorSelection']), $json['attestation'], isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray($json['extensions']) : new AuthenticationExtensionsClientInputs() ); } public function jsonSerialize(): array { $json = [ 'rp' => $this->rp, 'pubKeyCredParams' => $this->pubKeyCredParams, 'challenge' => Base64Url::encode($this->challenge), 'attestation' => $this->attestation, 'user' => $this->user, 'authenticatorSelection' => $this->authenticatorSelection, ]; if (0 !== \count($this->excludeCredentials)) { $json['excludeCredentials'] = $this->excludeCredentials; } if (0 !== $this->extensions->count()) { $json['extensions'] = $this->extensions; } if (null !== $this->timeout) { $json['timeout'] = $this->timeout; } return $json; } } src/PublicKeyCredentialEntity.php 0000644 00000001736 15173222011 0013124 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 JsonSerializable; abstract class PublicKeyCredentialEntity implements JsonSerializable { /** * @var string */ protected $name; /** * @var string|null */ protected $icon; public function __construct(string $name, ?string $icon) { $this->name = $name; $this->icon = $icon; } public function getName(): string { return $this->name; } public function getIcon(): ?string { return $this->icon; } public function jsonSerialize(): array { $json = [ 'name' => $this->name, ]; if (null !== $this->icon) { $json['icon'] = $this->icon; } return $json; } } src/AuthenticatorAssertionResponse.php 0000644 00000002742 15173222011 0014264 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; /** * @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse */ class AuthenticatorAssertionResponse extends AuthenticatorResponse { /** * @var AuthenticatorData */ private $authenticatorData; /** * @var string */ private $signature; /** * @var string|null */ private $userHandle; public function __construct(CollectedClientData $clientDataJSON, AuthenticatorData $authenticatorData, string $signature, ?string $userHandle) { parent::__construct($clientDataJSON); $this->authenticatorData = $authenticatorData; $this->signature = $signature; $this->userHandle = $userHandle; } public function getAuthenticatorData(): AuthenticatorData { return $this->authenticatorData; } public function getSignature(): string { return $this->signature; } public function getUserHandle(): ?string { if (null === $this->userHandle || '' === $this->userHandle) { return $this->userHandle; } $decoded = base64_decode($this->userHandle, true); Assertion::string($decoded, 'Unable to decode the data'); return $decoded; } } src/PublicKeyCredentialParameters.php 0000644 00000003231 15173222011 0013743 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use JsonSerializable; class PublicKeyCredentialParameters implements JsonSerializable { /** * @var string */ private $type; /** * @var int */ private $alg; public function __construct(string $type, int $alg) { $this->type = $type; $this->alg = $alg; } public function getType(): string { return $this->type; } public function getAlg(): int { return $this->alg; } public static function createFromString(string $data): self { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): self { Assertion::keyExists($json, 'type', 'Invalid input. "type" is missing.'); Assertion::string($json['type'], 'Invalid input. "type" is not a string.'); Assertion::keyExists($json, 'alg', 'Invalid input. "alg" is missing.'); Assertion::integer($json['alg'], 'Invalid input. "alg" is not an integer.'); return new self( $json['type'], $json['alg'] ); } public function jsonSerialize(): array { return [ 'type' => $this->type, 'alg' => $this->alg, ]; } } src/AuthenticatorResponse.php 0000644 00000001256 15173222011 0012373 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn; /** * @see https://www.w3.org/TR/webauthn/#authenticatorresponse */ abstract class AuthenticatorResponse { /** * @var CollectedClientData */ private $clientDataJSON; public function __construct(CollectedClientData $clientDataJSON) { $this->clientDataJSON = $clientDataJSON; } public function getClientDataJSON(): CollectedClientData { return $this->clientDataJSON; } } src/PublicKeyCredentialSource.php 0000644 00000015205 15173222011 0013104 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Base64Url\Base64Url; use InvalidArgumentException; use JsonSerializable; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Throwable; use Webauthn\TrustPath\TrustPath; use Webauthn\TrustPath\TrustPathLoader; /** * @see https://www.w3.org/TR/webauthn/#iface-pkcredential */ class PublicKeyCredentialSource implements JsonSerializable { /** * @var string */ protected $publicKeyCredentialId; /** * @var string */ protected $type; /** * @var string[] */ protected $transports; /** * @var string */ protected $attestationType; /** * @var TrustPath */ protected $trustPath; /** * @var UuidInterface */ protected $aaguid; /** * @var string */ protected $credentialPublicKey; /** * @var string */ protected $userHandle; /** * @var int */ protected $counter; public function __construct(string $publicKeyCredentialId, string $type, array $transports, string $attestationType, TrustPath $trustPath, UuidInterface $aaguid, string $credentialPublicKey, string $userHandle, int $counter) { $this->publicKeyCredentialId = $publicKeyCredentialId; $this->type = $type; $this->transports = $transports; $this->aaguid = $aaguid; $this->credentialPublicKey = $credentialPublicKey; $this->userHandle = $userHandle; $this->counter = $counter; $this->attestationType = $attestationType; $this->trustPath = $trustPath; } /** * @deprecated Deprecated since v2.1. Will be removed in v3.0. Please use response from the credential source returned by the AuthenticatorAttestationResponseValidator after "check" method */ public static function createFromPublicKeyCredential(PublicKeyCredential $publicKeyCredential, string $userHandle): self { $response = $publicKeyCredential->getResponse(); Assertion::isInstanceOf($response, AuthenticatorAttestationResponse::class, 'This method is only available with public key credential containing an authenticator attestation response.'); $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor(); $attestationStatement = $response->getAttestationObject()->getAttStmt(); $authenticatorData = $response->getAttestationObject()->getAuthData(); $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); Assertion::notNull($attestedCredentialData, 'No attested credential data available'); return new self( $publicKeyCredentialDescriptor->getId(), $publicKeyCredentialDescriptor->getType(), $publicKeyCredentialDescriptor->getTransports(), $attestationStatement->getType(), $attestationStatement->getTrustPath(), $attestedCredentialData->getAaguid(), $attestedCredentialData->getCredentialPublicKey(), $userHandle, $authenticatorData->getSignCount() ); } public function getPublicKeyCredentialId(): string { return $this->publicKeyCredentialId; } public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor { return new PublicKeyCredentialDescriptor( $this->type, $this->publicKeyCredentialId, $this->transports ); } public function getAttestationType(): string { return $this->attestationType; } public function getTrustPath(): TrustPath { return $this->trustPath; } public function getAttestedCredentialData(): AttestedCredentialData { return new AttestedCredentialData( $this->aaguid, $this->publicKeyCredentialId, $this->credentialPublicKey ); } public function getType(): string { return $this->type; } /** * @return string[] */ public function getTransports(): array { return $this->transports; } public function getAaguid(): UuidInterface { return $this->aaguid; } public function getCredentialPublicKey(): string { return $this->credentialPublicKey; } public function getUserHandle(): string { return $this->userHandle; } public function getCounter(): int { return $this->counter; } public function setCounter(int $counter): void { $this->counter = $counter; } public static function createFromArray(array $data): self { $keys = array_keys(get_class_vars(self::class)); foreach ($keys as $key) { Assertion::keyExists($data, $key, sprintf('The parameter "%s" is missing', $key)); } switch (true) { case 36 === mb_strlen($data['aaguid'], '8bit'): $uuid = Uuid::fromString($data['aaguid']); break; default: // Kept for compatibility with old format $decoded = base64_decode($data['aaguid'], true); Assertion::string($decoded, 'Invalid AAGUID'); $uuid = Uuid::fromBytes($decoded); } try { return new self( Base64Url::decode($data['publicKeyCredentialId']), $data['type'], $data['transports'], $data['attestationType'], TrustPathLoader::loadTrustPath($data['trustPath']), $uuid, Base64Url::decode($data['credentialPublicKey']), Base64Url::decode($data['userHandle']), $data['counter'] ); } catch (Throwable $throwable) { throw new InvalidArgumentException('Unable to load the data', $throwable->getCode(), $throwable); } } public function jsonSerialize(): array { return [ 'publicKeyCredentialId' => Base64Url::encode($this->publicKeyCredentialId), 'type' => $this->type, 'transports' => $this->transports, 'attestationType' => $this->attestationType, 'trustPath' => $this->trustPath, 'aaguid' => $this->aaguid->toString(), 'credentialPublicKey' => Base64Url::encode($this->credentialPublicKey), 'userHandle' => Base64Url::encode($this->userHandle), 'counter' => $this->counter, ]; } } src/CertificateToolbox.php 0000644 00000020321 15173222011 0011625 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use InvalidArgumentException; use Symfony\Component\Process\Process; use Webauthn\AttestationStatement\AttestationStatement; use Webauthn\MetadataService\MetadataStatement; use Webauthn\MetadataService\MetadataStatementRepository; class CertificateToolbox { public static function checkChain(array $certificates, array $trustedCertificates = []): void { $certificates = array_unique(array_merge($certificates, $trustedCertificates)); if (0 === \count($certificates)) { return; } self::checkCertificatesValidity($certificates); $filenames = []; $leafFilename = tempnam(sys_get_temp_dir(), 'webauthn-leaf-'); Assertion::string($leafFilename, 'Unable to get a temporary filename'); $leafCertificate = array_shift($certificates); $result = file_put_contents($leafFilename, $leafCertificate); Assertion::integer($result, 'Unable to write temporary data'); $filenames[] = $leafFilename; $processArguments = []; if (0 !== \count($certificates)) { $caFilename = tempnam(sys_get_temp_dir(), 'webauthn-ca-'); Assertion::string($caFilename, 'Unable to get a temporary filename'); $caCertificate = array_pop($certificates); $result = file_put_contents($caFilename, $caCertificate); Assertion::integer($result, 'Unable to write temporary data'); $processArguments[] = '-CAfile'; $processArguments[] = $caFilename; $filenames[] = $caFilename; } if (0 !== \count($certificates)) { $untrustedFilename = tempnam(sys_get_temp_dir(), 'webauthn-untrusted-'); Assertion::string($untrustedFilename, 'Unable to get a temporary filename'); foreach ($certificates as $certificate) { $result = file_put_contents($untrustedFilename, $certificate, FILE_APPEND); Assertion::integer($result, 'Unable to write temporary data'); $result = file_put_contents($untrustedFilename, PHP_EOL, FILE_APPEND); Assertion::integer($result, 'Unable to write temporary data'); } $processArguments[] = '-untrusted'; $processArguments[] = $untrustedFilename; $filenames[] = $untrustedFilename; } $processArguments[] = $leafFilename; array_unshift($processArguments, 'openssl', 'verify'); $process = new Process($processArguments); $process->start(); while ($process->isRunning()) { } foreach ($filenames as $filename) { $result = unlink($filename); Assertion::true($result, 'Unable to delete temporary file'); } if (!$process->isSuccessful()) { throw new InvalidArgumentException('Invalid certificate or certificate chain. Error is: '.$process->getErrorOutput()); } } public static function checkAttestationMedata(AttestationStatement $attestationStatement, string $aaguid, array $certificates, MetadataStatementRepository $metadataStatementRepository): array { $metadataStatement = $metadataStatementRepository->findOneByAAGUID($aaguid); if (null === $metadataStatement) { //Check certificate CA chain self::checkChain($certificates); return $certificates; } //FIXME: to decide later if relevant /*Assertion::eq('fido2', $metadataStatement->getProtocolFamily(), sprintf('The protocol family of the authenticator "%s" should be "fido2". Got "%s".', $aaguid, $metadataStatement->getProtocolFamily())); if (null !== $metadataStatement->getAssertionScheme()) { Assertion::eq('FIDOV2', $metadataStatement->getAssertionScheme(), sprintf('The assertion scheme of the authenticator "%s" should be "FIDOV2". Got "%s".', $aaguid, $metadataStatement->getAssertionScheme())); }*/ // Check Attestation Type is allowed if (0 !== \count($metadataStatement->getAttestationTypes())) { $type = self::getAttestationType($attestationStatement); Assertion::inArray($type, $metadataStatement->getAttestationTypes(), 'Invalid attestation statement. The attestation type is not allowed for this authenticator'); } $attestationRootCertificates = $metadataStatement->getAttestationRootCertificates(); if (0 === \count($attestationRootCertificates)) { self::checkChain($certificates); return $certificates; } foreach ($attestationRootCertificates as $key => $attestationRootCertificate) { $attestationRootCertificates[$key] = self::fixPEMStructure($attestationRootCertificate); } //Check certificate CA chain self::checkChain($certificates, $attestationRootCertificates); return $certificates; } private static function getAttestationType(AttestationStatement $attestationStatement): int { switch ($attestationStatement->getType()) { case AttestationStatement::TYPE_BASIC: return MetadataStatement::ATTESTATION_BASIC_FULL; case AttestationStatement::TYPE_SELF: return MetadataStatement::ATTESTATION_BASIC_SURROGATE; case AttestationStatement::TYPE_ATTCA: return MetadataStatement::ATTESTATION_ATTCA; case AttestationStatement::TYPE_ECDAA: return MetadataStatement::ATTESTATION_ECDAA; default: throw new InvalidArgumentException('Invalid attestation type'); } } public static function fixPEMStructure(string $certificate): string { $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL; $pemCert .= chunk_split($certificate, 64, PHP_EOL); $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL; return $pemCert; } public static function convertDERToPEM(string $certificate): string { $derCertificate = self::unusedBytesFix($certificate); return self::fixPEMStructure(base64_encode($derCertificate)); } public static function convertAllDERToPEM(array $certificates): array { $certs = []; foreach ($certificates as $publicKey) { $certs[] = self::convertDERToPEM($publicKey); } return $certs; } private static function unusedBytesFix(string $certificate): string { $certificateHash = hash('sha256', $certificate); if (\in_array($certificateHash, self::getCertificateHashes(), true)) { $certificate[mb_strlen($certificate, '8bit') - 257] = "\0"; } return $certificate; } /** * @param string[] $certificates */ private static function checkCertificatesValidity(array $certificates): void { foreach ($certificates as $certificate) { $parsed = openssl_x509_parse($certificate); Assertion::isArray($parsed, 'Unable to read the certificate'); Assertion::keyExists($parsed, 'validTo_time_t', 'The certificate has no validity period'); Assertion::keyExists($parsed, 'validFrom_time_t', 'The certificate has no validity period'); Assertion::lessOrEqualThan(time(), $parsed['validTo_time_t'], 'The certificate expired'); Assertion::greaterOrEqualThan(time(), $parsed['validFrom_time_t'], 'The certificate is not usable yet'); } } /** * @return string[] */ private static function getCertificateHashes(): array { return [ '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8', 'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f', '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae', 'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb', '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897', 'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511', ]; } } src/PublicKeyCredentialDescriptorCollection.php 0000644 00000004040 15173222011 0015771 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 ArrayIterator; use Assert\Assertion; use Countable; use Iterator; use IteratorAggregate; use JsonSerializable; class PublicKeyCredentialDescriptorCollection implements JsonSerializable, Countable, IteratorAggregate { /** * @var PublicKeyCredentialDescriptor[] */ private $publicKeyCredentialDescriptors = []; public function add(PublicKeyCredentialDescriptor $publicKeyCredentialDescriptor): void { $this->publicKeyCredentialDescriptors[$publicKeyCredentialDescriptor->getId()] = $publicKeyCredentialDescriptor; } public function has(string $id): bool { return \array_key_exists($id, $this->publicKeyCredentialDescriptors); } public function remove(string $id): void { if (!$this->has($id)) { return; } unset($this->publicKeyCredentialDescriptors[$id]); } public function getIterator(): Iterator { return new ArrayIterator($this->publicKeyCredentialDescriptors); } public function count(int $mode = COUNT_NORMAL): int { return \count($this->publicKeyCredentialDescriptors, $mode); } public function jsonSerialize(): array { return array_values($this->publicKeyCredentialDescriptors); } public static function createFromString(string $data): self { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): self { $collection = new self(); foreach ($json as $item) { $collection->add(PublicKeyCredentialDescriptor::createFromArray($item)); } return $collection; } } src/AuthenticatorAssertionResponseValidator.php 0000644 00000022034 15173222011 0016126 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use CBOR\Decoder; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Algorithm\Manager; use Cose\Algorithm\Signature\Signature; use Cose\Key\Key; use Psr\Http\Message\ServerRequestInterface; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\TokenBinding\TokenBindingHandler; use Webauthn\Util\CoseSignatureFixer; class AuthenticatorAssertionResponseValidator { /** * @var PublicKeyCredentialSourceRepository */ private $publicKeyCredentialSourceRepository; /** * @var Decoder */ private $decoder; /** * @var TokenBindingHandler */ private $tokenBindingHandler; /** * @var ExtensionOutputCheckerHandler */ private $extensionOutputCheckerHandler; /** * @var Manager|null */ private $algorithmManager; public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, ?Decoder $decoder, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, Manager $algorithmManager) { if (null !== $decoder) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } $this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository; $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->tokenBindingHandler = $tokenBindingHandler; $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; $this->algorithmManager = $algorithmManager; } /** * @see https://www.w3.org/TR/webauthn/#verifying-assertion */ public function check(string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface $request, ?string $userHandle): PublicKeyCredentialSource { /* @see 7.2.1 */ if (0 !== \count($publicKeyCredentialRequestOptions->getAllowCredentials())) { Assertion::true($this->isCredentialIdAllowed($credentialId, $publicKeyCredentialRequestOptions->getAllowCredentials()), 'The credential ID is not allowed.'); } /* @see 7.2.2 */ $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($credentialId); Assertion::notNull($publicKeyCredentialSource, 'The credential ID is invalid.'); /* @see 7.2.3 */ $attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData(); $credentialUserHandle = $publicKeyCredentialSource->getUserHandle(); $responseUserHandle = $authenticatorAssertionResponse->getUserHandle(); /* @see 7.2.2 User Handle*/ if (null !== $userHandle) { //If the user was identified before the authentication ceremony was initiated, Assertion::eq($credentialUserHandle, $userHandle, 'Invalid user handle'); if (null !== $responseUserHandle && '' !== $responseUserHandle) { Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle'); } } else { Assertion::notEmpty($responseUserHandle, 'User handle is mandatory'); Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle'); } $credentialPublicKey = $attestedCredentialData->getCredentialPublicKey(); Assertion::notNull($credentialPublicKey, 'No public key available.'); $stream = new StringStream($credentialPublicKey); $credentialPublicKeyStream = $this->decoder->decode($stream); Assertion::true($stream->isEOF(), 'Invalid key. Presence of extra bytes.'); $stream->close(); /** @see 7.2.4 */ /** @see 7.2.5 */ //Nothing to do. Use of objects directly /** @see 7.2.6 */ $C = $authenticatorAssertionResponse->getClientDataJSON(); /* @see 7.2.7 */ Assertion::eq('webauthn.get', $C->getType(), 'The client data type is not "webauthn.get".'); /* @see 7.2.8 */ Assertion::true(hash_equals($publicKeyCredentialRequestOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.'); /** @see 7.2.9 */ $rpId = $publicKeyCredentialRequestOptions->getRpId() ?? $request->getUri()->getHost(); $rpIdLength = mb_strlen($rpId); $parsedRelyingPartyId = parse_url($C->getOrigin()); Assertion::isArray($parsedRelyingPartyId, 'Invalid origin'); $scheme = $parsedRelyingPartyId['scheme'] ?? ''; Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.'); $clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.'); Assertion::eq(mb_substr($clientDataRpId, -$rpIdLength), $rpId, 'rpId mismatch.'); /* @see 7.2.10 */ if (null !== $C->getTokenBinding()) { $this->tokenBindingHandler->check($C->getTokenBinding(), $request); } /** @see 7.2.11 */ $facetId = $this->getFacetId($rpId, $publicKeyCredentialRequestOptions->getExtensions(), $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions()); $rpIdHash = hash('sha256', $rpId, true); Assertion::true(hash_equals($rpIdHash, $authenticatorAssertionResponse->getAuthenticatorData()->getRpIdHash()), 'rpId hash mismatch.'); /* @see 7.2.12 */ /* @see 7.2.13 */ if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialRequestOptions->getUserVerification()) { Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserPresent(), 'User was not present'); Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserVerified(), 'User authentication required.'); } /* @see 7.2.14 */ $extensions = $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions(); if (null !== $extensions) { $this->extensionOutputCheckerHandler->check($extensions); } /** @see 7.2.15 */ $getClientDataJSONHash = hash('sha256', $authenticatorAssertionResponse->getClientDataJSON()->getRawData(), true); /* @see 7.2.16 */ $dataToVerify = $authenticatorAssertionResponse->getAuthenticatorData()->getAuthData().$getClientDataJSONHash; $signature = $authenticatorAssertionResponse->getSignature(); $coseKey = new Key($credentialPublicKeyStream->getNormalizedData()); $algorithm = $this->algorithmManager->get($coseKey->alg()); Assertion::isInstanceOf($algorithm, Signature::class, 'Invalid algorithm identifier. Should refer to a signature algorithm'); $signature = CoseSignatureFixer::fix($signature, $algorithm); Assertion::true($algorithm->verify($dataToVerify, $coseKey, $signature), 'Invalid signature.'); /* @see 7.2.17 */ $storedCounter = $publicKeyCredentialSource->getCounter(); $currentCounter = $authenticatorAssertionResponse->getAuthenticatorData()->getSignCount(); if (0 !== $currentCounter || 0 !== $storedCounter) { Assertion::greaterThan($currentCounter, $storedCounter, 'Invalid counter.'); } $publicKeyCredentialSource->setCounter($currentCounter); $this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); /* @see 7.2.18 */ //All good. We can continue. return $publicKeyCredentialSource; } private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool { foreach ($allowedCredentials as $allowedCredential) { if (hash_equals($allowedCredential->getId(), $credentialId)) { return true; } } return false; } private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string { switch (true) { case !$authenticationExtensionsClientInputs->has('appid'): return $rpId; case null === $authenticationExtensionsClientOutputs: return $rpId; case !$authenticationExtensionsClientOutputs->has('appid'): return $rpId; case true !== $authenticationExtensionsClientOutputs->get('appid'): return $rpId; default: return $authenticationExtensionsClientInputs->get('appid'); } } } src/AttestationStatement/AndroidKeyAttestationStatementSupport.php 0000644 00000017435 15173222011 0021767 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use CBOR\Decoder; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Algorithms; use Cose\Key\Ec2Key; use Cose\Key\Key; use Cose\Key\RsaKey; use FG\ASN1\ASNObject; use FG\ASN1\ExplicitlyTaggedObject; use FG\ASN1\Universal\OctetString; use FG\ASN1\Universal\Sequence; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\StringStream; use Webauthn\TrustPath\CertificateTrustPath; final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport { /** * @var Decoder */ private $decoder; /** * @var MetadataStatementRepository|null */ private $metadataStatementRepository; public function __construct(?Decoder $decoder = null, ?MetadataStatementRepository $metadataStatementRepository = null) { if (null !== $decoder) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } if (null === $metadataStatementRepository) { @trigger_error('Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', E_USER_DEPRECATED); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->metadataStatementRepository = $metadataStatementRepository; } public function name(): string { return 'android-key'; } public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); foreach (['sig', 'x5c', 'alg'] as $key) { Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); } $certificates = $attestation['attStmt']['x5c']; Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); Assertion::greaterThan(\count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.'); Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); $certificates = CertificateToolbox::convertAllDERToPEM($certificates); return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); } public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { $trustPath = $attestationStatement->getTrustPath(); Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); $certificates = $trustPath->getCertificates(); if (null !== $this->metadataStatementRepository) { $certificates = CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), $certificates, $this->metadataStatementRepository ); } //Decode leaf attestation certificate $leaf = $certificates[0]; $this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData); $signedData = $authenticatorData->getAuthData().$clientDataJSONHash; $alg = $attestationStatement->get('alg'); return 1 === openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg)); } private function checkCertificateAndGetPublicKey(string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData): void { $resource = openssl_pkey_get_public($certificate); Assertion::isResource($resource, 'Unable to read the certificate'); $details = openssl_pkey_get_details($resource); Assertion::isArray($details, 'Unable to read the certificate'); //Check that authData publicKey matches the public key in the attestation certificate $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); Assertion::notNull($attestedCredentialData, 'No attested credential data found'); $publicKeyData = $attestedCredentialData->getCredentialPublicKey(); Assertion::notNull($publicKeyData, 'No attested public key found'); $publicDataStream = new StringStream($publicKeyData); $coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false); Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.'); $publicDataStream->close(); $publicKey = Key::createFromData($coseKey); Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type'); Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key'); /*---------------------------*/ $certDetails = openssl_x509_parse($certificate); //Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension'); Assertion::isArray($certDetails['extensions'], 'The certificate has no extension'); Assertion::keyExists($certDetails['extensions'], '1.3.6.1.4.1.11129.2.1.17', 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing'); $extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17']; $extensionAsAsn1 = ASNObject::fromBinary($extension); Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $objects = $extensionAsAsn1->getChildren(); //Check that attestationChallenge is set to the clientDataHash. Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid'); //Check that both teeEnforced and softwareEnforced structures don’t contain allApplications(600) tag. Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $softwareEnforcedFlags = $objects[6]; Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags); Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $teeEnforcedFlags = $objects[6]; Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'); $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags); } private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void { foreach ($sequence->getChildren() as $tag) { Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag'); /* @var ExplicitlyTaggedObject $tag */ Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found'); } } } src/AttestationStatement/AttestationObjectLoader.php 0000644 00000007637 15173222011 0017014 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use Base64Url\Base64Url; use CBOR\Decoder; use CBOR\MapObject; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Ramsey\Uuid\Uuid; use Webauthn\AttestedCredentialData; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader; use Webauthn\AuthenticatorData; use Webauthn\StringStream; class AttestationObjectLoader { private const FLAG_AT = 0b01000000; private const FLAG_ED = 0b10000000; /** * @var Decoder */ private $decoder; /** * @var AttestationStatementSupportManager */ private $attestationStatementSupportManager; public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, ?Decoder $decoder = null) { if (null !== $decoder) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->attestationStatementSupportManager = $attestationStatementSupportManager; } public function load(string $data): AttestationObject { $decodedData = Base64Url::decode($data); $stream = new StringStream($decodedData); $parsed = $this->decoder->decode($stream); $attestationObject = $parsed->getNormalizedData(); Assertion::true($stream->isEOF(), 'Invalid attestation object. Presence of extra bytes.'); $stream->close(); Assertion::isArray($attestationObject, 'Invalid attestation object'); Assertion::keyExists($attestationObject, 'authData', 'Invalid attestation object'); Assertion::keyExists($attestationObject, 'fmt', 'Invalid attestation object'); Assertion::keyExists($attestationObject, 'attStmt', 'Invalid attestation object'); $authData = $attestationObject['authData']; $attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']); $attestationStatement = $attestationStatementSupport->load($attestationObject); $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 AttestationObject($data, $attestationStatement, $authenticatorData); } } src/AttestationStatement/AttestationStatementSupport.php 0000644 00000001132 15173222011 0020000 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Webauthn\AuthenticatorData; interface AttestationStatementSupport { public function name(): string; public function load(array $attestation): AttestationStatement; public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool; } src/AttestationStatement/AttestationStatement.php 0000644 00000006422 15173222011 0016412 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use JsonSerializable; use Webauthn\TrustPath\TrustPath; use Webauthn\TrustPath\TrustPathLoader; class AttestationStatement implements JsonSerializable { public const TYPE_NONE = 'none'; public const TYPE_BASIC = 'basic'; public const TYPE_SELF = 'self'; public const TYPE_ATTCA = 'attca'; public const TYPE_ECDAA = 'ecdaa'; /** * @var string */ private $fmt; /** * @var array */ private $attStmt; /** * @var TrustPath */ private $trustPath; /** * @var string */ private $type; public function __construct(string $fmt, array $attStmt, string $type, TrustPath $trustPath) { $this->fmt = $fmt; $this->attStmt = $attStmt; $this->type = $type; $this->trustPath = $trustPath; } public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self { return new self($fmt, $attStmt, self::TYPE_NONE, $trustPath); } public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self { return new self($fmt, $attStmt, self::TYPE_BASIC, $trustPath); } public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self { return new self($fmt, $attStmt, self::TYPE_SELF, $trustPath); } public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self { return new self($fmt, $attStmt, self::TYPE_ATTCA, $trustPath); } public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self { return new self($fmt, $attStmt, self::TYPE_ECDAA, $trustPath); } public function getFmt(): string { return $this->fmt; } public function getAttStmt(): array { return $this->attStmt; } public function has(string $key): bool { return \array_key_exists($key, $this->attStmt); } /** * @return mixed */ public function get(string $key) { Assertion::true($this->has($key), sprintf('The attestation statement has no key "%s".', $key)); return $this->attStmt[$key]; } public function getTrustPath(): TrustPath { return $this->trustPath; } public function getType(): string { return $this->type; } public static function createFromArray(array $data): self { foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) { Assertion::keyExists($data, $key, sprintf('The key "%s" is missing', $key)); } return new self( $data['fmt'], $data['attStmt'], $data['type'], TrustPathLoader::loadTrustPath($data['trustPath']) ); } public function jsonSerialize(): array { return [ 'fmt' => $this->fmt, 'attStmt' => $this->attStmt, 'trustPath' => $this->trustPath, 'type' => $this->type, ]; } } src/AttestationStatement/NoneAttestationStatementSupport.php 0000644 00000002011 15173222011 0020615 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use Webauthn\AuthenticatorData; use Webauthn\TrustPath\EmptyTrustPath; final class NoneAttestationStatementSupport implements AttestationStatementSupport { public function name(): string { return 'none'; } public function load(array $attestation): AttestationStatement { Assertion::noContent($attestation['attStmt'], 'Invalid attestation object'); return AttestationStatement::createNone($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath()); } public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { return 0 === \count($attestationStatement->getAttStmt()); } } src/AttestationStatement/FidoU2FAttestationStatementSupport.php 0000644 00000013531 15173222011 0021125 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use CBOR\Decoder; use CBOR\MapObject; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Key\Ec2Key; use InvalidArgumentException; use Throwable; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\StringStream; use Webauthn\TrustPath\CertificateTrustPath; final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport { /** * @var Decoder */ private $decoder; /** * @var MetadataStatementRepository|null */ private $metadataStatementRepository; public function __construct(?Decoder $decoder = null, ?MetadataStatementRepository $metadataStatementRepository = null) { if (null !== $decoder) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } if (null === $metadataStatementRepository) { @trigger_error('Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', E_USER_DEPRECATED); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->metadataStatementRepository = $metadataStatementRepository; } public function name(): string { return 'fido-u2f'; } public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); foreach (['sig', 'x5c'] as $key) { Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); } $certificates = $attestation['attStmt']['x5c']; Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.'); Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.'); Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with one certificate.'); reset($certificates); $certificates = CertificateToolbox::convertAllDERToPEM($certificates); $this->checkCertificate($certificates[0]); return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); } public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { Assertion::eq( $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), '00000000-0000-0000-0000-000000000000', 'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"' ); if (null !== $this->metadataStatementRepository) { CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), [], $this->metadataStatementRepository ); } $trustPath = $attestationStatement->getTrustPath(); Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); $dataToVerify = "\0"; $dataToVerify .= $authenticatorData->getRpIdHash(); $dataToVerify .= $clientDataJSONHash; $dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId(); $dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()); return 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256); } private function extractPublicKey(?string $publicKey): string { Assertion::notNull($publicKey, 'The attested credential data does not contain a valid public key.'); $publicKeyStream = new StringStream($publicKey); $coseKey = $this->decoder->decode($publicKeyStream); Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.'); $publicKeyStream->close(); Assertion::isInstanceOf($coseKey, MapObject::class, 'The attested credential data does not contain a valid public key.'); $coseKey = $coseKey->getNormalizedData(); $ec2Key = new Ec2Key($coseKey + [Ec2Key::TYPE => 2, Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256]); return "\x04".$ec2Key->x().$ec2Key->y(); } private function checkCertificate(string $publicKey): void { try { $resource = openssl_pkey_get_public($publicKey); Assertion::isResource($resource, 'Unable to load the public key'); } catch (Throwable $throwable) { throw new InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable); } $details = openssl_pkey_get_details($resource); Assertion::keyExists($details, 'ec', 'Invalid certificate or certificate chain'); Assertion::keyExists($details['ec'], 'curve_name', 'Invalid certificate or certificate chain'); Assertion::eq($details['ec']['curve_name'], 'prime256v1', 'Invalid certificate or certificate chain'); Assertion::keyExists($details['ec'], 'curve_oid', 'Invalid certificate or certificate chain'); Assertion::eq($details['ec']['curve_oid'], '1.2.840.10045.3.1.7', 'Invalid certificate or certificate chain'); } } src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php 0000644 00000024171 15173222011 0023134 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use InvalidArgumentException; use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\Util\JsonConverter; use Jose\Component\KeyManagement\JWKFactory; use Jose\Component\Signature\Algorithm; use Jose\Component\Signature\JWS; use Jose\Component\Signature\JWSVerifier; use Jose\Component\Signature\Serializer\CompactSerializer; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ResponseInterface; use RuntimeException; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\TrustPath\CertificateTrustPath; final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport { /** * @var string|null */ private $apiKey; /** * @var ClientInterface|null */ private $client; /** * @var CompactSerializer */ private $jwsSerializer; /** * @var JWSVerifier|null */ private $jwsVerifier; /** * @var RequestFactoryInterface|null */ private $requestFactory; /** * @var int */ private $leeway; /** * @var int */ private $maxAge; /** * @var MetadataStatementRepository|null */ private $metadataStatementRepository; public function __construct(?ClientInterface $client = null, ?string $apiKey = null, ?RequestFactoryInterface $requestFactory = null, int $leeway = 0, int $maxAge = 60000, ?MetadataStatementRepository $metadataStatementRepository = null) { foreach ([Algorithm\RS256::class] as $algorithm) { if (!class_exists($algorithm)) { throw new RuntimeException('The algorithms RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?'); } } $this->jwsSerializer = new CompactSerializer(); $this->apiKey = $apiKey; $this->client = $client; $this->requestFactory = $requestFactory; $this->initJwsVerifier(); $this->leeway = $leeway; $this->maxAge = $maxAge; $this->metadataStatementRepository = $metadataStatementRepository; } public function name(): string { return 'android-safetynet'; } public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); foreach (['ver', 'response'] as $key) { Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); Assertion::notEmpty($attestation['attStmt'][$key], sprintf('The attestation statement value "%s" is empty.', $key)); } $jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']); $jwsHeader = $jws->getSignature(0)->getProtectedHeader(); Assertion::keyExists($jwsHeader, 'x5c', 'The response in the attestation statement must contain a "x5c" header.'); Assertion::notEmpty($jwsHeader['x5c'], 'The "x5c" parameter in the attestation statement response must contain at least one certificate.'); $certificates = $this->convertCertificatesToPem($jwsHeader['x5c']); $attestation['attStmt']['jws'] = $jws; return AttestationStatement::createBasic( $this->name(), $attestation['attStmt'], new CertificateTrustPath($certificates) ); } public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { $trustPath = $attestationStatement->getTrustPath(); Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); $certificates = $trustPath->getCertificates(); if (null !== $this->metadataStatementRepository) { $certificates = CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), $certificates, $this->metadataStatementRepository ); } $parsedCertificate = openssl_x509_parse(current($certificates)); Assertion::isArray($parsedCertificate, 'Invalid attestation object'); Assertion::keyExists($parsedCertificate, 'subject', 'Invalid attestation object'); Assertion::keyExists($parsedCertificate['subject'], 'CN', 'Invalid attestation object'); Assertion::eq($parsedCertificate['subject']['CN'], 'attest.android.com', 'Invalid attestation object'); /** @var JWS $jws */ $jws = $attestationStatement->get('jws'); $payload = $jws->getPayload(); $this->validatePayload($payload, $clientDataJSONHash, $authenticatorData); //Check the signature $this->validateSignature($jws, $trustPath); //Check against Google service $this->validateUsingGoogleApi($attestationStatement); return true; } private function validatePayload(?string $payload, string $clientDataJSONHash, AuthenticatorData $authenticatorData): void { Assertion::notNull($payload, 'Invalid attestation object'); $payload = JsonConverter::decode($payload); Assertion::isArray($payload, 'Invalid attestation object'); Assertion::keyExists($payload, 'nonce', 'Invalid attestation object. "nonce" is missing.'); Assertion::eq($payload['nonce'], base64_encode(hash('sha256', $authenticatorData->getAuthData().$clientDataJSONHash, true)), 'Invalid attestation object. Invalid nonce'); Assertion::keyExists($payload, 'ctsProfileMatch', 'Invalid attestation object. "ctsProfileMatch" is missing.'); Assertion::true($payload['ctsProfileMatch'], 'Invalid attestation object. "ctsProfileMatch" value is false.'); Assertion::keyExists($payload, 'timestampMs', 'Invalid attestation object. Timestamp is missing.'); Assertion::integer($payload['timestampMs'], 'Invalid attestation object. Timestamp shall be an integer.'); $currentTime = time() * 1000; Assertion::lessOrEqualThan($payload['timestampMs'], $currentTime + $this->leeway, sprintf('Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs'])); Assertion::lessOrEqualThan($currentTime - $payload['timestampMs'], $this->maxAge, sprintf('Invalid attestation object. Too old. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs'])); } private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void { $jwk = JWKFactory::createFromCertificate($trustPath->getCertificates()[0]); $isValid = $this->jwsVerifier->verifyWithKey($jws, $jwk, 0); Assertion::true($isValid, 'Invalid response signature'); } private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void { if (null === $this->client || null === $this->apiKey || null === $this->requestFactory) { return; } $uri = sprintf('https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s', urlencode($this->apiKey)); $requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response')); $request = $this->requestFactory->createRequest('POST', $uri); $request = $request->withHeader('content-type', 'application/json'); $request->getBody()->write($requestBody); $response = $this->client->sendRequest($request); $this->checkGoogleApiResponse($response); $responseBody = $this->getResponseBody($response); $responseBodyJson = json_decode($responseBody, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid response.'); Assertion::keyExists($responseBodyJson, 'isValidSignature', 'Invalid response.'); Assertion::boolean($responseBodyJson['isValidSignature'], 'Invalid response.'); Assertion::true($responseBodyJson['isValidSignature'], 'Invalid response.'); } private function getResponseBody(ResponseInterface $response): string { $responseBody = ''; $response->getBody()->rewind(); do { $tmp = $response->getBody()->read(1024); if ('' === $tmp) { break; } $responseBody .= $tmp; } while (true); return $responseBody; } private function checkGoogleApiResponse(ResponseInterface $response): void { Assertion::eq(200, $response->getStatusCode(), 'Request did not succeeded'); Assertion::true($response->hasHeader('content-type'), 'Unrecognized response'); foreach ($response->getHeader('content-type') as $header) { if (0 === mb_strpos($header, 'application/json')) { return; } } throw new InvalidArgumentException('Unrecognized response'); } private function convertCertificatesToPem(array $certificates): array { foreach ($certificates as $k => $v) { $certificates[$k] = CertificateToolbox::fixPEMStructure($v); } return $certificates; } private function initJwsVerifier(): void { $algorithmClasses = [ Algorithm\RS256::class, Algorithm\RS384::class, Algorithm\RS512::class, Algorithm\PS256::class, Algorithm\PS384::class, Algorithm\PS512::class, Algorithm\ES256::class, Algorithm\ES384::class, Algorithm\ES512::class, Algorithm\EdDSA::class, ]; $algorithms = []; foreach ($algorithmClasses as $key => $algorithm) { if (class_exists($algorithm)) { $algorithms[] = new $algorithm(); } } $algorithmManager = new AlgorithmManager($algorithms); $this->jwsVerifier = new JWSVerifier($algorithmManager); } } src/AttestationStatement/TPMAttestationStatementSupport.php 0000644 00000032412 15173222011 0020366 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use Base64Url\Base64Url; use CBOR\Decoder; use CBOR\MapObject; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Algorithms; use Cose\Key\Ec2Key; use Cose\Key\Key; use Cose\Key\OkpKey; use Cose\Key\RsaKey; use DateTimeImmutable; use InvalidArgumentException; use RuntimeException; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\StringStream; use Webauthn\TrustPath\CertificateTrustPath; use Webauthn\TrustPath\EcdaaKeyIdTrustPath; final class TPMAttestationStatementSupport implements AttestationStatementSupport { /** * @var MetadataStatementRepository|null */ private $metadataStatementRepository; public function name(): string { return 'tpm'; } public function __construct(?MetadataStatementRepository $metadataStatementRepository = null) { $this->metadataStatementRepository = $metadataStatementRepository; } public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object'); Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported'); foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) { Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key)); } Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object'); $certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']); Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object'); $pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']); $pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit'); $pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true); $attestedName = $pubAreaAttestedNameAlg.$pubAreaHash; Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name'); $attestation['attStmt']['parsedCertInfo'] = $certInfo; $attestation['attStmt']['parsedPubArea'] = $pubArea; $certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']); Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.'); return AttestationStatement::createAttCA( $this->name(), $attestation['attStmt'], new CertificateTrustPath($certificates) ); } public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { $attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash; $attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true); Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash'); $this->checkUniquePublicKey( $attestationStatement->get('parsedPubArea')['unique'], $authenticatorData->getAttestedCredentialData()->getCredentialPublicKey() ); switch (true) { case $attestationStatement->getTrustPath() instanceof CertificateTrustPath: return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData); case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath: return $this->processWithECDAA(); default: throw new InvalidArgumentException('Unsupported attestation statement'); } } private function checkUniquePublicKey(string $unique, string $cborPublicKey): void { $cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager()); $publicKey = $cborDecoder->decode(new StringStream($cborPublicKey)); Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key'); $key = new Key($publicKey->getNormalizedData(false)); switch ($key->type()) { case Key::TYPE_OKP: $uniqueFromKey = (new OkpKey($key->getData()))->x(); break; case Key::TYPE_EC2: $ec2Key = new Ec2Key($key->getData()); $uniqueFromKey = "\x04".$ec2Key->x().$ec2Key->y(); break; case Key::TYPE_RSA: $uniqueFromKey = (new RsaKey($key->getData()))->n(); break; default: throw new InvalidArgumentException('Invalid or unsupported key type.'); } Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value'); } private function checkCertInfo(string $data): array { $certInfo = new StringStream($data); $magic = $certInfo->read(4); Assertion::eq('ff544347', bin2hex($magic), 'Invalid attestation object'); $type = $certInfo->read(2); $qualifiedSignerLength = unpack('n', $certInfo->read(2))[1]; $qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored $extraDataLength = unpack('n', $certInfo->read(2))[1]; $extraData = $certInfo->read($extraDataLength); $clockInfo = $certInfo->read(17); //Ignore $firmwareVersion = $certInfo->read(8); $attestedNameLength = unpack('n', $certInfo->read(2))[1]; $attestedName = $certInfo->read($attestedNameLength); $attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1]; $attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore Assertion::true($certInfo->isEOF(), 'Invalid certificate information. Presence of extra bytes.'); $certInfo->close(); return [ 'magic' => $magic, 'type' => $type, 'qualifiedSigner' => $qualifiedSigner, 'extraData' => $extraData, 'clockInfo' => $clockInfo, 'firmwareVersion' => $firmwareVersion, 'attestedName' => $attestedName, 'attestedQualifiedName' => $attestedQualifiedName, ]; } private function checkPubArea(string $data): array { $pubArea = new StringStream($data); $type = $pubArea->read(2); $nameAlg = $pubArea->read(2); $objectAttributes = $pubArea->read(4); $authPolicyLength = unpack('n', $pubArea->read(2))[1]; $authPolicy = $pubArea->read($authPolicyLength); $parameters = $this->getParameters($type, $pubArea); $uniqueLength = unpack('n', $pubArea->read(2))[1]; $unique = $pubArea->read($uniqueLength); Assertion::true($pubArea->isEOF(), 'Invalid public area. Presence of extra bytes.'); $pubArea->close(); return [ 'type' => $type, 'nameAlg' => $nameAlg, 'objectAttributes' => $objectAttributes, 'authPolicy' => $authPolicy, 'parameters' => $parameters, 'unique' => $unique, ]; } private function getParameters(string $type, StringStream $stream): array { switch (bin2hex($type)) { case '0001': case '0014': case '0016': return [ 'symmetric' => $stream->read(2), 'scheme' => $stream->read(2), 'keyBits' => unpack('n', $stream->read(2))[1], 'exponent' => $this->getExponent($stream->read(4)), ]; case '0018': return [ 'symmetric' => $stream->read(2), 'scheme' => $stream->read(2), 'curveId' => $stream->read(2), 'kdf' => $stream->read(2), ]; default: throw new InvalidArgumentException('Unsupported type'); } } private function getExponent(string $exponent): string { return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent; } private function convertCertificatesToPem(array $certificates): array { foreach ($certificates as $k => $v) { $tmp = '-----BEGIN CERTIFICATE-----'.PHP_EOL; $tmp .= chunk_split(base64_encode($v), 64, PHP_EOL); $tmp .= '-----END CERTIFICATE-----'.PHP_EOL; $certificates[$k] = $tmp; } return $certificates; } private function getTPMHash(string $nameAlg): string { switch (bin2hex($nameAlg)) { case '0004': return 'sha1'; //: "TPM_ALG_SHA1", case '000b': return 'sha256'; //: "TPM_ALG_SHA256", case '000c': return 'sha384'; //: "TPM_ALG_SHA384", case '000d': return 'sha512'; //: "TPM_ALG_SHA512", default: throw new InvalidArgumentException('Unsupported hash algorithm'); } } private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { $trustPath = $attestationStatement->getTrustPath(); Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path'); $certificates = $trustPath->getCertificates(); if (null !== $this->metadataStatementRepository) { $certificates = CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), $certificates, $this->metadataStatementRepository ); } // Check certificate CA chain and returns the Attestation Certificate $this->checkCertificate($certificates[0], $authenticatorData); // Get the COSE algorithm identifier and the corresponding OpenSSL one $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg'); $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier); $result = openssl_verify($attestationStatement->get('certInfo'), $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier); return 1 === $result; } private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void { $parsed = openssl_x509_parse($attestnCert); Assertion::isArray($parsed, 'Invalid certificate'); //Check version Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version'); //Check subject field is empty Assertion::false(!isset($parsed['subject']) || !\is_array($parsed['subject']) || 0 !== \count($parsed['subject']), 'Invalid certificate name. The Subject should be empty'); // Check period of validity Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.'); Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.'); $startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']); Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.'); Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.'); Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.'); $endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']); Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.'); //Check extensions Assertion::false(!isset($parsed['extensions']) || !\is_array($parsed['extensions']), 'Certificate extensions are missing'); //Check subjectAltName Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing'); //Check extendedKeyUsage Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing'); Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid'); // id-fido-gen-ce-aaguid OID check Assertion::false(\in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($authenticatorData->getAttestedCredentialData()->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate'); // TODO: For attestationRoot in metadata.attestationRootCertificates, generate verification chain verifX5C by appending attestationRoot to the x5c. Try verifying verifX5C. If successful go to next step. If fail try next attestationRoot. If no attestationRoots left to try, fail. } private function processWithECDAA(): bool { throw new RuntimeException('ECDAA not supported'); } } src/AttestationStatement/PackedAttestationStatementSupport.php 0000644 00000022442 15173222011 0021117 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; use CBOR\Decoder; use CBOR\MapObject; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use Cose\Algorithm\Manager; use Cose\Algorithm\Signature\Signature; use Cose\Algorithms; use Cose\Key\Key; use InvalidArgumentException; use RuntimeException; use Webauthn\AuthenticatorData; use Webauthn\CertificateToolbox; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\StringStream; use Webauthn\TrustPath\CertificateTrustPath; use Webauthn\TrustPath\EcdaaKeyIdTrustPath; use Webauthn\TrustPath\EmptyTrustPath; use Webauthn\Util\CoseSignatureFixer; final class PackedAttestationStatementSupport implements AttestationStatementSupport { /** * @var Decoder */ private $decoder; /** * @var Manager */ private $algorithmManager; /** * @var MetadataStatementRepository|null */ private $metadataStatementRepository; public function __construct(?Decoder $decoder, Manager $algorithmManager, ?MetadataStatementRepository $metadataStatementRepository = null) { if (null !== $decoder) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } if (null === $metadataStatementRepository) { @trigger_error('Setting "null" for argument "$metadataStatementRepository" is deprecated since 2.1 and will be mandatory in v3.0.', E_USER_DEPRECATED); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->algorithmManager = $algorithmManager; $this->metadataStatementRepository = $metadataStatementRepository; } public function name(): string { return 'packed'; } public function load(array $attestation): AttestationStatement { Assertion::keyExists($attestation['attStmt'], 'sig', 'The attestation statement value "sig" is missing.'); Assertion::keyExists($attestation['attStmt'], 'alg', 'The attestation statement value "alg" is missing.'); Assertion::string($attestation['attStmt']['sig'], 'The attestation statement value "sig" is missing.'); switch (true) { case \array_key_exists('x5c', $attestation['attStmt']): return $this->loadBasicType($attestation); case \array_key_exists('ecdaaKeyId', $attestation['attStmt']): return $this->loadEcdaaType($attestation['attStmt']); default: return $this->loadEmptyType($attestation); } } public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { $trustPath = $attestationStatement->getTrustPath(); switch (true) { case $trustPath instanceof CertificateTrustPath: return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData, $trustPath); case $trustPath instanceof EcdaaKeyIdTrustPath: return $this->processWithECDAA(); case $trustPath instanceof EmptyTrustPath: return $this->processWithSelfAttestation($clientDataJSONHash, $attestationStatement, $authenticatorData); default: throw new InvalidArgumentException('Unsupported attestation statement'); } } private function loadBasicType(array $attestation): AttestationStatement { $certificates = $attestation['attStmt']['x5c']; Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.'); Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.'); $certificates = CertificateToolbox::convertAllDERToPEM($certificates); return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates)); } private function loadEcdaaType(array $attestation): AttestationStatement { $ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId']; Assertion::string($ecdaaKeyId, 'The attestation statement value "ecdaaKeyId" is invalid.'); return AttestationStatement::createEcdaa($attestation['fmt'], $attestation['attStmt'], new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId'])); } private function loadEmptyType(array $attestation): AttestationStatement { return AttestationStatement::createSelf($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath()); } private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void { $parsed = openssl_x509_parse($attestnCert); Assertion::isArray($parsed, 'Invalid certificate'); //Check version Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version'); //Check subject field Assertion::false(!isset($parsed['name']) || false === mb_strpos($parsed['name'], '/OU=Authenticator Attestation'), 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"'); //Check extensions Assertion::false(!isset($parsed['extensions']) || !\is_array($parsed['extensions']), 'Certificate extensions are missing'); //Check certificate is not a CA cert Assertion::false(!isset($parsed['extensions']['basicConstraints']) || 'CA:FALSE' !== $parsed['extensions']['basicConstraints'], 'The Basic Constraints extension must have the CA component set to false'); $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); Assertion::notNull($attestedCredentialData, 'No attested credential available'); // id-fido-gen-ce-aaguid OID check Assertion::false(\in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($attestedCredentialData->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate'); } private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData, CertificateTrustPath $trustPath): bool { $certificates = $trustPath->getCertificates(); if (null !== $this->metadataStatementRepository) { $certificates = CertificateToolbox::checkAttestationMedata( $attestationStatement, $authenticatorData->getAttestedCredentialData()->getAaguid()->toString(), $certificates, $this->metadataStatementRepository ); } // Check leaf certificate $this->checkCertificate($certificates[0], $authenticatorData); // Get the COSE algorithm identifier and the corresponding OpenSSL one $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg'); $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier); // Verification of the signature $signedData = $authenticatorData->getAuthData().$clientDataJSONHash; $result = openssl_verify($signedData, $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier); return 1 === $result; } private function processWithECDAA(): bool { throw new RuntimeException('ECDAA not supported'); } private function processWithSelfAttestation(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool { $attestedCredentialData = $authenticatorData->getAttestedCredentialData(); Assertion::notNull($attestedCredentialData, 'No attested credential available'); $credentialPublicKey = $attestedCredentialData->getCredentialPublicKey(); Assertion::notNull($credentialPublicKey, 'No credential public key available'); $publicKeyStream = new StringStream($credentialPublicKey); $publicKey = $this->decoder->decode($publicKeyStream); Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.'); $publicKeyStream->close(); Assertion::isInstanceOf($publicKey, MapObject::class, 'The attested credential data does not contain a valid public key.'); $publicKey = $publicKey->getNormalizedData(false); $publicKey = new Key($publicKey); Assertion::eq($publicKey->alg(), (int) $attestationStatement->get('alg'), 'The algorithm of the attestation statement and the key are not identical.'); $dataToVerify = $authenticatorData->getAuthData().$clientDataJSONHash; $algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg')); if (!$algorithm instanceof Signature) { throw new RuntimeException('Invalid algorithm'); } $signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm); return $algorithm->verify($dataToVerify, $publicKey, $signature); } } src/AttestationStatement/AttestationObject.php 0000644 00000002143 15173222011 0015650 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Webauthn\AuthenticatorData; class AttestationObject { /** * @var string */ private $rawAttestationObject; /** * @var AttestationStatement */ private $attStmt; /** * @var AuthenticatorData */ private $authData; public function __construct(string $rawAttestationObject, AttestationStatement $attStmt, AuthenticatorData $authData) { $this->rawAttestationObject = $rawAttestationObject; $this->attStmt = $attStmt; $this->authData = $authData; } public function getRawAttestationObject(): string { return $this->rawAttestationObject; } public function getAttStmt(): AttestationStatement { return $this->attStmt; } public function getAuthData(): AuthenticatorData { return $this->authData; } } src/AttestationStatement/AttestationStatementSupportManager.php 0000644 00000002043 15173222012 0021276 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AttestationStatement; use Assert\Assertion; class AttestationStatementSupportManager { /** * @var AttestationStatementSupport[] */ private $attestationStatementSupports = []; public function add(AttestationStatementSupport $attestationStatementSupport): void { $this->attestationStatementSupports[$attestationStatementSupport->name()] = $attestationStatementSupport; } public function has(string $name): bool { return \array_key_exists($name, $this->attestationStatementSupports); } public function get(string $name): AttestationStatementSupport { Assertion::true($this->has($name), sprintf('The attestation statement format "%s" is not supported.', $name)); return $this->attestationStatementSupports[$name]; } } src/AttestedCredentialData.php 0000644 00000005117 15173222012 0012405 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use JsonSerializable; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; /** * @see https://www.w3.org/TR/webauthn/#sec-attested-credential-data */ class AttestedCredentialData implements JsonSerializable { /** * @var UuidInterface */ private $aaguid; /** * @var string */ private $credentialId; /** * @var string|null */ private $credentialPublicKey; public function __construct(UuidInterface $aaguid, string $credentialId, ?string $credentialPublicKey) { $this->aaguid = $aaguid; $this->credentialId = $credentialId; $this->credentialPublicKey = $credentialPublicKey; } public function getAaguid(): UuidInterface { return $this->aaguid; } public function getCredentialId(): string { return $this->credentialId; } public function getCredentialPublicKey(): ?string { return $this->credentialPublicKey; } public static function createFromArray(array $json): self { Assertion::keyExists($json, 'aaguid', 'Invalid input. "aaguid" is missing.'); Assertion::keyExists($json, 'credentialId', 'Invalid input. "credentialId" is missing.'); switch (true) { case 36 === mb_strlen($json['aaguid'], '8bit'): $uuid = Uuid::fromString($json['aaguid']); break; default: // Kept for compatibility with old format $decoded = base64_decode($json['aaguid'], true); Assertion::string($decoded, 'Unable to decode the data'); $uuid = Uuid::fromBytes($decoded); } $credentialId = base64_decode($json['credentialId'], true); Assertion::string($credentialId, 'Unable to decode the data'); return new self( $uuid, $credentialId, isset($json['credentialPublicKey']) ? base64_decode($json['credentialPublicKey'], true) : null ); } public function jsonSerialize(): array { $result = [ 'aaguid' => $this->aaguid->toString(), 'credentialId' => base64_encode($this->credentialId), ]; if (null !== $this->credentialPublicKey) { $result['credentialPublicKey'] = base64_encode($this->credentialPublicKey); } return $result; } } src/TokenBinding/IgnoreTokenBindingHandler.php 0000644 00000001003 15173222012 0015421 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TokenBinding; use Psr\Http\Message\ServerRequestInterface; final class IgnoreTokenBindingHandler implements TokenBindingHandler { public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void { //Does nothing } } src/TokenBinding/TokenBinding.php 0000644 00000003572 15173222012 0012774 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TokenBinding; use Assert\Assertion; use Base64Url\Base64Url; class TokenBinding { public const TOKEN_BINDING_STATUS_PRESENT = 'present'; public const TOKEN_BINDING_STATUS_SUPPORTED = 'supported'; public const TOKEN_BINDING_STATUS_NOT_SUPPORTED = 'not-supported'; /** * @var string */ private $status; /** * @var string|null */ private $id; public function __construct(string $status, ?string $id) { Assertion::false(self::TOKEN_BINDING_STATUS_PRESENT === $status && null === $id, 'The member "id" is required when status is "present"'); $this->status = $status; $this->id = $id; } public static function createFormArray(array $json): self { Assertion::keyExists($json, 'status', 'The member "status" is required'); $status = $json['status']; Assertion::inArray( $status, self::getSupportedStatus(), sprintf('The member "status" is invalid. Supported values are: %s', implode(', ', self::getSupportedStatus())) ); $id = \array_key_exists('id', $json) ? Base64Url::decode($json['id']) : null; return new self($status, $id); } public function getStatus(): string { return $this->status; } public function getId(): ?string { return $this->id; } /** * @return string[] */ private static function getSupportedStatus(): array { return [ self::TOKEN_BINDING_STATUS_PRESENT, self::TOKEN_BINDING_STATUS_SUPPORTED, self::TOKEN_BINDING_STATUS_NOT_SUPPORTED, ]; } } src/TokenBinding/TokenBindingNotSupportedHandler.php 0000644 00000001214 15173222012 0016650 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TokenBinding; use Assert\Assertion; use Psr\Http\Message\ServerRequestInterface; final class TokenBindingNotSupportedHandler implements TokenBindingHandler { public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void { Assertion::true(TokenBinding::TOKEN_BINDING_STATUS_PRESENT !== $tokenBinding->getStatus(), 'Token binding not supported.'); } } src/TokenBinding/TokenBindingHandler.php 0000644 00000000672 15173222012 0014270 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TokenBinding; use Psr\Http\Message\ServerRequestInterface; interface TokenBindingHandler { public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void; } src/PublicKeyCredentialSourceRepository.php 0000644 00000001251 15173222012 0015201 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn; interface PublicKeyCredentialSourceRepository { public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource; /** * @return PublicKeyCredentialSource[] */ public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array; public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void; } src/Util/CoseSignatureFixer.php 0000644 00000002717 15173222012 0012534 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\Util; use Cose\Algorithm\Signature\ECDSA; use Cose\Algorithm\Signature\Signature; /** * This class fixes the signature of the ECDSA based algorithms. * * @internal * * @see https://www.w3.org/TR/webauthn/#signature-attestation-types */ abstract class CoseSignatureFixer { public static function fix(string $signature, Signature $algorithm): string { switch ($algorithm::identifier()) { case ECDSA\ES256K::ID: case ECDSA\ES256::ID: if (64 === mb_strlen($signature, '8bit')) { return $signature; } return ECDSA\ECSignature::fromAsn1($signature, 64); //TODO: fix this hardcoded value by adding a dedicated method for the algorithms case ECDSA\ES384::ID: if (96 === mb_strlen($signature, '8bit')) { return $signature; } return ECDSA\ECSignature::fromAsn1($signature, 96); case ECDSA\ES512::ID: if (132 === mb_strlen($signature, '8bit')) { return $signature; } return ECDSA\ECSignature::fromAsn1($signature, 132); } return $signature; } } src/PublicKeyCredentialRequestOptions.php 0000644 00000007120 15173222012 0014646 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Base64Url\Base64Url; use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs; class PublicKeyCredentialRequestOptions extends PublicKeyCredentialOptions { public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required'; public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred'; public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged'; /** * @var string|null */ private $rpId; /** * @var PublicKeyCredentialDescriptor[] */ private $allowCredentials; /** * @var string|null */ private $userVerification; /** * @param PublicKeyCredentialDescriptor[] $allowCredentials */ public function __construct(string $challenge, ?int $timeout = null, ?string $rpId = null, array $allowCredentials = [], ?string $userVerification = null, ?AuthenticationExtensionsClientInputs $extensions = null) { parent::__construct($challenge, $timeout, $extensions); $this->rpId = $rpId; $this->allowCredentials = array_values($allowCredentials); $this->userVerification = $userVerification; } public function getRpId(): ?string { return $this->rpId; } /** * @return PublicKeyCredentialDescriptor[] */ public function getAllowCredentials(): array { return $this->allowCredentials; } public function getUserVerification(): ?string { return $this->userVerification; } public static function createFromString(string $data): PublicKeyCredentialOptions { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): PublicKeyCredentialOptions { Assertion::keyExists($json, 'challenge', 'Invalid input. "challenge" is missing.'); $allowCredentials = []; $allowCredentialList = $json['allowCredentials'] ?? []; foreach ($allowCredentialList as $allowCredential) { $allowCredentials[] = PublicKeyCredentialDescriptor::createFromArray($allowCredential); } return new self( Base64Url::decode($json['challenge']), $json['timeout'] ?? null, $json['rpId'] ?? null, $allowCredentials, $json['userVerification'] ?? null, isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray($json['extensions']) : new AuthenticationExtensionsClientInputs() ); } public function jsonSerialize(): array { $json = [ 'challenge' => Base64Url::encode($this->challenge), ]; if (null !== $this->rpId) { $json['rpId'] = $this->rpId; } if (null !== $this->userVerification) { $json['userVerification'] = $this->userVerification; } if (0 !== \count($this->allowCredentials)) { $json['allowCredentials'] = $this->allowCredentials; } if (0 !== $this->extensions->count()) { $json['extensions'] = $this->extensions; } if (null !== $this->timeout) { $json['timeout'] = $this->timeout; } return $json; } } src/AuthenticatorAttestationResponseValidator.php 0000644 00000014655 15173222012 0016471 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Psr\Http\Message\ServerRequestInterface; use Webauthn\AttestationStatement\AttestationObject; use Webauthn\AttestationStatement\AttestationStatementSupportManager; use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler; use Webauthn\TokenBinding\TokenBindingHandler; class AuthenticatorAttestationResponseValidator { /** * @var AttestationStatementSupportManager */ private $attestationStatementSupportManager; /** * @var PublicKeyCredentialSourceRepository */ private $publicKeyCredentialSource; /** * @var TokenBindingHandler */ private $tokenBindingHandler; /** * @var ExtensionOutputCheckerHandler */ private $extensionOutputCheckerHandler; public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, PublicKeyCredentialSourceRepository $publicKeyCredentialSource, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler) { $this->attestationStatementSupportManager = $attestationStatementSupportManager; $this->publicKeyCredentialSource = $publicKeyCredentialSource; $this->tokenBindingHandler = $tokenBindingHandler; $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; } /** * @see https://www.w3.org/TR/webauthn/#registering-a-new-credential */ public function check(AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $request): PublicKeyCredentialSource { /** @see 7.1.1 */ //Nothing to do /** @see 7.1.2 */ $C = $authenticatorAttestationResponse->getClientDataJSON(); /* @see 7.1.3 */ Assertion::eq('webauthn.create', $C->getType(), 'The client data type is not "webauthn.create".'); /* @see 7.1.4 */ Assertion::true(hash_equals($publicKeyCredentialCreationOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.'); /** @see 7.1.5 */ $rpId = $publicKeyCredentialCreationOptions->getRp()->getId() ?? $request->getUri()->getHost(); $parsedRelyingPartyId = parse_url($C->getOrigin()); Assertion::isArray($parsedRelyingPartyId, sprintf('The origin URI "%s" is not valid', $C->getOrigin())); Assertion::keyExists($parsedRelyingPartyId, 'scheme', 'Invalid origin rpId.'); $scheme = $parsedRelyingPartyId['scheme'] ?? ''; Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.'); $clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.'); $rpIdLength = mb_strlen($rpId); Assertion::eq(mb_substr($clientDataRpId, -$rpIdLength), $rpId, 'rpId mismatch.'); /* @see 7.1.6 */ if (null !== $C->getTokenBinding()) { $this->tokenBindingHandler->check($C->getTokenBinding(), $request); } /** @see 7.1.7 */ $clientDataJSONHash = hash('sha256', $authenticatorAttestationResponse->getClientDataJSON()->getRawData(), true); /** @see 7.1.8 */ $attestationObject = $authenticatorAttestationResponse->getAttestationObject(); /** @see 7.1.9 */ $rpIdHash = hash('sha256', $rpId, true); Assertion::true(hash_equals($rpIdHash, $attestationObject->getAuthData()->getRpIdHash()), 'rpId hash mismatch.'); /* @see 7.1.10 */ /* @see 7.1.11 */ if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialCreationOptions->getAuthenticatorSelection()->getUserVerification()) { Assertion::true($attestationObject->getAuthData()->isUserPresent(), 'User was not present'); Assertion::true($attestationObject->getAuthData()->isUserVerified(), 'User authentication required.'); } /* @see 7.1.12 */ $extensions = $attestationObject->getAuthData()->getExtensions(); if (null !== $extensions) { $this->extensionOutputCheckerHandler->check($extensions); } /** @see 7.1.13 */ $fmt = $attestationObject->getAttStmt()->getFmt(); Assertion::true($this->attestationStatementSupportManager->has($fmt), 'Unsupported attestation statement format.'); /** @see 7.1.14 */ $attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt); Assertion::true($attestationStatementSupport->isValid($clientDataJSONHash, $attestationObject->getAttStmt(), $attestationObject->getAuthData()), 'Invalid attestation statement.'); /* @see 7.1.15 */ /* @see 7.1.16 */ /* @see 7.1.17 */ Assertion::true($attestationObject->getAuthData()->hasAttestedCredentialData(), 'There is no attested credential data.'); $attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData(); Assertion::notNull($attestedCredentialData, 'There is no attested credential data.'); $credentialId = $attestedCredentialData->getCredentialId(); Assertion::null($this->publicKeyCredentialSource->findOneByCredentialId($credentialId), 'The credential ID already exists.'); /* @see 7.1.18 */ /* @see 7.1.19 */ return $this->createPublicKeyCredentialSource( $credentialId, $attestedCredentialData, $attestationObject, $publicKeyCredentialCreationOptions->getUser()->getId() ); } private function createPublicKeyCredentialSource(string $credentialId, AttestedCredentialData $attestedCredentialData, AttestationObject $attestationObject, string $userHandle): PublicKeyCredentialSource { return new PublicKeyCredentialSource( $credentialId, PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, [], $attestationObject->getAttStmt()->getType(), $attestationObject->getAttStmt()->getTrustPath(), $attestedCredentialData->getAaguid(), $attestedCredentialData->getCredentialPublicKey(), $userHandle, $attestationObject->getAuthData()->getSignCount() ); } } src/PublicKeyCredentialUserEntity.php 0000644 00000003650 15173222012 0013761 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; class PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity { /** * @var string */ protected $id; /** * @var string */ protected $displayName; public function __construct(string $name, string $id, string $displayName, ?string $icon = null) { parent::__construct($name, $icon); $this->id = $id; $this->displayName = $displayName; } public function getId(): string { return $this->id; } public function getDisplayName(): string { return $this->displayName; } public static function createFromString(string $data): self { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): self { Assertion::keyExists($json, 'name', 'Invalid input. "name" is missing.'); Assertion::keyExists($json, 'id', 'Invalid input. "id" is missing.'); Assertion::keyExists($json, 'displayName', 'Invalid input. "displayName" is missing.'); $id = base64_decode($json['id'], true); Assertion::string($id, 'Invalid parameter "id".'); return new self( $json['name'], $id, $json['displayName'], $json['icon'] ?? null ); } public function jsonSerialize(): array { $json = parent::jsonSerialize(); $json['id'] = base64_encode($this->id); $json['displayName'] = $this->displayName; return $json; } } src/PublicKeyCredentialLoader.php 0000644 00000012346 15173222012 0013056 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Base64Url\Base64Url; use CBOR\Decoder; use CBOR\MapObject; use CBOR\OtherObject\OtherObjectManager; use CBOR\Tag\TagObjectManager; use InvalidArgumentException; use Ramsey\Uuid\Uuid; 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; public function __construct(AttestationObjectLoader $attestationObjectLoader, ?Decoder $decoder = null) { if (null !== $decoder) { @trigger_error('The argument "$decoder" is deprecated since 2.1 and will be removed in v3.0. Set null instead', E_USER_DEPRECATED); } $this->decoder = $decoder ?? new Decoder(new TagObjectManager(), new OtherObjectManager()); $this->attestationObjectLoader = $attestationObjectLoader; } public function loadArray(array $json): PublicKeyCredential { 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']) ); return $publicKeyCredential; } public function load(string $data): PublicKeyCredential { $json = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); return $this->loadArray($json); } private function createResponse(array $response): AuthenticatorResponse { Assertion::keyExists($response, 'clientDataJSON'); switch (true) { case \array_key_exists('attestationObject', $response): $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'); } } } src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php 0000644 00000003165 15173222012 0022504 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; use ArrayIterator; use Assert\Assertion; use function count; use Countable; use Iterator; use IteratorAggregate; use JsonSerializable; class AuthenticationExtensionsClientInputs implements JsonSerializable, Countable, IteratorAggregate { /** * @var AuthenticationExtension[] */ private $extensions = []; public function add(AuthenticationExtension $extension): void { $this->extensions[$extension->name()] = $extension; } public static function createFromArray(array $json): self { $object = new self(); foreach ($json as $k => $v) { $object->add(new AuthenticationExtension($k, $v)); } return $object; } public function has(string $key): bool { return \array_key_exists($key, $this->extensions); } /** * @return mixed */ public function get(string $key) { Assertion::true($this->has($key), sprintf('The extension with key "%s" is not available', $key)); return $this->extensions[$key]; } public function jsonSerialize(): array { return $this->extensions; } public function getIterator(): Iterator { return new ArrayIterator($this->extensions); } public function count(int $mode = COUNT_NORMAL): int { return \count($this->extensions, $mode); } } src/AuthenticationExtensions/AuthenticationExtensionsClientOutputsLoader.php 0000644 00000001630 15173222012 0024027 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; use Assert\Assertion; use CBOR\CBORObject; use CBOR\MapObject; class AuthenticationExtensionsClientOutputsLoader { public static function load(CBORObject $object): AuthenticationExtensionsClientOutputs { Assertion::isInstanceOf($object, MapObject::class, 'Invalid extension object'); $data = $object->getNormalizedData(); $extensions = new AuthenticationExtensionsClientOutputs(); foreach ($data as $key => $value) { Assertion::string($key, 'Invalid extension key'); $extensions->add(new AuthenticationExtension($key, $value)); } return $extensions; } } src/AuthenticationExtensions/ExtensionOutputCheckerHandler.php 0000644 00000001373 15173222012 0021062 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; class ExtensionOutputCheckerHandler { /** * @var ExtensionOutputChecker[] */ private $checkers = []; public function add(ExtensionOutputChecker $checker): void { $this->checkers[] = $checker; } /** * @throws ExtensionOutputError */ public function check(AuthenticationExtensionsClientOutputs $extensions): void { foreach ($this->checkers as $checker) { $checker->check($extensions); } } } src/AuthenticationExtensions/AuthenticationExtensionsClientOutputs.php 0000644 00000003605 15173222012 0022704 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; use ArrayIterator; use Assert\Assertion; use Countable; use Iterator; use IteratorAggregate; use JsonSerializable; class AuthenticationExtensionsClientOutputs implements JsonSerializable, Countable, IteratorAggregate { /** * @var AuthenticationExtension[] */ private $extensions = []; public function add(AuthenticationExtension $extension): void { $this->extensions[$extension->name()] = $extension; } public static function createFromString(string $data): self { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): self { $object = new self(); foreach ($json as $k => $v) { $object->add(new AuthenticationExtension($k, $v)); } return $object; } public function has(string $key): bool { return \array_key_exists($key, $this->extensions); } /** * @return mixed */ public function get(string $key) { Assertion::true($this->has($key), sprintf('The extension with key "%s" is not available', $key)); return $this->extensions[$key]; } public function jsonSerialize(): array { return $this->extensions; } public function getIterator(): Iterator { return new ArrayIterator($this->extensions); } public function count(int $mode = COUNT_NORMAL): int { return \count($this->extensions, $mode); } } src/AuthenticationExtensions/ExtensionOutputError.php 0000644 00000001544 15173222012 0017311 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; use Exception; use Throwable; class ExtensionOutputError extends Exception { /** * @var AuthenticationExtension */ private $authenticationExtension; public function __construct(AuthenticationExtension $authenticationExtension, string $message = '', int $code = 0, Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->authenticationExtension = $authenticationExtension; } public function getAuthenticationExtension(): AuthenticationExtension { return $this->authenticationExtension; } } src/AuthenticationExtensions/AuthenticationExtension.php 0000644 00000001656 15173222012 0017762 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; use JsonSerializable; class AuthenticationExtension implements JsonSerializable { /** * @var string */ private $name; /** * @var mixed */ private $value; /** * @param mixed $value */ public function __construct(string $name, $value) { $this->name = $name; $this->value = $value; } public function name(): string { return $this->name; } /** * @return mixed */ public function value() { return $this->value; } /** * @return mixed */ public function jsonSerialize() { return $this->value; } } src/AuthenticationExtensions/ExtensionOutputChecker.php 0000644 00000000705 15173222012 0017562 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\AuthenticationExtensions; interface ExtensionOutputChecker { /** * @throws ExtensionOutputError */ public function check(AuthenticationExtensionsClientOutputs $extensions): void; } src/TrustPath/CertificateTrustPath.php 0000644 00000002073 15173222012 0014100 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TrustPath; use Assert\Assertion; final class CertificateTrustPath implements TrustPath { /** * @var string[] */ private $certificates; /** * @param string[] $certificates */ public function __construct(array $certificates) { $this->certificates = $certificates; } /** * @return string[] */ public function getCertificates(): array { return $this->certificates; } public static function createFromArray(array $data): TrustPath { Assertion::keyExists($data, 'x5c', 'The trust path type is invalid'); return new CertificateTrustPath($data['x5c']); } public function jsonSerialize(): array { return [ 'type' => self::class, 'x5c' => $this->certificates, ]; } } src/TrustPath/TrustPathLoader.php 0000644 00000002621 15173222012 0013063 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TrustPath; use Assert\Assertion; use InvalidArgumentException; abstract class TrustPathLoader { public static function loadTrustPath(array $data): TrustPath { Assertion::keyExists($data, 'type', 'The trust path type is missing'); $type = $data['type']; $oldTypes = self::oldTrustPathTypes(); switch (true) { case \array_key_exists($type, $oldTypes): return $oldTypes[$type]::createFromArray($data); case class_exists($type): $implements = class_implements($type); if (\is_array($implements) && \in_array(TrustPath::class, $implements, true)) { return $type::createFromArray($data); } // no break default: throw new InvalidArgumentException(sprintf('The trust path type "%s" is not supported', $data['type'])); } } private static function oldTrustPathTypes(): array { return [ 'empty' => EmptyTrustPath::class, 'ecdaa_key_id' => EcdaaKeyIdTrustPath::class, 'x5c' => CertificateTrustPath::class, ]; } } src/TrustPath/TrustPath.php 0000644 00000000620 15173222012 0011731 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TrustPath; use JsonSerializable; interface TrustPath extends JsonSerializable { public static function createFromArray(array $data): self; } src/TrustPath/EcdaaKeyIdTrustPath.php 0000644 00000001743 15173222012 0013604 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TrustPath; use Assert\Assertion; final class EcdaaKeyIdTrustPath implements TrustPath { /** * @var string */ private $ecdaaKeyId; public function __construct(string $ecdaaKeyId) { $this->ecdaaKeyId = $ecdaaKeyId; } public function getEcdaaKeyId(): string { return $this->ecdaaKeyId; } public function jsonSerialize(): array { return [ 'type' => self::class, 'ecdaaKeyId' => $this->ecdaaKeyId, ]; } public static function createFromArray(array $data): TrustPath { Assertion::keyExists($data, 'ecdaaKeyId', 'The trust path type is invalid'); return new EcdaaKeyIdTrustPath($data['ecdaaKeyId']); } } src/TrustPath/EmptyTrustPath.php 0000644 00000001050 15173222012 0012746 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 Spomky-Labs * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ namespace Webauthn\TrustPath; final class EmptyTrustPath implements TrustPath { public function jsonSerialize(): array { return [ 'type' => self::class, ]; } public static function createFromArray(array $data): TrustPath { return new EmptyTrustPath(); } } src/PublicKeyCredential.php 0000644 00000002550 15173222012 0011723 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; /** * @see https://www.w3.org/TR/webauthn/#iface-pkcredential */ class PublicKeyCredential extends Credential { /** * @var string */ protected $rawId; /** * @var AuthenticatorResponse */ protected $response; public function __construct(string $id, string $type, string $rawId, AuthenticatorResponse $response) { parent::__construct($id, $type); $this->rawId = $rawId; $this->response = $response; } public function getRawId(): string { return $this->rawId; } public function getResponse(): AuthenticatorResponse { return $this->response; } /** * @param string[] $transport */ public function getPublicKeyCredentialDescriptor(array $transport = []): PublicKeyCredentialDescriptor { return new PublicKeyCredentialDescriptor($this->getType(), $this->getRawId(), $transport); } public function __toString() { $encoded = json_encode($this); Assertion::string($encoded, 'Unable to encode the data'); return $encoded; } } src/AuthenticatorSelectionCriteria.php 0000644 00000005156 15173222012 0014211 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use JsonSerializable; class AuthenticatorSelectionCriteria implements JsonSerializable { public const AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE = null; public const AUTHENTICATOR_ATTACHMENT_PLATFORM = 'platform'; public const AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 'cross-platform'; public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required'; public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred'; public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged'; /** * @var string|null */ private $authenticatorAttachment; /** * @var bool */ private $requireResidentKey; /** * @var string */ private $userVerification; public function __construct(?string $authenticatorAttachment = null, bool $requireResidentKey = false, string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED) { $this->authenticatorAttachment = $authenticatorAttachment; $this->requireResidentKey = $requireResidentKey; $this->userVerification = $userVerification; } public function getAuthenticatorAttachment(): ?string { return $this->authenticatorAttachment; } public function isRequireResidentKey(): bool { return $this->requireResidentKey; } public function getUserVerification(): string { return $this->userVerification; } public static function createFromString(string $data): self { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): self { return new self( $json['authenticatorAttachment'] ?? null, $json['requireResidentKey'] ?? false, $json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED ); } public function jsonSerialize(): array { $json = [ 'requireResidentKey' => $this->requireResidentKey, 'userVerification' => $this->userVerification, ]; if (null !== $this->authenticatorAttachment) { $json['authenticatorAttachment'] = $this->authenticatorAttachment; } return $json; } } src/CollectedClientData.php 0000644 00000005627 15173222012 0011700 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Base64Url\Base64Url; use InvalidArgumentException; use Webauthn\TokenBinding\TokenBinding; class CollectedClientData { /** * @var string */ private $rawData; /** * @var array */ private $data; /** * @var string */ private $type; /** * @var string */ private $challenge; /** * @var string */ private $origin; /** * @var array|null */ private $tokenBinding; public function __construct(string $rawData, array $data) { $this->type = $this->findData($data, 'type'); $this->challenge = $this->findData($data, 'challenge', true, true); $this->origin = $this->findData($data, 'origin'); $this->tokenBinding = $this->findData($data, 'tokenBinding', false); $this->rawData = $rawData; $this->data = $data; } public static function createFormJson(string $data): self { $rawData = Base64Url::decode($data); $json = json_decode($rawData, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid collected client data'); Assertion::isArray($json, 'Invalid collected client data'); return new self($rawData, $json); } public function getType(): string { return $this->type; } public function getChallenge(): string { return $this->challenge; } public function getOrigin(): string { return $this->origin; } public function getTokenBinding(): ?TokenBinding { return null === $this->tokenBinding ? null : TokenBinding::createFormArray($this->tokenBinding); } public function getRawData(): string { return $this->rawData; } /** * @return string[] */ public function all(): array { return array_keys($this->data); } public function has(string $key): bool { return \array_key_exists($key, $this->data); } /** * @return mixed */ public function get(string $key) { if (!$this->has($key)) { throw new InvalidArgumentException(sprintf('The key "%s" is missing', $key)); } return $this->data[$key]; } /** * @return mixed|null */ private function findData(array $json, string $key, bool $isRequired = true, bool $isB64 = false) { if (!\array_key_exists($key, $json)) { if ($isRequired) { throw new InvalidArgumentException(sprintf('The key "%s" is missing', $key)); } return; } return $isB64 ? Base64Url::decode($json[$key]) : $json[$key]; } } src/PublicKeyCredentialDescriptor.php 0000644 00000004465 15173222012 0013771 0 ustar 00 <?php declare(strict_types=1); /* * The MIT License (MIT) * * Copyright (c) 2014-2019 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 Assert\Assertion; use Base64Url\Base64Url; use JsonSerializable; class PublicKeyCredentialDescriptor implements JsonSerializable { public const CREDENTIAL_TYPE_PUBLIC_KEY = 'public-key'; public const AUTHENTICATOR_TRANSPORT_USB = 'usb'; public const AUTHENTICATOR_TRANSPORT_NFC = 'nfc'; public const AUTHENTICATOR_TRANSPORT_BLE = 'ble'; public const AUTHENTICATOR_TRANSPORT_INTERNAL = 'internal'; /** * @var string */ protected $type; /** * @var string */ protected $id; /** * @var string[] */ protected $transports; /** * @param string[] $transports */ public function __construct(string $type, string $id, array $transports = []) { $this->type = $type; $this->id = $id; $this->transports = $transports; } public function getType(): string { return $this->type; } public function getId(): string { return $this->id; } /** * @return string[] */ public function getTransports(): array { return $this->transports; } public static function createFromString(string $data): self { $data = json_decode($data, true); Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'Invalid data'); Assertion::isArray($data, 'Invalid data'); return self::createFromArray($data); } public static function createFromArray(array $json): self { Assertion::keyExists($json, 'type', 'Invalid input. "type" is missing.'); Assertion::keyExists($json, 'id', 'Invalid input. "id" is missing.'); return new self( $json['type'], Base64Url::decode($json['id']), $json['transports'] ?? [] ); } public function jsonSerialize(): array { $json = [ 'type' => $this->type, 'id' => Base64Url::encode($this->id), ]; if (0 !== \count($this->transports)) { $json['transports'] = $this->transports; } return $json; } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.23 | Generation time: 0 |
proxy
|
phpinfo
|
Settings