Test Failed
Push — master ( 51d50e...3266fb )
by Thomas
06:40 queued 03:58
created

checkTpmPublicKeyMatchesAuthenticatorData()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 31.2496

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 27
c 1
b 0
f 0
nc 12
nop 2
dl 0
loc 44
ccs 11
cts 21
cp 0.5238
crap 31.2496
rs 6.6166

How to fix   Complexity   

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\Fido\FidoAaguidExtension;
8
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatInterface;
9
use MadWizard\WebAuthn\Attestation\Registry\BuiltInAttestationFormat;
10
use MadWizard\WebAuthn\Attestation\Statement\AttestationStatementInterface;
11
use MadWizard\WebAuthn\Attestation\Statement\TpmAttestationStatement;
12
use MadWizard\WebAuthn\Attestation\Tpm\TpmEccParameters;
13
use MadWizard\WebAuthn\Attestation\Tpm\TpmEccPublicId;
14
use MadWizard\WebAuthn\Attestation\Tpm\TpmPublic;
15
use MadWizard\WebAuthn\Attestation\Tpm\TpmRsaParameters;
16
use MadWizard\WebAuthn\Attestation\Tpm\TpmRsaPublicId;
17
use MadWizard\WebAuthn\Attestation\TrustPath\CertificateTrustPath;
18
use MadWizard\WebAuthn\Crypto\CoseHash;
19
use MadWizard\WebAuthn\Crypto\Ec2Key;
20
use MadWizard\WebAuthn\Crypto\RsaKey;
21
use MadWizard\WebAuthn\Exception\UnsupportedException;
22
use MadWizard\WebAuthn\Exception\VerificationException;
23
use MadWizard\WebAuthn\Exception\WebAuthnException;
24
use MadWizard\WebAuthn\Format\ByteBuffer;
25
use MadWizard\WebAuthn\Pki\CertificateDetails;
26
use MadWizard\WebAuthn\Pki\CertificateDetailsInterface;
27
28
final class TpmAttestationVerifier implements AttestationVerifierInterface
29
{
30
    public const OID_TCG_AT_TPM_MANUFACTURER = '2.23.133.2.1';
31
32
    public const OID_TCG_AT_TPM_MODEL = '2.23.133.2.2';
33
34
    public const OID_TCG_AT_TPM_VERSION = '2.23.133.2.3';
35
36 1
    public const OID_TCG_KP_AIK_CERTIFICATE = '2.23.133.8.3';
37
38
    public function verify(AttestationStatementInterface $attStmt, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
39 1
    {
40
        // Verification procedure from https://www.w3.org/TR/webauthn/#tpm-attestation
41
        if (!($attStmt instanceof TpmAttestationStatement)) {
42
            throw new VerificationException('Expecting TpmAttestationStatement.');
43
        }
44
45
        // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to
46
        // extract the contained fields.
47
        // -> this is done in TpmAttestationStatement
48
49 1
        // Verify that the public key specified by the parameters and unique fields of pubArea is identical to the
50
        // credentialPublicKey in the attestedCredentialData in authenticatorData.
51
        if (!$this->checkTpmPublicKeyMatchesAuthenticatorData($attStmt->getPubArea(), $authenticatorData)) {
52
            throw new VerificationException('Public key in pubArea does not match the key in authenticatorData');
53
        }
54 1
55
        // Concatenate authenticatorData and clientDataHash to form attToBeSigned.
56
        $attToBeSigned = $authenticatorData->getRaw()->getBinaryString() . $clientDataHash;
57 1
58
        //Validate that certInfo is valid:
59
        if (!$this->checkCertInfo($attStmt, $attStmt->getAlgorithm(), $attToBeSigned)) {
60
            throw new VerificationException('TPM certInfo is not valid.');
61
        }
62 1
63 1
        // If x5c is present, this indicates that the attestation type is not ECDAA. In this case:
64 1
        $x5c = $attStmt->getCertificates();
65
        if ($x5c !== null) {
66
            return  $this->verifyX5C($x5c, $attStmt->getSignature(), $attStmt->getAlgorithm(), $attStmt->getRawCertInfo(), $authenticatorData);
67
        }
68
69
        // Either x5c or ECDAA is set, but only x5c is supported by this library. So if we reach this the statement
70
        // is unsupported.
71
        throw new UnsupportedException('ECDAA is not supported by this library and is removed in later WebAuthn specifications.');
72 1
    }
73
74
    private function verifyX5c(array $x5c, ByteBuffer $signature, int $signatureAlgorithm, ByteBuffer $rawCertInfo, AuthenticatorData $authenticatorData): VerificationResult
75
    {
76
        // Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the
77 1
        // algorithm specified in alg.
78
79
        if (!isset($x5c[0])) {
80
            throw new VerificationException('Empty X5C in attestation.');
81 1
        }
82
        try {
83 1
            $cert = CertificateDetails::fromCertificate($x5c[0]);
84
85
            $valid = $cert->verifySignature($rawCertInfo->getBinaryString(), $signature->getBinaryString(), $signatureAlgorithm);
86
        } catch (WebAuthnException $e) {
87
            throw new VerificationException('Failed to process attestation certificate.', 0, $e);
88 1
        }
89
90
        if (!$valid) {
91
            throw new VerificationException('Attestation signature is invalid.');
92
        }
93 1
94
        // Verify that aikCert meets the requirements in §8.3.1 TPM attestation statement certificate requirements.
95
        $this->checkCertRequirements($cert);
96
97 1
        // If aikCert contains an extension with OID 1 3 6 1 4 1 45724 1 1 4 (id-fido-gen-ce-aaguid) verify that the
98
        // value of this extension matches the aaguid in authenticatorData.
99
        FidoAaguidExtension::checkAaguidExtension($cert, $authenticatorData->getAaguid());
100 1
101
        // If successful, return attestation type AttCA and attestation trust path x5c.
102
        return new VerificationResult(AttestationType::ATT_CA, new CertificateTrustPath(...$x5c));
103 1
    }
104
105 1
    private function checkTpmPublicKeyMatchesAuthenticatorData(TpmPublic $pubArea, AuthenticatorData $authData): bool
106 1
    {
107 1
        $key = $authData->getKey();
108 1
        $params = $pubArea->getParameters();
109
        $publicdId = $pubArea->getUnique();
110
        if ($params instanceof TpmRsaParameters) {
111
            if (!($key instanceof RsaKey)) {
112 1
                return false;
113
            }
114
115
            if (!$params->getExponentAsBuffer()->equals($key->getExponent())) {
116 1
                return false;
117
            }
118
            if (!$publicdId instanceof TpmRsaPublicId) {
119
                return false;
120 1
            }
121
            if (!$publicdId->getModulus()->equals($key->getModulus())) {
122
                return false;
123
            }
124
125
            return true;
126
        }
127
        if ($params instanceof TpmEccParameters) {
128
            if (!($key instanceof Ec2Key)) {
129
                return false;
130
            }
131
            if (!$publicdId instanceof TpmEccPublicId) {
132
                return false;
133
            }
134
135
            if (!$publicdId->getX()->equals($key->getX()) ||
136
                !$publicdId->getY()->equals($key->getY())) {
137
                return false;
138 1
            }
139
140 1
            if ($key->getCurve() !== Ec2Key::CURVE_P256) {
141 1
                throw new UnsupportedException("Only P-256 NIST curves supported for TPM ECC keys");
142
            }
143 1
            if ($params->getCurveId() !== TpmEccParameters::TPM_ECC_NIST_P256) {
144
                return false;
145
            }
146
            return true;
147
        }
148
        throw new VerificationException('Unsupported TPM parameters type');
149
    }
150 1
151
    private function checkCertInfo(TpmAttestationStatement $attStmt, int $algorithm, string $attToBeSigned): bool
152
    {
153
        $certInfo = $attStmt->getCertInfo();
154
        $pubArea = $attStmt->getPubArea();
155
156
        $hash = new CoseHash($algorithm);
157 1
158
        // Verify that magic is set to TPM_GENERATED_VALUE.
159
        // Verify that type is set to TPM_ST_ATTEST_CERTIFY.
160
        // -> both done by TpmAttest class.
161
162
        // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
163
        if (!\hash_equals($certInfo->getExtraData()->getBinaryString(), $hash->hash($attToBeSigned))) {
164 1
            return false;
165
        }
166
167 1
        // Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in [TPMv2-Part2] section 10.12.3,
168
        // whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of
169
        // pubArea using the procedure specified in [TPMv2-Part1] section 16.
170
        if (!$pubArea->isValidPubInfoName($certInfo->getAttName())) {
171
            return false;
172 1
        }
173 1
174
        // Note that the remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e.,
175
        // qualifiedSigner, clockInfo and firmwareVersion are ignored. These fields MAY be used as an input to risk engines.
176
        // -> not used here
177
        return true;
178 1
    }
179
180
    private function checkCertRequirements(CertificateDetailsInterface $cert): void
181
    {
182
        // 8.3.1. TPM Attestation Statement Certificate Requirement
183 1
184 1
        // Version MUST be set to 3.
185 1
        $version = $cert->getCertificateVersion();
186
        if ($version !== CertificateDetails::VERSION_3) {
187
            throw new VerificationException(sprintf('Attestation certificate version value is %s but should be %s (version 3).', $version ?? 'null', CertificateDetails::VERSION_3));
188 1
        }
189
190
        // Subject field MUST be set to empty.
191
        if ($cert->getSubject() !== '') {
192 1
            throw new VerificationException('Subject of attestation certificate should be empty.');
193
        }
194
195
        // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
196
        $tpmManufacturer = $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_MANUFACTURER);
197 1
        $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_MODEL);
198
        $tpmVersion = $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_VERSION);
199
200
        // Syntax is id:AABBCCDDEE where AABCCDDEE is the 4 byte manufacturer ID in hex.
201
        if (!preg_match('~^id:[0-9A-Fa-f]{8}$~', $tpmManufacturer, $match)) {
202 1
            throw new VerificationException('Invalid TPM manufacturer attribute in subjectAlternateName of attestation certificate.');
203
        }
204
205
        if (!preg_match('~^id:[0-9A-Fa-f]{2,}$~', $tpmVersion, $match)) {
206
            throw new VerificationException('Invalid TPM version attribute in subjectAlternateName of attestation certificate.');
207
        }
208
209
        // The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
210 1
        if (!$cert->extendedKeyUsageContains(self::OID_TCG_KP_AIK_CERTIFICATE)) {
211
            throw new VerificationException('Extended key usage of attestation certificate should contain tcg-kp-AIKCertificate.');
212 19
        }
213
214 19
        // The Basic Constraints extension MUST have the CA component set to false.
215 19
        if ($cert->isCA() !== false) {
216 19
            throw new VerificationException('Attestation certificate should not the CA basic constraint set to false.');
217
        }
218
219
        // An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension
220
        // [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through metadata services.
221
        // See, for example, the FIDO Metadata Service [FIDOMetadataService].
222
        // -> not handled here.
223
    }
224
225
    public function getSupportedFormat(): AttestationFormatInterface
226
    {
227
        return new BuiltInAttestationFormat(
228
            TpmAttestationStatement::FORMAT_ID,
229
            TpmAttestationStatement::class,
230
            $this
231
        );
232
    }
233
}
234