Passed
Push — master ( 3569aa...ab7ae0 )
by Florent
02:27
created

FidoU2FAttestationStatement::isValid()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 7
nop 3
dl 0
loc 29
rs 8.8333
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 U2FAuthentication\Fido2\AuthenticatorData;
17
use U2FAuthentication\Fido2\CollectedClientData;
18
19
final class FidoU2FAttestationStatement implements AttestationStatementSupport
20
{
21
    private const CERTIFICATES_HASHES = [
22
        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
23
        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
24
        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
25
        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
26
        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
27
        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
28
    ];
29
30
    /**
31
     * @var string[]
32
     */
33
    private $attestationCertificates = [];
34
35
    /**
36
     * @param string[] $attestationCertificates
37
     */
38
    public function __construct(array $attestationCertificates = [])
39
    {
40
        $this->attestationCertificates = $attestationCertificates;
41
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46
    public function name(): string
47
    {
48
        return 'fido-u2f';
49
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54
    public function isValid(AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData, CollectedClientData $collectedClientData): bool
55
    {
56
        foreach (['sig', 'x5c'] as $key) {
57
            if (!$attestationStatement->has($key)) {
58
                throw new \InvalidArgumentException(sprintf('The attestation statement value "%s" is missing.', $key));
59
            }
60
        }
61
        $x5c = $attestationStatement->get('x5c');
62
        if (!\is_array($x5c) || empty($x5c)) {
63
            throw new \InvalidArgumentException('The attestation statement value "x5c" must contain at least one certificate.');
64
        }
65
66
        reset($x5c);
67
        $x5c = $this->getPublicKeyAsPem(current($x5c));
68
69
        if (!empty($this->attestationCertificates) && true !== openssl_x509_checkpurpose($x5c, X509_PURPOSE_ANY, $this->attestationCertificates)) {
70
            return false;
71
        }
72
73
        $dataToVerify = "\0";
74
        $dataToVerify .= $authenticatorData->getRpIdHash();
75
        $dataToVerify .= hash('sha256', $collectedClientData->getRawData(), true);
76
        $dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId();
77
        $dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey());
78
        dump($dataToVerify);
79
        dump($attestationStatement->get('sig'));
80
        dump($x5c);
81
82
        return 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $x5c, OPENSSL_ALGO_SHA256);
83
    }
84
85
    private function getPublicKeyAsPem(string $publicKey): string
86
    {
87
        $derCertificate = $this->unusedBytesFix($publicKey);
88
        $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
89
        $pemCert .= chunk_split(base64_encode($derCertificate), 64, PHP_EOL);
90
        $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL;
91
92
        return $pemCert;
93
    }
94
95
    private function extractPublicKey(?array $publicKey): string
96
    {
97
        if (!\is_array($publicKey) || !array_key_exists(-2, $publicKey) || !\is_string($publicKey[-2]) || 32 !== mb_strlen($publicKey[-2], '8bit')) {
98
            throw new \InvalidArgumentException();
99
        }
100
        if (!array_key_exists(-3, $publicKey) || !\is_string($publicKey[-3]) || 32 !== mb_strlen($publicKey[-3], '8bit')) {
101
            throw new \InvalidArgumentException();
102
        }
103
104
        return "\x04".$publicKey[-2].$publicKey[-3];
105
    }
106
107
    private function unusedBytesFix(string $derCertificate): string
108
    {
109
        $certificateHash = hash('sha256', $derCertificate);
110
        if (\in_array($certificateHash, self::CERTIFICATES_HASHES, true)) {
111
            $derCertificate[mb_strlen($derCertificate, '8bit') - 257] = "\0";
112
        }
113
114
        return $derCertificate;
115
    }
116
}
117