Tpm   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 99
c 1
b 0
f 0
dl 0
loc 243
rs 9.92
wmc 31

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getTPMLengthPrefix() 0 8 1
A validateAttestation() 0 3 1
A getCertificatePem() 0 7 2
B validateOverX5C() 0 50 6
C __construct() 0 76 17
A validateRootCertificate() 0 27 4
1
<?php
2
3
/**
4
 * Platine Webauth
5
 *
6
 * Platine Webauthn is the implementation of webauthn specifications
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Webauth
11
 * Copyright (c) Jakob Bennemann <[email protected]>
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
declare(strict_types=1);
33
34
namespace Platine\Webauthn\Attestation\Format;
35
36
use Platine\Webauthn\Attestation\AuthenticatorData;
37
use Platine\Webauthn\Exception\WebauthnException;
38
use Platine\Webauthn\Helper\ByteBuffer;
39
40
/**
41
 * @class Tpm
42
 * @package Platine\Webauthn\Attestation\Format
43
 */
44
class Tpm extends BaseFormat
45
{
46
    public const TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
47
    public const TPM_ST_ATTEST_CERTIFY = "\x80\x17";
48
49
    /**
50
     * The algorithm used
51
     * @var int
52
     */
53
    protected int $algo;
54
55
    /**
56
     * The signature
57
     * @var string
58
     */
59
    protected string $signature;
60
61
    /**
62
     * The certificate information
63
     * @var ByteBuffer
64
     */
65
    protected ByteBuffer $certInfo;
66
67
    /**
68
     * The public area information
69
     * @var ByteBuffer
70
     */
71
    protected ByteBuffer $pubArea;
72
73
74
    /**
75
     * The X5C information
76
     * @var string
77
     */
78
    protected string $x5c = '';
79
80
    /**
81
     * Create new instance
82
     * @param array<string|int, mixed> $attestationData
83
     * @param AuthenticatorData $authenticatorData
84
     */
85
    public function __construct(
86
        array $attestationData,
87
        AuthenticatorData $authenticatorData
88
    ) {
89
        parent::__construct($attestationData, $authenticatorData);
90
91
        // check packed data
92
        $attestationStatement = $this->attestationData['attStmt'];
93
94
        if (
95
            ! array_key_exists('ver', $attestationStatement) ||
96
            $attestationStatement['ver'] !== '2.0'
97
        ) {
98
            throw new WebauthnException(sprintf(
99
                'Invalid TPM version [%s]',
100
                $attestationStatement['ver']
101
            ));
102
        }
103
104
        if (
105
            ! array_key_exists('alg', $attestationStatement) ||
106
            $this->getCoseAlgorithm($attestationStatement['alg']) === null
107
        ) {
108
            throw new WebauthnException(sprintf(
109
                'Unsupported algorithm provided, got [%d]',
110
                $attestationStatement['alg']
111
            ));
112
        }
113
114
        if (
115
            ! array_key_exists('sig', $attestationStatement) ||
116
            ! $attestationStatement['sig'] instanceof ByteBuffer
117
        ) {
118
            throw new WebauthnException('No signature found');
119
        }
120
121
        if (
122
            ! array_key_exists('certInfo', $attestationStatement) ||
123
            ! $attestationStatement['certInfo'] instanceof ByteBuffer
124
        ) {
125
            throw new WebauthnException('No certificate information found');
126
        }
127
128
        if (
129
            ! array_key_exists('pubArea', $attestationStatement) ||
130
            ! $attestationStatement['pubArea'] instanceof ByteBuffer
131
        ) {
132
            throw new WebauthnException('No public area information found');
133
        }
134
135
        $this->algo = $attestationStatement['alg'];
136
        $this->signature = $attestationStatement['sig']->getBinaryString();
137
        $this->certInfo = $attestationStatement['certInfo'];
138
        $this->pubArea = $attestationStatement['pubArea'];
139
140
        if (
141
            array_key_exists('x5c', $attestationStatement) &&
142
            is_array($attestationStatement['x5c']) &&
143
            count($attestationStatement['x5c']) > 0
144
        ) {
145
            // The attestation certificate attestnCert MUST be the first element in the array
146
            $attestCert = array_shift($attestationStatement['x5c']);
147
            if (! $attestCert instanceof ByteBuffer) {
148
                throw new WebauthnException('Invalid X5C certificate');
149
            }
150
151
            $this->x5c = $attestCert->getBinaryString();
152
153
            // Certificate chains
154
            foreach ($attestationStatement['x5c'] as $chain) {
155
                if ($chain instanceof ByteBuffer) {
156
                    $this->x5cChain[] = $chain->getBinaryString();
157
                }
158
            }
159
        } else {
160
            throw new WebauthnException('Invalid X5C certificate');
161
        }
162
    }
163
164
    /**
165
    * {@inheritdoc}
166
    */
167
    public function getCertificatePem(): ?string
168
    {
169
        if (empty($this->x5c)) {
170
            return null;
171
        }
172
173
        return $this->createCertificatePem($this->x5c);
174
    }
175
176
    /**
177
    * {@inheritdoc}
178
    */
179
    public function validateAttestation(string $clientData): bool
180
    {
181
        return $this->validateOverX5C($clientData);
182
    }
183
184
    /**
185
    * {@inheritdoc}
186
    */
187
    public function validateRootCertificate(array $rootCertificates): bool
188
    {
189
        if (empty($this->x5c)) {
190
            return false;
191
        }
192
193
        $chain = $this->createX5cChainFile();
194
        if ($chain !== null) {
195
            $rootCertificates[] = $chain;
196
        }
197
198
        $value = openssl_x509_checkpurpose(
199
            // TODO phpstan complains so cast to string
200
            (string) $this->getCertificatePem(),
201
            -1,
202
            $rootCertificates
203
        );
204
205
        if ($value === -1) {
206
            throw new WebauthnException(sprintf(
207
                'Error when validate root certificate, error message: [%s]',
208
                openssl_error_string()
209
            ));
210
        }
211
212
        // TODO phpstan complains so cast to bool
213
        return (bool) $value;
214
    }
215
216
    /**
217
     * Validate if x5c is present
218
     * @param string $clientData
219
     * @return bool
220
     */
221
    protected function validateOverX5C(string $clientData): bool
222
    {
223
        // TODO phpstan complains so cast to string
224
        $publicKey = openssl_pkey_get_public((string) $this->getCertificatePem());
225
226
        if ($publicKey === false) {
227
            throw new WebauthnException(sprintf(
228
                'Invalid public key used, error: [%s]',
229
                openssl_error_string()
230
            ));
231
        }
232
233
        // Verify that sig is a valid signature over the concatenation of authenticatorData
234
        // and clientDataHash using the attestation public key in attestnCert
235
        // with the algorithm specified in alg.
236
        $dataToVerify = $this->authenticatorData->getBinary();
237
        $dataToVerify .= $clientData;
238
239
         // Verify that magic is set to TPM_GENERATED_VALUE.
240
        if ($this->certInfo->getBytes(0, 4) !== self::TPM_GENERATED_VALUE) {
241
            throw new WebauthnException('TPM magic value not the same TPM_GENERATED_VALUE');
242
        }
243
244
        // Verify that type is set to TPM_ST_ATTEST_CERTIFY.
245
        if ($this->certInfo->getBytes(4, 2) !== self::TPM_ST_ATTEST_CERTIFY) {
246
            throw new WebauthnException('TPM type value not the same TPM_ST_ATTEST_CERTIFY');
247
        }
248
249
        $offset = 6;
250
        /* variable not used */
251
        $qualifiedSigner = $this->getTPMLengthPrefix($this->certInfo, $offset);
0 ignored issues
show
Unused Code introduced by
The assignment to $qualifiedSigner is dead and can be removed.
Loading history...
252
        $extraData = $this->getTPMLengthPrefix($this->certInfo, $offset);
253
        $coseAlgo = $this->getCoseAlgorithm($this->algo);
254
        if ($coseAlgo === null) {
255
            throw new WebauthnException(sprintf(
256
                'Invalid algorithm [%d]',
257
                $this->algo
258
            ));
259
        }
260
261
        if ($extraData->getBinaryString() !== hash($coseAlgo['hash'], $dataToVerify, true)) {
262
            throw new WebauthnException('certInfo:extraData hash is invalid');
263
        }
264
265
        return openssl_verify(
266
            $this->certInfo->getBinaryString(),
267
            $this->signature,
268
            $publicKey,
269
            $coseAlgo['openssl']
270
        ) === 1;
271
    }
272
273
    /**
274
     * Return the TPM Prefix length byte buffer
275
     * @param ByteBuffer $buffer
276
     * @param int $offset
277
     * @return ByteBuffer
278
     */
279
    protected function getTPMLengthPrefix(ByteBuffer $buffer, int &$offset): ByteBuffer
280
    {
281
        $length = $buffer->getUint16Value($offset);
282
        $data = $buffer->getBytes($offset + 2, $length);
283
284
        $offset += (2 + $length);
285
286
        return new ByteBuffer($data);
287
    }
288
}
289