checkTpmPublicKeyMatchesAuthenticatorData()   C
last analyzed

Complexity

Conditions 13
Paths 12

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 67.9167

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 13
eloc 32
c 2
b 0
f 0
nc 12
nop 2
dl 0
loc 52
ccs 10
cts 32
cp 0.3125
crap 67.9167
rs 6.6166

How to fix   Long Method    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
            $curveMap = [
141
                TpmEccParameters::TPM_ECC_NIST_P256 => Ec2Key::CURVE_P256,
142
                TpmEccParameters::TPM_ECC_NIST_P384 => Ec2Key::CURVE_P384,
143
                TpmEccParameters::TPM_ECC_NIST_P521 => Ec2Key::CURVE_P521,
144
            ];
145
146
            $expectedCurve = $curveMap[$params->getCurveId()] ?? null;
147
            if ($expectedCurve === null) {
148
                throw new UnsupportedException("Unsupported curve for TPM ECC keys");
149
            }
150
151
            if ($key->getCurve() !== $expectedCurve) {
152
                return false;
153
            }
154
            return true;
155
        }
156
        throw new VerificationException('Unsupported TPM parameters type');
157
    }
158
159 1
    private function checkCertInfo(TpmAttestationStatement $attStmt, int $algorithm, string $attToBeSigned): bool
160
    {
161 1
        $certInfo = $attStmt->getCertInfo();
162 1
        $pubArea = $attStmt->getPubArea();
163
164 1
        $hash = new CoseHash($algorithm);
165
166
        // Verify that magic is set to TPM_GENERATED_VALUE.
167
        // Verify that type is set to TPM_ST_ATTEST_CERTIFY.
168
        // -> both done by TpmAttest class.
169
170
        // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
171 1
        if (!\hash_equals($certInfo->getExtraData()->getBinaryString(), $hash->hash($attToBeSigned))) {
172
            return false;
173
        }
174
175
        // Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in [TPMv2-Part2] section 10.12.3,
176
        // whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of
177
        // pubArea using the procedure specified in [TPMv2-Part1] section 16.
178 1
        if (!$pubArea->isValidPubInfoName($certInfo->getAttName())) {
179
            return false;
180
        }
181
182
        // Note that the remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e.,
183
        // qualifiedSigner, clockInfo and firmwareVersion are ignored. These fields MAY be used as an input to risk engines.
184
        // -> not used here
185 1
        return true;
186
    }
187
188 1
    private function checkCertRequirements(CertificateDetailsInterface $cert): void
189
    {
190
        // 8.3.1. TPM Attestation Statement Certificate Requirement
191
192
        // Version MUST be set to 3.
193 1
        $version = $cert->getCertificateVersion();
194 1
        if ($version !== CertificateDetails::VERSION_3) {
195
            throw new VerificationException(sprintf('Attestation certificate version value is %s but should be %s (version 3).', $version ?? 'null', CertificateDetails::VERSION_3));
196
        }
197
198
        // Subject field MUST be set to empty.
199 1
        if ($cert->getSubject() !== '') {
200
            throw new VerificationException('Subject of attestation certificate should be empty.');
201
        }
202
203
        // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9.
204 1
        $tpmManufacturer = $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_MANUFACTURER);
205 1
        $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_MODEL);
206 1
        $tpmVersion = $cert->getSubjectAlternateNameDN(self::OID_TCG_AT_TPM_VERSION);
207
208
        // Syntax is id:AABBCCDDEE where AABCCDDEE is the 4 byte manufacturer ID in hex.
209 1
        if (!preg_match('~^id:[0-9A-Fa-f]{8}$~', $tpmManufacturer, $match)) {
210
            throw new VerificationException('Invalid TPM manufacturer attribute in subjectAlternateName of attestation certificate.');
211
        }
212
213 1
        if (!preg_match('~^id:[0-9A-Fa-f]{2,}$~', $tpmVersion, $match)) {
214
            throw new VerificationException('Invalid TPM version attribute in subjectAlternateName of attestation certificate.');
215
        }
216
217
        // The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
218 1
        if (!$cert->extendedKeyUsageContains(self::OID_TCG_KP_AIK_CERTIFICATE)) {
219
            throw new VerificationException('Extended key usage of attestation certificate should contain tcg-kp-AIKCertificate.');
220
        }
221
222
        // The Basic Constraints extension MUST have the CA component set to false.
223 1
        if ($cert->isCA() !== false) {
224
            throw new VerificationException('Attestation certificate should not the CA basic constraint set to false.');
225
        }
226
227
        // An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension
228
        // [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through metadata services.
229
        // See, for example, the FIDO Metadata Service [FIDOMetadataService].
230
        // -> not handled here.
231 1
    }
232
233 19
    public function getSupportedFormat(): AttestationFormatInterface
234
    {
235 19
        return new BuiltInAttestationFormat(
236 19
            TpmAttestationStatement::FORMAT_ID,
237 19
            TpmAttestationStatement::class,
238
            $this
239
        );
240
    }
241
}
242