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

checkTpmPublicKeyMatchesAuthenticatorData()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 57.9073

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 10
cts 28
cp 0.357
crap 57.9073
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
    public const OID_TCG_KP_AIK_CERTIFICATE = '2.23.133.8.3';
37
38 1
    public function verify(AttestationStatementInterface $attStmt, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
39
    {
40
        // Verification procedure from https://www.w3.org/TR/webauthn/#tpm-attestation
41 1
        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
        // 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 1
        if (!$this->checkTpmPublicKeyMatchesAuthenticatorData($attStmt->getPubArea(), $authenticatorData)) {
52
            throw new VerificationException('Public key in pubArea does not match the key in authenticatorData');
53
        }
54
55
        // Concatenate authenticatorData and clientDataHash to form attToBeSigned.
56 1
        $attToBeSigned = $authenticatorData->getRaw()->getBinaryString() . $clientDataHash;
57
58
        //Validate that certInfo is valid:
59 1
        if (!$this->checkCertInfo($attStmt, $attStmt->getAlgorithm(), $attToBeSigned)) {
60
            throw new VerificationException('TPM certInfo is not valid.');
61
        }
62
63
        // If x5c is present, this indicates that the attestation type is not ECDAA. In this case:
64 1
        $x5c = $attStmt->getCertificates();
65 1
        if ($x5c !== null) {
66 1
            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
    }
73
74 1
    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
        // algorithm specified in alg.
78
79 1
        if (!isset($x5c[0])) {
80
            throw new VerificationException('Empty X5C in attestation.');
81
        }
82
        try {
83 1
            $cert = CertificateDetails::fromCertificate($x5c[0]);
84
85 1
            $valid = $cert->verifySignature($rawCertInfo->getBinaryString(), $signature->getBinaryString(), $signatureAlgorithm);
86
        } catch (WebAuthnException $e) {
87
            throw new VerificationException('Failed to process attestation certificate.', 0, $e);
88
        }
89
90 1
        if (!$valid) {
91
            throw new VerificationException('Attestation signature is invalid.');
92
        }
93
94
        // Verify that aikCert meets the requirements in §8.3.1 TPM attestation statement certificate requirements.
95 1
        $this->checkCertRequirements($cert);
96
97
        // 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 1
        FidoAaguidExtension::checkAaguidExtension($cert, $authenticatorData->getAaguid());
100
101
        // If successful, return attestation type AttCA and attestation trust path x5c.
102 1
        return new VerificationResult(AttestationType::ATT_CA, new CertificateTrustPath(...$x5c));
103
    }
104
105 1
    private function checkTpmPublicKeyMatchesAuthenticatorData(TpmPublic $pubArea, AuthenticatorData $authData): bool
106
    {
107 1
        $key = $authData->getKey();
108 1
        $params = $pubArea->getParameters();
109 1
        $publicdId = $pubArea->getUnique();
110 1
        if ($params instanceof TpmRsaParameters) {
111 1
            if (!($key instanceof RsaKey)) {
112
                return false;
113
            }
114
115 1
            if (!$params->getExponentAsBuffer()->equals($key->getExponent())) {
116
                return false;
117
            }
118 1
            if (!$publicdId instanceof TpmRsaPublicId) {
119
                return false;
120
            }
121 1
            if (!$publicdId->getModulus()->equals($key->getModulus())) {
122
                return false;
123
            }
124
125 1
            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
            }
139
140
            if ($key->getCurve() !== Ec2Key::CURVE_P256) {
141
                throw new UnsupportedException("Only P-256 NIST curves supported for TPM ECC keys");
142
            }
143
            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
151 1
    private function checkCertInfo(TpmAttestationStatement $attStmt, int $algorithm, string $attToBeSigned): bool
152
    {
153 1
        $certInfo = $attStmt->getCertInfo();
154 1
        $pubArea = $attStmt->getPubArea();
155
156 1
        $hash = new CoseHash($algorithm);
157
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 1
        if (!\hash_equals($certInfo->getExtraData()->getBinaryString(), $hash->hash($attToBeSigned))) {
164
            return false;
165
        }
166
167
        // 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 1
        if (!$pubArea->isValidPubInfoName($certInfo->getAttName())) {
171
            return false;
172
        }
173
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 1
        return true;
178
    }
179
180 1
    private function checkCertRequirements(CertificateDetailsInterface $cert): void
181
    {
182
        // 8.3.1. TPM Attestation Statement Certificate Requirement
183
184
        // Version MUST be set to 3.
185 1
        $version = $cert->getCertificateVersion();
186 1
        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
        }
189
190
        // Subject field MUST be set to empty.
191 1
        if ($cert->getSubject() !== '') {
192
            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 1
        $tpmManufacturer = $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_MANUFACTURER);
197 1
        $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_MODEL);
198 1
        $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 1
        if (!preg_match('~^id:[0-9A-Fa-f]{8}$~', $tpmManufacturer, $match)) {
202
            throw new VerificationException('Invalid TPM manufacturer attribute in subjectAlternateName of attestation certificate.');
203
        }
204
205 1
        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
        }
213
214
        // The Basic Constraints extension MUST have the CA component set to false.
215 1
        if ($cert->isCA() !== false) {
216
            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 1
    }
224
225 19
    public function getSupportedFormat(): AttestationFormatInterface
226
    {
227 19
        return new BuiltInAttestationFormat(
228 19
            TpmAttestationStatement::FORMAT_ID,
229 19
            TpmAttestationStatement::class,
230
            $this
231
        );
232
    }
233
}
234