Passed
Push — master ( 3266fb...94512e )
by Thomas
03:56 queued 01:13
created

FidoU2fAttestationVerifier::verify()   B

Complexity

Conditions 7
Paths 22

Size

Total Lines 54
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7.0957

Importance

Changes 0
Metric Value
cc 7
eloc 25
c 0
b 0
f 0
nc 22
nop 3
dl 0
loc 54
ccs 21
cts 24
cp 0.875
crap 7.0957
rs 8.5866

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace MadWizard\WebAuthn\Attestation\Verifier;
4
5
use MadWizard\WebAuthn\Attestation\AttestationType;
6
use MadWizard\WebAuthn\Attestation\AuthenticatorData;
7
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatInterface;
8
use MadWizard\WebAuthn\Attestation\Registry\BuiltInAttestationFormat;
9
use MadWizard\WebAuthn\Attestation\Statement\AttestationStatementInterface;
10
use MadWizard\WebAuthn\Attestation\Statement\FidoU2fAttestationStatement;
11
use MadWizard\WebAuthn\Attestation\TrustPath\CertificateTrustPath;
12
use MadWizard\WebAuthn\Crypto\Ec2Key;
13
use MadWizard\WebAuthn\Exception\VerificationException;
14
use function openssl_free_key;
15
use function openssl_pkey_get_details;
16
use function openssl_verify;
17
use const OPENSSL_ALGO_SHA256;
18
19
final class FidoU2fAttestationVerifier implements AttestationVerifierInterface
20
{
21 5
    public function verify(AttestationStatementInterface $attStmt, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
22
    {
23 5
        if (!($attStmt instanceof FidoU2fAttestationStatement)) {
24 1
            throw new VerificationException('Expecting FidoU2fAttestationStatement');
25
        }
26
27
        // AAGUID for U2F should be zeroes (not in WebAuthn spec but in FIDO2 CTAP specs and FIDO conformance tools)
28 4
        if (!$authenticatorData->getAaguid()->isZeroAaguid()) {
29
            throw new VerificationException('AAGUID should be zeroed for U2F attestations.');
30
        }
31
32
        // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding
33
        //    on it to extract the contained fields.
34
        // -> This is done in FidoU2fAttestationStatement
35
36
        // 2
37 4
        $key = $this->checkAttCertKey($attStmt);
38
39
        try {
40
            // 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey
41
            //    from authenticatorData.attestedCredentialData.
42 4
            $rpIdHash = $authenticatorData->getRpIdHash();
43 4
            $credentialId = $authenticatorData->getCredentialId();
44 4
            if ($credentialId === null) {
45
                throw new VerificationException('No credential id available.');
46
            }
47
48
            // 4
49 4
            $publicKeyU2f = $this->getPublicKeyU2f($authenticatorData);
50
51
            // 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
52 4
            $verificationData = "\x00" . $rpIdHash->getBinaryString() . $clientDataHash . $credentialId->getBinaryString() . $publicKeyU2f;
53
54
            // 6. Verify the sig using verificationData and certificate public key per [SEC1].
55 4
            $result = openssl_verify(
56 4
                $verificationData,
57 4
                $attStmt->getSignature()->getBinaryString(),
58 4
                $key,
59 4
                OPENSSL_ALGO_SHA256
60
            );
61
62 4
            if ($result === 1) {
63
                // 7. If successful, return attestation type Basic with the attestation trust path set to x5c.
64 3
                return new VerificationResult(AttestationType::BASIC, new CertificateTrustPath(...$attStmt->getCertificates()));
65
            }
66
67 1
            if ($result === 0) {
68 1
                throw new VerificationException('Signature invalid.');
69
            }
70
71
            throw new VerificationException('Failed to check signature');
72
        } finally {
73 4
            if (PHP_VERSION_ID < 80000) {
74 4
                openssl_free_key($key);
75
            }
76
        }
77
    }
78
79
    /**
80
     * @return resource
81
     *
82
     * @throws VerificationException
83
     */
84 4
    private function checkAttCertKey(FidoU2fAttestationStatement $attStmt)
85
    {
86
        // 2. Check that x5c has exactly one element and let attCert be that element. Let certificate public key
87
        //    be the public key conveyed by attCert. If certificate public key is not an Elliptic Curve (EC) public
88
        //    key over the P-256 curve, terminate this algorithm and return an appropriate error.
89 4
        $certificates = $attStmt->getCertificates();
90 4
        if (count($certificates) === 0) {
91
            throw new VerificationException('FIDO-U2F statements should contain exactly one certificate.');
92
        }
93
94 4
        $attCert = $certificates[0];
95
96 4
        $x509 = openssl_pkey_get_public($attCert->asPem());
97 4
        if ($x509 === false) {
98
            throw new VerificationException('Failed to parse x509 public key.');
99
        }
100
101 4
        $details = openssl_pkey_get_details($x509);
102
103 4
        if ($details === false || ($details['ec']['curve_name'] ?? null) !== 'prime256v1') {
104
            throw new VerificationException('Expecting first certificate to have P-256 EC key.');
105
        }
106 4
        return $x509;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $x509 also could return the type OpenSSLAsymmetricKey which is incompatible with the documented return type resource.
Loading history...
107
    }
108
109 4
    private function getPublicKeyU2f(AuthenticatorData $authData): string
110
    {
111
        // 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of [RFC8152]) to Raw ANSI X9.62 public
112
        //    key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key Representation Formats of [FIDO-Registry]).
113
        //
114
        //      Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey,
115
        //      and confirm its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm
116
        //
117
        //      Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey,
118
        //      and confirm its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm
119
        //      and return an appropriate error.
120
        //
121
122 4
        $credentialPublicKey = $authData->getKey();
123
124 4
        if (!($credentialPublicKey instanceof Ec2Key)) {
125
            throw new VerificationException('Public key is not EC2 key.');
126
        }
127
128 4
        $x = $credentialPublicKey->getX();
129 4
        $y = $credentialPublicKey->getY();
130
131 4
        if ($x->getLength() !== 32 || $y->getLength() !== 32) {
132
            throw new VerificationException('Unexpected key size.');
133
        }
134
135
        // Let publicKeyU2F be the concatenation 0x04 || x || y. This signifies uncompressed ECC key format.
136 4
        return "\x04" . $x->getBinaryString() . $y->getBinaryString();
137
    }
138
139 19
    public function getSupportedFormat(): AttestationFormatInterface
140
    {
141 19
        return new BuiltInAttestationFormat(
142 19
            FidoU2fAttestationStatement::FORMAT_ID,
143 19
            FidoU2fAttestationStatement::class,
144
            $this
145
        );
146
    }
147
}
148