Passed
Pull Request — master (#18)
by
unknown
08:11
created

TpmAttestationVerifier::getSupportedFormat()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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