Completed
Push — master ( d7d555...8d86ce )
by Thomas
06:51 queued 03:43
created

AndroidSafetyNetAttestationVerifier::useFixedTimestamp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
namespace MadWizard\WebAuthn\Attestation\Verifier;
4
5
use MadWizard\WebAuthn\Attestation\Android\SafetyNetResponseParser;
6
use MadWizard\WebAuthn\Attestation\Android\SafetyNetResponseParserInterface;
7
use MadWizard\WebAuthn\Attestation\AttestationType;
8
use MadWizard\WebAuthn\Attestation\AuthenticatorData;
9
use MadWizard\WebAuthn\Attestation\Statement\AndroidSafetyNetAttestationStatement;
10
use MadWizard\WebAuthn\Attestation\Statement\AttestationStatementInterface;
11
use MadWizard\WebAuthn\Attestation\TrustPath\CertificateTrustPath;
12
use MadWizard\WebAuthn\Exception\VerificationException;
13
use MadWizard\WebAuthn\Pki\CertificateParser;
14
use MadWizard\WebAuthn\Pki\CertificateParserInterface;
15
use function base64_encode;
16
use function hash_equals;
17
use function microtime;
18
19
final class AndroidSafetyNetAttestationVerifier implements AttestationVerifierInterface
20
{
21
    private const ATTEST_HOSTNAME = 'attest.android.com';
22
23
    /**
24
     * @var CertificateParserInterface
25
     */
26
    private $certificateParser;
27
28
    /**
29
     * @var SafetyNetResponseParserInterface
30
     */
31
    private $responseParser;
32
33
    /**
34
     * @var float|null
35
     */
36
    private $fixedTimestamp = null;
37
38 2
    public function __construct(?CertificateParserInterface $certificateParser = null, ?SafetyNetResponseParserInterface $responseParser = null)
39
    {
40 2
        $this->certificateParser = $certificateParser ?? new CertificateParser();
41 2
        $this->responseParser = $responseParser ?? new SafetyNetResponseParser();
42 2
    }
43
44 1
    private function getMsTimestamp(): float
45
    {
46 1
        if ($this->fixedTimestamp !== null) {
47 1
            return $this->fixedTimestamp;
48
        }
49
        return microtime(true) * 1000;
50
    }
51
52
    /**
53
     * @internal Used for testing only
54
     */
55 2
    public function useFixedTimestamp(float $time): void
56
    {
57 2
        $this->fixedTimestamp = $time;
58 2
    }
59
60 2
    public function verify(AttestationStatementInterface $attStmt, AuthenticatorData $authenticatorData, string $clientDataHash): VerificationResult
61
    {
62 2
        if (!($attStmt instanceof AndroidSafetyNetAttestationStatement)) {
63
            throw new VerificationException('Expecting AndroidSafetyNetAttestationStatement');
64
        }
65
66
        // Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract the contained fields.
67
        // -> this is done in AndroidSafetyNetAttestationStatement
68
69
        // Verify that response is a valid SafetyNet response of version ver.
70 2
        $response = $this->responseParser->parse($attStmt->getResponse());
71
72
        // Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.
73 2
        $expectedNonce = base64_encode(hash('sha256', $authenticatorData->getRaw()->getBinaryString() . $clientDataHash, true));
74
75 2
        if (!hash_equals($expectedNonce, $response->getNonce())) {
76
            throw new VerificationException('Nonce is invalid.');
77
        }
78
79
        // Let attestationCert be the attestation certificate.
80 2
        $x5c = $response->getCertificateChain();
81 2
        if (!isset($x5c[0])) {
82
            throw new VerificationException('No certificates in chain.');
83
        }
84 2
        $attCert = $x5c[0];
85
86
        // Verify that attestationCert is issued to the hostname "attest.android.com" (see SafetyNet online documentation).
87 2
        $certInfo = $this->certificateParser->parsePem($attCert->asPem());
88 2
        $cn = $certInfo->getSubjectCommonName();
89
90 2
        if ($cn !== self::ATTEST_HOSTNAME) {
91
            throw new VerificationException(sprintf('Attestation certificate should be issued to %s.', self::ATTEST_HOSTNAME));
92
        }
93
94
        // Verify that the ctsProfileMatch attribute in the payload of response is true.
95 2
        if (!$response->isCtsProfileMatch()) {
96 1
            throw new VerificationException('Attestation should have ctsProfileMatch set to true.');
97
        }
98
99 1
        $diff = $this->getMsTimestamp() - $response->getTimestampMs();
100
101 1
        if ($diff < -60e3 || $diff > 60e3) {
102
            throw new VerificationException('Timestamp is not within margin of one minute');
103
        }
104
105
        // If successful, return implementation-specific values representing attestation type Basic and attestation trust path attestationCert.
106 1
        return new VerificationResult(AttestationType::BASIC, new CertificateTrustPath($x5c));
107
    }
108
}
109