Passed
Push — master ( 8d86ce...bd9937 )
by Thomas
07:31
created

FidoU2fAttestationVerifier::getSupportedFormat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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