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