Completed
Push — master ( 6f0ba5...dd01b0 )
by Florent
03:51
created

FidoU2FAttestationStatementSupport::isValid()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 5
nop 3
dl 0
loc 22
rs 9.4888
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\Fido2\AttestationStatement;
15
16
use CBOR\Decoder;
17
use CBOR\MapObject;
18
use CBOR\StringStream;
19
use U2FAuthentication\Fido2\AuthenticatorData;
20
21
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport
22
{
23
    private const CERTIFICATES_HASHES = [
24
        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
25
        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
26
        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
27
        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
28
        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
29
        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
30
    ];
31
32
    private $decoder;
33
34
    public function __construct(Decoder $decoder)
35
    {
36
        $this->decoder = $decoder;
37
    }
38
39
    public function name(): string
40
    {
41
        return 'fido-u2f';
42
    }
43
44
    public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
45
    {
46
        foreach (['sig', 'x5c'] as $key) {
47
            if (!$attestationStatement->has($key)) {
48
                throw new \InvalidArgumentException(\Safe\sprintf('The attestation statement value "%s" is missing.', $key));
49
            }
50
        }
51
        $certificates = $attestationStatement->get('x5c');
52
        if (!\is_array($certificates) || 1 !== \count($certificates)) {
53
            throw new \InvalidArgumentException('The attestation statement value "x5c" must be a list with one certificate.');
54
        }
55
56
        reset($certificates);
57
        $certificate = $this->getCertificateAsPem(current($certificates));
58
59
        $dataToVerify = "\0";
60
        $dataToVerify .= $authenticatorData->getRpIdHash();
61
        $dataToVerify .= $clientDataJSONHash;
62
        $dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId();
63
        $dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey());
64
65
        return 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $certificate, OPENSSL_ALGO_SHA256);
66
    }
67
68
    private function getCertificateAsPem(string $publicKey): string
69
    {
70
        $derCertificate = $this->unusedBytesFix($publicKey);
71
72
        $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
73
        $pemCert .= chunk_split(base64_encode($derCertificate), 64, PHP_EOL);
74
        $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL;
75
76
        return $pemCert;
77
    }
78
79
    private function extractPublicKey(?string $publicKey): string
80
    {
81
        if (!$publicKey) {
82
            throw new \InvalidArgumentException('The attestated credential data does not contain a valid public key.');
83
        }
84
85
        $publicKey = $this->decoder->decode(new StringStream($publicKey));
86
        if (!$publicKey instanceof MapObject) {
87
            throw new \InvalidArgumentException('The attestated credential data does not contain a valid public key.');
88
        }
89
90
        $publicKey = $publicKey->getNormalizedData();
91
        if (!array_key_exists(-2, $publicKey) || !\is_string($publicKey[-2]) || 32 !== mb_strlen($publicKey[-2], '8bit')) {
92
            throw new \InvalidArgumentException('The public key of the attestation statement is not valid.');
93
        }
94
        if (!array_key_exists(-3, $publicKey) || !\is_string($publicKey[-3]) || 32 !== mb_strlen($publicKey[-3], '8bit')) {
95
            throw new \InvalidArgumentException('The public key of the attestation statement is not valid.');
96
        }
97
98
        return "\x04".$publicKey[-2].$publicKey[-3];
99
    }
100
101
    private function unusedBytesFix(string $derCertificate): string
102
    {
103
        $certificateHash = hash('sha256', $derCertificate);
104
        if (\in_array($certificateHash, self::CERTIFICATES_HASHES, true)) {
105
            $derCertificate[mb_strlen($derCertificate, '8bit') - 257] = "\0";
106
        }
107
108
        return $derCertificate;
109
    }
110
}
111