Completed
Push — master ( 7acc61...cd9e85 )
by Florent
22s
created

SignatureResponse::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 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
14
namespace U2FAuthentication\Fido;
15
16
use Base64Url\Base64Url;
17
18
class SignatureResponse
19
{
20
    /**
21
     * @var ClientData
22
     */
23
    private $clientData;
24
25
    /**
26
     * @var KeyHandler
27
     */
28
    private $keyHandle;
29
30
    /**
31
     * @var bool
32
     */
33
    private $userPresence;
34
35
    /**
36
     * @var string
37
     */
38
    private $userPresenceByte;
39
40
    /**
41
     * @var int
42
     */
43
    private $counter;
44
45
    /**
46
     * @var string
47
     */
48
    private $counterBytes;
49
50
    /**
51
     * @var string
52
     */
53
    private $signature;
54
55
    public function __construct(array $data)
56
    {
57
        if (array_key_exists('errorCode', $data) && 0 !== $data['errorCode']) {
58
            throw new \InvalidArgumentException('Invalid response.');
59
        }
60
61
        $this->keyHandle = $this->retrieveKeyHandle($data);
62
        $this->clientData = $this->retrieveClientData($data);
63
        if ('navigator.id.getAssertion' !== $this->clientData->getType()) {
64
            throw new \InvalidArgumentException('Invalid response.');
65
        }
66
        list($this->userPresence, $this->userPresenceByte, $this->counter, $this->counterBytes, $this->signature) = $this->extractSignatureData($data);
67
    }
68
69
    public function getClientData(): ClientData
70
    {
71
        return $this->clientData;
72
    }
73
74
    public function getKeyHandle(): KeyHandler
75
    {
76
        return $this->keyHandle;
77
    }
78
79
    public function isUserPresence(): bool
80
    {
81
        return $this->userPresence;
82
    }
83
84
    public function getCounter(): int
85
    {
86
        return $this->counter;
87
    }
88
89
    public function getSignature(): string
90
    {
91
        return $this->signature;
92
    }
93
94
    /**
95
     * @throws \InvalidArgumentException
96
     */
97
    private function retrieveKeyHandle(array $data): KeyHandler
98
    {
99
        if (!array_key_exists('keyHandle', $data) || !\is_string($data['keyHandle'])) {
100
            throw new \InvalidArgumentException('Invalid response.');
101
        }
102
103
        return new KeyHandler(Base64Url::decode($data['keyHandle']));
104
    }
105
106
    /**
107
     * @throws \InvalidArgumentException
108
     */
109
    private function retrieveClientData(array $data): ClientData
110
    {
111
        if (!array_key_exists('clientData', $data) || !\is_string($data['clientData'])) {
112
            throw new \InvalidArgumentException('Invalid response.');
113
        }
114
115
        return new ClientData($data['clientData']);
116
    }
117
118
    /**
119
     * @throws \InvalidArgumentException
120
     */
121
    private function extractSignatureData(array $data): array
122
    {
123
        if (!array_key_exists('signatureData', $data) || !\is_string($data['signatureData'])) {
124
            throw new \InvalidArgumentException('Invalid response.');
125
        }
126
127
        $stream = fopen('php://memory', 'r+');
128
        if (false === $stream) {
129
            throw new \InvalidArgumentException('Unable to load the registration data.');
130
        }
131
        $signatureData = Base64Url::decode($data['signatureData']);
132
        fwrite($stream, $signatureData);
133
        rewind($stream);
134
135
        $userPresenceByte = fread($stream, 1);
136
        if (1 !== mb_strlen($userPresenceByte, '8bit')) {
137
            fclose($stream);
138
139
            throw new \InvalidArgumentException('Invalid response.');
140
        }
141
        $userPresence = (bool) \ord($userPresenceByte);
142
143
        $counterBytes = fread($stream, 4);
144
        if (4 !== mb_strlen($counterBytes, '8bit')) {
145
            fclose($stream);
146
147
            throw new \InvalidArgumentException('Invalid response.');
148
        }
149
        $counter = unpack('Nctr', $counterBytes)['ctr'];
150
        $signature = '';
151
        while (!feof($stream)) {
152
            $signature .= fread($stream, 1024);
153
        }
154
        fclose($stream);
155
156
        return [
157
            $userPresence,
158
            $userPresenceByte,
159
            $counter,
160
            $counterBytes,
161
            $signature,
162
        ];
163
    }
164
165
    public function isValid(SignatureRequest $request, ?int $currentCounter = null): bool
166
    {
167
        if (!hash_equals($request->getChallenge(), $this->clientData->getChallenge())) {
168
            return false;
169
        }
170
        if (!hash_equals($request->getApplicationId(), $this->clientData->getOrigin())) {
171
            return false;
172
        }
173
174
        if (null !== $currentCounter && $currentCounter >= $this->counter) {
175
            return false;
176
        }
177
178
        $dataToVerify = hash('sha256', $this->clientData->getOrigin(), true);
179
        $dataToVerify .= $this->userPresenceByte;
180
        $dataToVerify .= $this->counterBytes;
181
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
182
183
        $registeredKey = $request->getRegisteredKey($this->keyHandle);
184
185
        return 1 === openssl_verify($dataToVerify, $this->signature, $registeredKey->getPublicKeyAsPem(), OPENSSL_ALGO_SHA256);
186
    }
187
}
188