Passed
Pull Request — master (#18)
by
unknown
08:11
created

FidoU2fAttestationVerifier::verify()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 53
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6.0798

Importance

Changes 0
Metric Value
cc 6
eloc 24
c 0
b 0
f 0
nc 12
nop 3
dl 0
loc 53
ccs 20
cts 23
cp 0.8696
crap 6.0798
rs 8.9137

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
            openssl_free_key($key);
74
        }
75
    }
76
77
    /**
78
     * @return resource
79
     *
80
     * @throws VerificationException
81
     */
82 4
    private function checkAttCertKey(FidoU2fAttestationStatement $attStmt)
83
    {
84
        // 2. Check that x5c has exactly one element and let attCert be that element. Let certificate public key
85
        //    be the public key conveyed by attCert. If certificate public key is not an Elliptic Curve (EC) public
86
        //    key over the P-256 curve, terminate this algorithm and return an appropriate error.
87 4
        $certificates = $attStmt->getCertificates();
88 4
        if (count($certificates) === 0) {
89
            throw new VerificationException('FIDO-U2F statements should contain exactly one certificate.');
90
        }
91
92 4
        $attCert = $certificates[0];
93
94 4
        $x509 = openssl_pkey_get_public($attCert->asPem());
95 4
        if ($x509 === false) {
96
            throw new VerificationException('Failed to parse x509 public key.');
97
        }
98
99 4
        $details = openssl_pkey_get_details($x509);
100
101 4
        if ($details === false || ($details['ec']['curve_name'] ?? null) !== 'prime256v1') {
102
            throw new VerificationException('Expecting first certificate to have P-256 EC key.');
103
        }
104 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...
105
    }
106
107 4
    private function getPublicKeyU2f(AuthenticatorData $authData): string
108
    {
109
        // 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of [RFC8152]) to Raw ANSI X9.62 public
110
        //    key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key Representation Formats of [FIDO-Registry]).
111
        //
112
        //      Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey,
113
        //      and confirm its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm
114
        //
115
        //      Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey,
116
        //      and confirm its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm
117
        //      and return an appropriate error.
118
        //
119
120 4
        $credentialPublicKey = $authData->getKey();
121
122 4
        if (!($credentialPublicKey instanceof Ec2Key)) {
123
            throw new VerificationException('Public key is not EC2 key.');
124
        }
125
126 4
        $x = $credentialPublicKey->getX();
127 4
        $y = $credentialPublicKey->getY();
128
129 4
        if ($x->getLength() !== 32 || $y->getLength() !== 32) {
130
            throw new VerificationException('Unexpected key size.');
131
        }
132
133
        // Let publicKeyU2F be the concatenation 0x04 || x || y. This signifies uncompressed ECC key format.
134 4
        return "\x04" . $x->getBinaryString() . $y->getBinaryString();
135
    }
136
137 19
    public function getSupportedFormat(): AttestationFormatInterface
138
    {
139 19
        return new BuiltInAttestationFormat(
140 19
            FidoU2fAttestationStatement::FORMAT_ID,
141 19
            FidoU2fAttestationStatement::class,
142
            $this
143
        );
144
    }
145
}
146