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

FidoU2fAttestationVerifier   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 126
Duplicated Lines 0 %

Test Coverage

Coverage 83.67%

Importance

Changes 0
Metric Value
eloc 48
c 0
b 0
f 0
dl 0
loc 126
ccs 41
cts 49
cp 0.8367
rs 10
wmc 17

4 Methods

Rating   Name   Duplication   Size   Complexity  
B verify() 0 54 7
A checkAttCertKey() 0 23 5
A getSupportedFormat() 0 6 1
A getPublicKeyU2f() 0 28 4
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