MetadataServiceProvider::getCachedToc()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 1
b 0
f 0
nc 7
nop 0
dl 0
loc 23
ccs 0
cts 15
cp 0
crap 30
rs 9.4888
1
<?php
2
3
namespace MadWizard\WebAuthn\Metadata\Provider;
4
5
use DateTimeImmutable;
6
use MadWizard\WebAuthn\Attestation\TrustAnchor\MetadataInterface;
7
use MadWizard\WebAuthn\Cache\CacheProviderInterface;
8
use MadWizard\WebAuthn\Exception\ParseException;
9
use MadWizard\WebAuthn\Exception\VerificationException;
10
use MadWizard\WebAuthn\Exception\WebAuthnException;
11
use MadWizard\WebAuthn\Format\Base64UrlEncoding;
12
use MadWizard\WebAuthn\Format\ByteBuffer;
13
use MadWizard\WebAuthn\Metadata\Source\MetadataServiceSource;
14
use MadWizard\WebAuthn\Metadata\Statement\MetadataStatement;
15
use MadWizard\WebAuthn\Metadata\Statement\MetadataToc;
16
use MadWizard\WebAuthn\Pki\ChainValidatorInterface;
17
use MadWizard\WebAuthn\Pki\Jwt\Jwt;
18
use MadWizard\WebAuthn\Pki\Jwt\JwtInterface;
19
use MadWizard\WebAuthn\Pki\Jwt\JwtValidator;
20
use MadWizard\WebAuthn\Pki\Jwt\ValidationContext;
21
use MadWizard\WebAuthn\Pki\Jwt\X5cParameterReader;
22
use MadWizard\WebAuthn\Pki\X509Certificate;
23
use MadWizard\WebAuthn\Remote\DownloaderInterface;
24
use MadWizard\WebAuthn\Server\Registration\RegistrationResultInterface;
25
use Psr\Cache\CacheItemPoolInterface;
26
use Psr\Log\LoggerAwareInterface;
27
use Psr\Log\LoggerAwareTrait;
28
use Psr\Log\NullLogger;
29
30
final class MetadataServiceProvider implements MetadataProviderInterface, LoggerAwareInterface
31
{
32
    use LoggerAwareTrait;
33
34
    /**
35
     * @var DownloaderInterface
36
     */
37
    private $downloader;
38
39
    /**
40
     * @var CacheItemPoolInterface
41
     */
42
    private $cachePool;
43
44
    /**
45
     * @var CacheProviderInterface
46
     */
47
    private $cacheProvider;
48
49
    /**
50
     * @var ChainValidatorInterface
51
     */
52
    private $chainValidator;
53
54
    /**
55
     * Cache time for specific metadata statements. Because caching of metadata statements is done by the hash of its
56
     * contents (which is also included in the TOC) in theory these items can be cached indefinitely because any update
57
     * would cause the hash in the TOC to change and trigger a new download.
58
     * Set to 1 day by default to prevent keeping stale data.
59
     */
60
    private const METADATA_HASH_CACHE_TTL = 86400;
61
62
    /**
63
     * @var MetadataServiceSource
64
     */
65
    private $mdsSource;
66
67
    public function __construct(
68
        MetadataServiceSource $mdsSource,
69
        DownloaderInterface $downloader,
70
        CacheProviderInterface $cacheProvider,
71
        ChainValidatorInterface $chainValidator
72
    ) {
73
        $this->mdsSource = $mdsSource;
74
        $this->cachePool = $cacheProvider->getCachePool('metadataService');
75
        $this->downloader = $downloader;
76
        $this->cacheProvider = $cacheProvider;
77
        $this->chainValidator = $chainValidator;
78
        $this->logger = new NullLogger();
79
    }
80
81
    private function getTokenUrl(string $url): string
82
    {
83
        $token = $this->mdsSource->getAccessToken();
84
        if ($token === null) {
85
            return $url;
86
        }
87
88
        // Only add token if host matches host of main TOC URL to prevent leaking token to other hosts.
89
        $mainHost = parse_url($this->mdsSource->getUrl(), PHP_URL_HOST);
90
        $host = parse_url($url, PHP_URL_HOST);
91
92
        if (strcasecmp($mainHost, $host) === 0) {
93
            return $url . '?' . http_build_query(['token' => $token]);
94
        }
95
        return $url;
96
    }
97
98
    public function getMetadata(RegistrationResultInterface $registrationResult): ?MetadataInterface
99
    {
100
        $identifier = $registrationResult->getIdentifier();
101
        if ($identifier === null) {
102
            return null;
103
        }
104
        $toc = $this->getCachedToc();
105
        $tocItem = $toc->findItem($identifier);
106
107
        $this->logger->debug('Searching MDS for identifier {id}.', ['id' => $identifier->toString()]);
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
        $this->logger->/** @scrutinizer ignore-call */ 
108
                       debug('Searching MDS for identifier {id}.', ['id' => $identifier->toString()]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
108
109
        if ($tocItem === null) {
110
            return null;
111
        }
112
113
        $url = $tocItem->getUrl();
114
        $hash = $tocItem->getHash();
115
        if ($url === null || $hash === null) {
116
            return null;
117
        }
118
119
        $meta = $this->getMetadataItem($url, $hash);
120
        $meta->setStatusReports($tocItem->getStatusReports());
121
        return $meta;
122
    }
123
124
    private function getMetadataItem(string $url, ByteBuffer $hash): MetadataStatement
125
    {
126
        $item = $this->cachePool->getItem($hash->getHex());
127
128
        if ($item->isHit()) {
129
            $meta = $item->get();
130
        } else {
131
            $urlWithToken = $this->getTokenUrl($url);
132
            $meta = $this->downloader->downloadFile($urlWithToken);
133
            $fileHash = hash('sha256', $meta->getData(), true);
134
            if (!hash_equals($hash->getBinaryString(), $fileHash)) {
135
                throw new VerificationException(sprintf('Hash mismatch for url %s, ignoring metadata entry.', $url));
136
            }
137
138
            $item->set($meta);
139
            $item->expiresAfter(self::METADATA_HASH_CACHE_TTL);
140
            $this->cachePool->save($item);
141
        }
142
        return MetadataStatement::decodeString(Base64UrlEncoding::decode($meta->getData()));
143
    }
144
145
    private function getCachedToc(): MetadataToc
146
    {
147
        $url = $this->getTokenUrl($this->mdsSource->getUrl());
148
        $urlHash = hash('sha256', $url);
149
        $item = $this->cachePool->getItem($urlHash);
150
151
        if ($item->isHit()) {
152
            $data = $item->get();
153
            if ($data instanceof MetadataToc) {
154
                if ($data->getNextUpdate() > new DateTimeImmutable()) {           // TODO: abstract time for unit tests
155
                    return $data;
156
                }
157
            }
158
        }
159
160
        $data = $this->downloadToc($url);
161
162
        if ($data !== null) {
163
            $item->set($data);
164
            $item->expiresAt($data->getNextUpdate());
165
            $this->cachePool->save($item);
166
        }
167
        return $data;
168
    }
169
170
    private function downloadToc(string $url): MetadataToc
171
    {
172
        $this->logger->debug('Dowloading TOC {url}', ['url' => preg_replace('~\?.*$~', '', $url)]);   // Remove parameters to hide token in logs
173
        $a = $this->downloader->downloadFile($url);
174
        if (!in_array(strtolower($a->getContentType()), ['application/octet-stream', 'application/jose'])) {
175
            throw new ParseException('Unexpected mime type.');
176
        }
177
178
        $jwt = new Jwt($a->getData());
179
180
        $x5cParam = X5cParameterReader::getX5cParameter($jwt);
181
        if ($x5cParam === null) {
182
            throw new ParseException('MDS has no x5c certificate chain in header.');
183
        }
184
185
        $jwtValidator = new JwtValidator();
186
        $context = new ValidationContext(JwtInterface::ES_AND_RSA, $x5cParam->getCoseKey());
187
        try {
188
            $claims = $jwtValidator->validate($jwt, $context);
189
        } catch (WebAuthnException $e) {
190
            throw new VerificationException(sprintf('Failed to verify JWT: %s', $e->getMessage()), 0, $e);
191
        }
192
193
        if (!$this->chainValidator->validateChain(X509Certificate::fromPem($this->mdsSource->getRootCert()), ...array_reverse($x5cParam->getCertificates()))) {
194
            throw new VerificationException('Failed to verify x5c chain in JWT.');
195
        }
196
        return MetadataToc::fromJson($claims);
197
    }
198
199
    public function getDescription(): string
200
    {
201
        return sprintf('Metadata service url=%s', $this->mdsSource->getUrl());
202
    }
203
}
204