PackedAttestationVerifier::getSupportedFormat()   A
last analyzed

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\PackedAttestationStatement;
12
use MadWizard\WebAuthn\Attestation\TrustPath\CertificateTrustPath;
13
use MadWizard\WebAuthn\Attestation\TrustPath\EmptyTrustPath;
14
use MadWizard\WebAuthn\Exception\ParseException;
15
use MadWizard\WebAuthn\Exception\UnsupportedException;
16
use MadWizard\WebAuthn\Exception\VerificationException;
17
use MadWizard\WebAuthn\Exception\WebAuthnException;
18
use MadWizard\WebAuthn\Format\ByteBuffer;
19
use MadWizard\WebAuthn\Pki\CertificateDetails;
20
use MadWizard\WebAuthn\Pki\CertificateDetailsInterface;
21
use MadWizard\WebAuthn\Pki\X509Certificate;
22
23
final class PackedAttestationVerifier implements AttestationVerifierInterface
24
{
25 4
    public function verify(AttestationStatementInterface $attStmt, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
26
    {
27
        // Verification procedure from https://www.w3.org/TR/webauthn/#packed-attestation
28
29 4
        if (!($attStmt instanceof PackedAttestationStatement)) {
30 1
            throw new VerificationException('Expecting PackedAttestationStatement');
31
        }
32
33
        // 1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it
34
        //    to extract the contained fields.
35
        // -> This is done in PackedAttestationStatement
36
37
        // 2. If x5c is present, this indicates that the attestation type is not ECDAA.
38 3
        $x5c = $attStmt->getCertificates();
39 3
        if ($x5c !== null) {
40 1
            return $this->verifyX5C($x5c, $attStmt->getSignature(), $attStmt->getAlgorithm(), $authenticatorData, $clientDataHash);
41
        }
42
43
        // 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
44 2
        $ecdaaKeyId = $attStmt->getEcdaaKeyId();
45 2
        if ($ecdaaKeyId !== null) {
46 1
            throw new UnsupportedException('ECDAA is not supported by this library and is removed in later WebAuthn specifications.');
47
        }
48
49
        // 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
50 1
        return $this->verifySelf($attStmt->getSignature(), $attStmt->getAlgorithm(), $authenticatorData, $clientDataHash);
51
    }
52
53
    /**
54
     * @param X509Certificate[] $x5c
55
     */
56 1
    private function verifyX5c(array $x5c, ByteBuffer $signature, int $signatureAlgorithm, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
57
    {
58
        // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using
59
        // the attestation public key in attestnCert with the algorithm specified in alg.
60
61 1
        if (!isset($x5c[0])) {
62
            throw new VerificationException('Empty X5C in attestation.');
63
        }
64
        try {
65 1
            $cert = CertificateDetails::fromPem($x5c[0]->asPem());
66 1
            $verificationData = $authenticatorData->getRaw()->getBinaryString() . $clientDataHash;
67 1
            $valid = $cert->verifySignature($verificationData, $signature->getBinaryString(), $signatureAlgorithm);
68
        } catch (WebAuthnException $e) {
69
            throw new VerificationException('Failed to process attestation certificate.', 0, $e);
70
        }
71
72 1
        if (!$valid) {
73
            throw new VerificationException('Attestation signature is invalid.');
74
        }
75
76
        // Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
77 1
        $this->checkCertRequirements($cert);
78
79
        // If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData.
80 1
        FidoAaguidExtension::checkAaguidExtension($cert, $authenticatorData->getAaguid());
81
82
        // If successful, return attestation type Basic and attestation trust path x5c.
83 1
        return new VerificationResult(AttestationType::BASIC, new CertificateTrustPath(...$x5c));
84
    }
85
86 1
    private function verifySelf(ByteBuffer $signature, int $algorithm, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
87
    {
88
        // Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
89 1
        if (!$authenticatorData->hasKey()) {
90
            throw new VerificationException('No key in authenticator data.');
91
        }
92 1
        $key = $authenticatorData->getKey();
93 1
        if ($key->getAlgorithm() !== $algorithm) {
94
            throw new VerificationException(sprintf('Algorithm in packed attestation statement (%d) should match public key algorithm (%d)', $algorithm, $key->getAlgorithm()));
95
        }
96
97
        // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the credential public key with alg.
98
99
        try {
100 1
            $verificationData = $authenticatorData->getRaw()->getBinaryString() . $clientDataHash;
101 1
            $valid = $key->verifySignature(new ByteBuffer($verificationData), $signature);
102
        } catch (WebAuthnException $e) {
103
            throw new VerificationException('Error while verifying signature for packed attestation', 0, $e);
104
        }
105
106
        // If successful, return attestation type Self and empty attestation trust path.
107 1
        if ($valid) {
108 1
            return new VerificationResult(AttestationType::SELF, new EmptyTrustPath());
109
        }
110
111
        throw new VerificationException('Signature for self attestation could not be verified.');
112
    }
113
114 1
    private function checkCertRequirements(CertificateDetailsInterface $cert): void
115
    {
116
        // 8.2.1. Packed attestation statement certificate requirements
117
        //  The attestation certificate MUST have the following fields/extensions:
118
119
        // Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
120 1
        $version = $cert->getCertificateVersion();
121 1
        if ($version !== CertificateDetails::VERSION_3) {
122
            throw new VerificationException(sprintf('Attestation certificate version value is %s but should be %s (version 3).', $version ?? 'null', CertificateDetails::VERSION_3));
123
        }
124
125
        // Subject field MUST be set to:
126
        // [... Most fields are vendor specific, only subject-OU is specified in the spec ...]
127
        // Subject-OU Literal string "Authenticator Attestation" (UTF8String)
128
        try {
129 1
            $ou = $cert->getOrganizationalUnit();
130 1
            if ($ou !== 'Authenticator Attestation') {
131 1
                throw new VerificationException(sprintf("Subject-OU is '%s' but expecting 'Authenticator Attestation'.", $ou));
132
            }
133
        } catch (ParseException $e) {
134
            throw new VerificationException('Failed to parse Subject-OU in attestation certificate.', 0, $e);
135
        }
136
137
        // If the related attestation root certificate is used for multiple authenticator models, the Extension OID
138
        // 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET
139
        // STRING. The extension MUST NOT be marked as critical.
140
        // -> this is already verified in checkAaguidExtension
141
142
        // The Basic Constraints extension MUST have the CA component set to false.
143 1
        if ($cert->isCA() !== false) {
144
            throw new VerificationException('Attestation certificate should not the CA basic constraint set to false.');
145
        }
146
147
        // An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension
148
        // [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through authenticator
149
        // metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService].
150
        // -> not handled here.
151 1
    }
152
153 19
    public function getSupportedFormat(): AttestationFormatInterface
154
    {
155 19
        return new BuiltInAttestationFormat(
156 19
            PackedAttestationStatement::FORMAT_ID,
157 19
            PackedAttestationStatement::class,
158
            $this
159
        );
160
    }
161
}
162