1<?php 2 3declare(strict_types=1); 4 5/* 6 * The MIT License (MIT) 7 * 8 * Copyright (c) 2014-2020 Spomky-Labs 9 * 10 * This software may be modified and distributed under the terms 11 * of the MIT license. See the LICENSE file for details. 12 */ 13 14namespace Webauthn\AttestationStatement; 15 16use Assert\Assertion; 17use Base64Url\Base64Url; 18use CBOR\Decoder; 19use CBOR\MapObject; 20use CBOR\OtherObject\OtherObjectManager; 21use CBOR\Tag\TagObjectManager; 22use function ord; 23use Psr\Log\LoggerInterface; 24use Psr\Log\NullLogger; 25use Ramsey\Uuid\Uuid; 26use function Safe\sprintf; 27use function Safe\unpack; 28use Throwable; 29use Webauthn\AttestedCredentialData; 30use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader; 31use Webauthn\AuthenticatorData; 32use Webauthn\MetadataService\MetadataStatementRepository; 33use Webauthn\StringStream; 34 35class AttestationObjectLoader 36{ 37 private const FLAG_AT = 0b01000000; 38 private const FLAG_ED = 0b10000000; 39 40 /** 41 * @var Decoder 42 */ 43 private $decoder; 44 45 /** 46 * @var AttestationStatementSupportManager 47 */ 48 private $attestationStatementSupportManager; 49 50 /** 51 * @var LoggerInterface|null 52 */ 53 private $logger; 54 55 public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null) 56 { 57 if (null !== $metadataStatementRepository) { 58 @trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.2 and will be removed in 4.0. Please set `null` instead.', E_USER_DEPRECATED); 59 } 60 if (null !== $logger) { 61 @trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger" instead.', E_USER_DEPRECATED); 62 } 63 $this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager()); 64 $this->attestationStatementSupportManager = $attestationStatementSupportManager; 65 $this->logger = $logger ?? new NullLogger(); 66 } 67 68 public static function create(AttestationStatementSupportManager $attestationStatementSupportManager): self 69 { 70 return new self($attestationStatementSupportManager); 71 } 72 73 public function load(string $data): AttestationObject 74 { 75 try { 76 $this->logger->info('Trying to load the data', ['data' => $data]); 77 $decodedData = Base64Url::decode($data); 78 $stream = new StringStream($decodedData); 79 $parsed = $this->decoder->decode($stream); 80 81 $this->logger->info('Loading the Attestation Statement'); 82 $attestationObject = $parsed->getNormalizedData(); 83 Assertion::true($stream->isEOF(), 'Invalid attestation object. Presence of extra bytes.'); 84 $stream->close(); 85 Assertion::isArray($attestationObject, 'Invalid attestation object'); 86 Assertion::keyExists($attestationObject, 'authData', 'Invalid attestation object'); 87 Assertion::keyExists($attestationObject, 'fmt', 'Invalid attestation object'); 88 Assertion::keyExists($attestationObject, 'attStmt', 'Invalid attestation object'); 89 $authData = $attestationObject['authData']; 90 91 $attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']); 92 $attestationStatement = $attestationStatementSupport->load($attestationObject); 93 $this->logger->info('Attestation Statement loaded'); 94 $this->logger->debug('Attestation Statement loaded', ['attestationStatement' => $attestationStatement]); 95 96 $authDataStream = new StringStream($authData); 97 $rp_id_hash = $authDataStream->read(32); 98 $flags = $authDataStream->read(1); 99 $signCount = $authDataStream->read(4); 100 $signCount = unpack('N', $signCount)[1]; 101 $this->logger->debug(sprintf('Signature counter: %d', $signCount)); 102 103 $attestedCredentialData = null; 104 if (0 !== (ord($flags) & self::FLAG_AT)) { 105 $this->logger->info('Attested Credential Data is present'); 106 $aaguid = Uuid::fromBytes($authDataStream->read(16)); 107 $credentialLength = $authDataStream->read(2); 108 $credentialLength = unpack('n', $credentialLength)[1]; 109 $credentialId = $authDataStream->read($credentialLength); 110 $credentialPublicKey = $this->decoder->decode($authDataStream); 111 Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.'); 112 $attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey); 113 $this->logger->info('Attested Credential Data loaded'); 114 $this->logger->debug('Attested Credential Data loaded', ['at' => $attestedCredentialData]); 115 } 116 117 $extension = null; 118 if (0 !== (ord($flags) & self::FLAG_ED)) { 119 $this->logger->info('Extension Data loaded'); 120 $extension = $this->decoder->decode($authDataStream); 121 $extension = AuthenticationExtensionsClientOutputsLoader::load($extension); 122 $this->logger->info('Extension Data loaded'); 123 $this->logger->debug('Extension Data loaded', ['ed' => $extension]); 124 } 125 Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.'); 126 $authDataStream->close(); 127 128 $authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension); 129 $attestationObject = new AttestationObject($data, $attestationStatement, $authenticatorData); 130 $this->logger->info('Attestation Object loaded'); 131 $this->logger->debug('Attestation Object', ['ed' => $attestationObject]); 132 133 return $attestationObject; 134 } catch (Throwable $throwable) { 135 $this->logger->error('An error occurred', [ 136 'exception' => $throwable, 137 ]); 138 throw $throwable; 139 } 140 } 141 142 public function setLogger(LoggerInterface $logger): self 143 { 144 $this->logger = $logger; 145 146 return $this; 147 } 148} 149