Test Failed
Push — master ( 9eb68a...5271f3 )
by Thomas
02:48
created

AbstractVerifier::processExtensions()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 19
c 0
b 0
f 0
nc 16
nop 4
dl 0
loc 31
ccs 0
cts 0
cp 0
crap 42
rs 9.0111
1
<?php
2
3
namespace MadWizard\WebAuthn\Server;
4
5
use MadWizard\WebAuthn\Attestation\AuthenticatorData;
6
use MadWizard\WebAuthn\Dom\AuthenticatorResponseInterface;
7
use MadWizard\WebAuthn\Dom\PublicKeyCredentialInterface;
8
use MadWizard\WebAuthn\Dom\TokenBindingStatus;
9
use MadWizard\WebAuthn\Exception\DataValidationException;
10
use MadWizard\WebAuthn\Exception\ParseException;
11
use MadWizard\WebAuthn\Exception\VerificationException;
12
use MadWizard\WebAuthn\Extension\ExtensionProcessingContext;
13
use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface;
14
use MadWizard\WebAuthn\Extension\ExtensionResponse;
15
use MadWizard\WebAuthn\Format\CborMap;
16
use MadWizard\WebAuthn\Format\DataValidator;
17 10
use MadWizard\WebAuthn\Web\Origin;
18
19
abstract class AbstractVerifier
20 10
{
21
    /**
22
     * @var ExtensionRegistryInterface
23
     */
24
    protected $extensionRegistry;
25 10
26
    public function __construct(ExtensionRegistryInterface $extensionRegistry)
27
    {
28 6
        $this->extensionRegistry = $extensionRegistry;
29
    }
30
31 6
    // TODO: move?
32 6
    protected function verifyOrigin(string $origin, Origin $rpOrigin): bool
33
    {
34
        try {
35 5
            $clientOrigin = Origin::parse($origin);
36
        } catch (ParseException $e) {
37
            throw new VerificationException('Client has specified an invalid origin.', 0, $e);
38
        }
39
40
        return $clientOrigin->equals($rpOrigin);
41
    }
42
43
    protected function verifyRpIdHash(AuthenticatorData $authData, AbstractContext $context, ExtensionProcessingContext $extensionContext)
44 5
    {
45
        $effectiveRpId = $context->getRpId();
46
        $overruledRpId = $extensionContext->getOverruledRpId();
47
        if ($overruledRpId !== null) {
48 5
            $effectiveRpId = $overruledRpId;
49
        }
50
        $validHash = hash('sha256', $effectiveRpId, true);
51
        return hash_equals($validHash, $authData->getRpIdHash()->getBinaryString());
52
    }
53
54 5
    protected function verifyUser(AuthenticatorData $authData, AbstractContext $context)
55
    {
56
        // Reg 10/11, Auth 12/13
57 13
58
        // Reg 7.1 #10 Verify that the User Present bit of the flags in authData is set.
59
        // Note: isUserPresenceRequired is true by default to conform to the WebAuthn spec.
60 13
        // It can be set to false manually when required to pass full FIDO2 compliance, which conflicts the
61 13
        // WebAuthn spec.
62
        // @see https://github.com/fido-alliance/conformance-tools-issues/issues/434
63 13
        if (!$authData->isUserPresent() && $context->isUserPresenceRequired()) {
64
            return false;
65
        }
66
67
        if ($context->isUserVerificationRequired()) {
68 13
            // Reg 7.1 #11 If user verification is required for this registration, verify that the User Verified bit of the
69
            // flags in authData is set.
70 1
            return $authData->isUserVerified();
71 1
        }
72
73 12
        return true;
74
    }
75 4
76
    protected function validateClientData(array $clientData)
77
    {
78 4
        try {
79 4
            DataValidator::checkArray(
80
                $clientData,
81 4
                [
82
                    'type' => 'string',
83
                    'challenge' => 'string',
84 4
                    'origin' => 'string',
85
                    'tokenBinding' => '?array',
86 1
                ],
87 1
                false
88
            );
89
        } catch (DataValidationException $e) {
90 3
            throw new VerificationException('Missing data or unexpected type in clientDataJSON', 0, $e);
91
        }
92
    }
93 3
94 1
    protected function checkTokenBinding(array $tokenBinding)
95
    {
96
        try {
97 2
            DataValidator::checkArray(
98 1
                $tokenBinding,
99
                [
100 1
                    'status' => 'string',
101
                    'id' => '?string',
102 1
                ],
103
                false
104 1
            );
105
        } catch (DataValidationException $e) {
106
            throw new VerificationException('Missing data or unexpected type in tokenBinding', 0, $e);
107
        }
108
109
        $status = $tokenBinding['status'];
110
        // $id = $tokenBinding['id'] ?? null;
111
112
        if (!TokenBindingStatus::isValidValue($status)) {
113
            throw new VerificationException(sprintf("Token binding status '%s' is invalid", $status));
114
        }
115
        // NOTE: token binding is currently not supported by this library
116
        if ($status === TokenBindingStatus::PRESENT) {
117
            throw new VerificationException('Token binding is not supported by the relying party.');
118
        }
119
    }
120
121
    protected function getClientDataHash(AuthenticatorResponseInterface $response)
122
    {
123
        return hash('sha256', $response->getClientDataJson(), true);
124
    }
125
126
    protected function processExtensions(PublicKeyCredentialInterface $credential, AuthenticatorData $authData, AbstractContext $operationContext, string $operation): ExtensionProcessingContext
127
    {
128
        $authExtensionOutputs = $authData->hasExtensionData() ? $authData->getExtensionData() : new CborMap();
129
130
        // TODO: check for unwanted $authExtensionOutputs
131
        $extensionContext = new ExtensionProcessingContext($operation);
132
133
        $results = $credential->getClientExtensionResults();
134
        $inputs = [];
135
        foreach ($operationContext->getExtensionInputs() as $input) {
136
            $inputs[$input->getIdentifier()] = $input;
137
        }
138
139
        foreach ($results as $id => $result) {
140
            $input = $inputs[$id] ?? null;
141
            if ($input === null) {
142
                throw new VerificationException(sprintf('Extension "%s" is present in clientExtensionResults but was not used in the input.', $id));
143
            }
144
            $extension = $this->extensionRegistry->getExtension($id);
145
146
            $extensionResponse = new ExtensionResponse($id);
147
            $extensionResponse->setClientExtensionOutput($result);
148
            if ($authExtensionOutputs->has($id)) {
149
                $extensionResponse->setAuthenticatorExtensionOutput($authExtensionOutputs->get($id));
150
            }
151
            $output = $extension->parseResponse($extensionResponse);
152
            $extensionContext->addOutput($output);
153
            $extension->processExtension($input, $output, $extensionContext);
154
        }
155
156
        return $extensionContext;
157
    }
158
}
159