Completed
Push — master ( 698a0f...8180db )
by Florent
01:57
created

extractPublicKey()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 1
nop 1
dl 0
loc 12
rs 9.6111
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 Assert\Assertion;
17
use CBOR\Decoder;
18
use CBOR\MapObject;
19
use CBOR\StringStream;
20
use U2FAuthentication\Fido2\AuthenticatorData;
21
22
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport
23
{
24
    private const CERTIFICATES_HASHES = [
25
        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
26
        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
27
        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
28
        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
29
        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
30
        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
31
    ];
32
33
    private $decoder;
34
35
    public function __construct(Decoder $decoder)
36
    {
37
        $this->decoder = $decoder;
38
    }
39
40
    public function name(): string
41
    {
42
        return 'fido-u2f';
43
    }
44
45
    public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
46
    {
47
        foreach (['sig', 'x5c'] as $key) {
48
            Assertion::true($attestationStatement->has($key), \Safe\sprintf('The attestation statement value "%s" is missing.', $key));
49
        }
50
        $certificates = $attestationStatement->get('x5c');
51
        Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
52
        Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.');
53
54
        reset($certificates);
55
        $certificate = $this->getCertificateAsPem(current($certificates));
56
57
        $dataToVerify = "\0";
58
        $dataToVerify .= $authenticatorData->getRpIdHash();
59
        $dataToVerify .= $clientDataJSONHash;
60
        $dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId();
61
        $dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey());
62
63
        return 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $certificate, OPENSSL_ALGO_SHA256);
64
    }
65
66
    private function getCertificateAsPem(string $publicKey): string
67
    {
68
        $derCertificate = $this->unusedBytesFix($publicKey);
69
70
        $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
71
        $pemCert .= chunk_split(base64_encode($derCertificate), 64, PHP_EOL);
72
        $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL;
73
74
        return $pemCert;
75
    }
76
77
    private function extractPublicKey(?string $publicKey): string
78
    {
79
        Assertion::notNull($publicKey, 'The attestated credential data does not contain a valid public key.');
80
81
        $publicKey = $this->decoder->decode(new StringStream($publicKey));
82
        Assertion::isInstanceOf($publicKey, MapObject::class, 'The attestated credential data does not contain a valid public key.');
83
84
        $publicKey = $publicKey->getNormalizedData();
85
        Assertion::false(!array_key_exists(-2, $publicKey) || !\is_string($publicKey[-2]) || 32 !== mb_strlen($publicKey[-2], '8bit'), 'The public key of the attestation statement is not valid.');
1 ignored issue
show
Bug introduced by
It seems like $publicKey can also be of type null; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

85
        Assertion::false(!array_key_exists(-2, /** @scrutinizer ignore-type */ $publicKey) || !\is_string($publicKey[-2]) || 32 !== mb_strlen($publicKey[-2], '8bit'), 'The public key of the attestation statement is not valid.');
Loading history...
86
        Assertion::false(!array_key_exists(-3, $publicKey) || !\is_string($publicKey[-3]) || 32 !== mb_strlen($publicKey[-3], '8bit'), 'The public key of the attestation statement is not valid.');
87
88
        return "\x04".$publicKey[-2].$publicKey[-3];
89
    }
90
91
    private function unusedBytesFix(string $derCertificate): string
92
    {
93
        $certificateHash = hash('sha256', $derCertificate);
94
        if (\in_array($certificateHash, self::CERTIFICATES_HASHES, true)) {
95
            $derCertificate[mb_strlen($derCertificate, '8bit') - 257] = "\0";
96
        }
97
98
        return $derCertificate;
99
    }
100
}
101