Passed
Push — master ( 771420...d4ea3c )
by Florent
03:41
created

RegistrationResponse::unusedBytesFix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
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 Assert\Assertion;
17
use Base64Url\Base64Url;
18
use U2FAuthentication\CertificateToolbox;
19
20
class RegistrationResponse
21
{
22
    private const SUPPORTED_PROTOCOL_VERSIONS = ['U2F_V2'];
23
    private const PUBLIC_KEY_LENGTH = 65;
24
25
    /**
26
     * @var ClientData
27
     */
28
    private $clientData;
29
30
    /**
31
     * @var RegisteredKey
32
     */
33
    private $registeredKey;
34
35
    /**
36
     * @var string
37
     */
38
    private $signature;
39
40
    public function __construct(array $data)
41
    {
42
        Assertion::false(array_key_exists('errorCode', $data) && 0 !== $data['errorCode'], 'Invalid response.');
43
44
        $this->checkVersion($data);
45
        $clientData = $this->retrieveClientData($data);
46
        Assertion::eq('navigator.id.finishEnrollment', $clientData->getType(), 'Invalid response.');
47
        list($publicKey, $keyHandle, $pemCert, $signature) = $this->extractKeyData($data);
48
49
        $this->clientData = $clientData;
50
        $this->registeredKey = new RegisteredKey($data['version'], $keyHandle, $publicKey, $pemCert);
51
        $this->signature = $signature;
52
    }
53
54
    public function getClientData(): ClientData
55
    {
56
        return $this->clientData;
57
    }
58
59
    public function getRegisteredKey(): RegisteredKey
60
    {
61
        return $this->registeredKey;
62
    }
63
64
    public function getSignature(): string
65
    {
66
        return $this->signature;
67
    }
68
69
    private function retrieveClientData(array $data): ClientData
70
    {
71
        if (!array_key_exists('clientData', $data) || !\is_string($data['clientData'])) {
72
            throw new \InvalidArgumentException('Invalid response.');
73
        }
74
75
        return new ClientData($data['clientData']);
76
    }
77
78
    private function checkVersion(array $data): void
79
    {
80
        Assertion::false(!array_key_exists('version', $data) || !\is_string($data['version']), 'Invalid response.');
81
        Assertion::false(!\in_array($data['version'], self::SUPPORTED_PROTOCOL_VERSIONS, true), 'Unsupported protocol version.');
82
    }
83
84
    private function extractKeyData(array $data): array
85
    {
86
        Assertion::false(!array_key_exists('registrationData', $data) || !\is_string($data['registrationData']), 'Invalid response.');
87
        $stream = \Safe\fopen('php://memory', 'r+');
88
        $registrationData = Base64Url::decode($data['registrationData']);
89
        \Safe\fwrite($stream, $registrationData);
90
        \Safe\rewind($stream);
91
92
        $reservedByte = \Safe\fread($stream, 1);
93
        try {
94
            // 1 byte reserved with value x05
95
            Assertion::eq("\x05", $reservedByte, 'Bad reserved byte.');
96
97
            $publicKey = \Safe\fread($stream, self::PUBLIC_KEY_LENGTH); // 65 bytes for the public key
98
            Assertion::eq(self::PUBLIC_KEY_LENGTH, mb_strlen($publicKey, '8bit'), 'Bad public key length.');
99
100
            $keyHandleLength = \Safe\fread($stream, 1); // 1 byte for the key handle length
101
            Assertion::notEq(0, \ord($keyHandleLength), 'Bad key handle length.');
102
103
            $keyHandle = \Safe\fread($stream, \ord($keyHandleLength)); // x bytes for the key handle
104
            Assertion::eq(mb_strlen($keyHandle, '8bit'), \ord($keyHandleLength), 'Bad key handle.');
105
106
            $certHeader = \Safe\fread($stream, 4); // 4 bytes for the certificate header
107
            Assertion::eq(4, mb_strlen($certHeader, '8bit'), 'Bad certificate header.');
108
109
            $highOrder = \ord($certHeader[2]) << 8;
110
            $lowOrder = \ord($certHeader[3]);
111
            $certLength = $highOrder + $lowOrder;
112
            $certBody = \Safe\fread($stream, $certLength); // x bytes for the certificate
113
            Assertion::eq(mb_strlen($certBody, '8bit'), $certLength, 'Bad certificate.');
114
        } catch (\Throwable $throwable) {
115
            \Safe\fclose($stream);
116
            throw $throwable;
117
        }
118
119
        $pemCert = CertificateToolbox::convertDERToPEM($certHeader.$certBody);
120
        $signature = ''; // The rest is the signature
121
        while (!feof($stream)) {
122
            $signature .= \Safe\fread($stream, 1024);
123
        }
124
        \Safe\fclose($stream);
125
126
        return [
127
            new PublicKey($publicKey),
128
            new KeyHandler($keyHandle),
129
            $pemCert,
130
            $signature,
131
        ];
132
    }
133
134
    public function isValid(RegistrationRequest $challenge): bool
135
    {
136
        if (!hash_equals($challenge->getChallenge(), $this->clientData->getChallenge())) {
137
            return false;
138
        }
139
        if (!hash_equals($challenge->getApplicationId(), $this->clientData->getOrigin())) {
140
            return false;
141
        }
142
143
        $dataToVerify = "\0";
144
        $dataToVerify .= hash('sha256', $this->clientData->getOrigin(), true);
145
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
146
        $dataToVerify .= $this->registeredKey->getKeyHandler();
147
        $dataToVerify .= $this->registeredKey->getPublicKey();
148
149
        return 1 === openssl_verify($dataToVerify, $this->signature, $this->registeredKey->getAttestationCertificate(), OPENSSL_ALGO_SHA256);
150
    }
151
}
152