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

TpmAttestationVerifier   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 203
Duplicated Lines 0 %

Test Coverage

Coverage 64.38%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 83
c 1
b 0
f 0
dl 0
loc 203
ccs 47
cts 73
cp 0.6438
rs 9.76
wmc 33

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getSupportedFormat() 0 6 1
A checkCertInfo() 0 27 3
A verifyX5c() 0 29 4
C checkTpmPublicKeyMatchesAuthenticatorData() 0 44 13
B checkCertRequirements() 0 37 7
A verify() 0 34 5
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