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

FidoU2fAttestationVerifier::getSupportedFormat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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