blob: 32bb7663055d21462ddfa999adcbd0c81cbfb65e [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3
4namespace WebAuthn\Attestation\Format;
5use WebAuthn\WebAuthnException;
6use WebAuthn\Binary\ByteBuffer;
7
8class Tpm extends FormatBase {
9 private $_TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
10 private $_TPM_ST_ATTEST_CERTIFY = "\x80\x17";
11 private $_alg;
12 private $_signature;
13 private $_pubArea;
14 private $_x5c;
15
16 /**
17 * @var ByteBuffer
18 */
19 private $_certInfo;
20
21
22 public function __construct($AttestionObject, \WebAuthn\Attestation\AuthenticatorData $authenticatorData) {
23 parent::__construct($AttestionObject, $authenticatorData);
24
25 // check packed data
26 $attStmt = $this->_attestationObject['attStmt'];
27
28 if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') {
29 throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA);
30 }
31
32 if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
33 throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
34 }
35
36 if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
37 throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA);
38 }
39
40 if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) {
41 throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA);
42 }
43
44 if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) {
45 throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA);
46 }
47
48 $this->_alg = $attStmt['alg'];
49 $this->_signature = $attStmt['sig']->getBinaryString();
50 $this->_certInfo = $attStmt['certInfo'];
51 $this->_pubArea = $attStmt['pubArea'];
52
53 // certificate for validation
54 if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
55
56 // The attestation certificate attestnCert MUST be the first element in the array
57 $attestnCert = array_shift($attStmt['x5c']);
58
59 if (!($attestnCert instanceof ByteBuffer)) {
60 throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
61 }
62
63 $this->_x5c = $attestnCert->getBinaryString();
64
65 // certificate chain
66 foreach ($attStmt['x5c'] as $chain) {
67 if ($chain instanceof ByteBuffer) {
68 $this->_x5c_chain[] = $chain->getBinaryString();
69 }
70 }
71
72 } else {
73 throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA);
74 }
75 }
76
77
78 /*
79 * returns the key certificate in PEM format
80 * @return string|null
81 */
82 public function getCertificatePem() {
83 if (!$this->_x5c) {
84 return null;
85 }
86 return $this->_createCertificatePem($this->_x5c);
87 }
88
89 /**
90 * @param string $clientDataHash
91 */
92 public function validateAttestation($clientDataHash) {
93 return $this->_validateOverX5c($clientDataHash);
94 }
95
96 /**
97 * validates the certificate against root certificates
98 * @param array $rootCas
99 * @return boolean
100 * @throws WebAuthnException
101 */
102 public function validateRootCertificate($rootCas) {
103 if (!$this->_x5c) {
104 return false;
105 }
106
107 $chainC = $this->_createX5cChainFile();
108 if ($chainC) {
109 $rootCas[] = $chainC;
110 }
111
112 $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
113 if ($v === -1) {
114 throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
115 }
116 return $v;
117 }
118
119 /**
120 * validate if x5c is present
121 * @param string $clientDataHash
122 * @return bool
123 * @throws WebAuthnException
124 */
125 protected function _validateOverX5c($clientDataHash) {
126 $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
127
128 if ($publicKey === false) {
129 throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
130 }
131
132 // Concatenate authenticatorData and clientDataHash to form attToBeSigned.
133 $attToBeSigned = $this->_authenticatorData->getBinary();
134 $attToBeSigned .= $clientDataHash;
135
136 // Validate that certInfo is valid:
137
138 // Verify that magic is set to TPM_GENERATED_VALUE.
139 if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) {
140 throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA);
141 }
142
143 // Verify that type is set to TPM_ST_ATTEST_CERTIFY.
144 if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) {
145 throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA);
146 }
147
148 $offset = 6;
149 $qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
150 $extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
151 $coseAlg = $this->_getCoseAlgorithm($this->_alg);
152
153 // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
154 if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) {
155 throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA);
156 }
157
158 // Verify the sig is a valid signature over certInfo using the attestation
159 // public key in aikCert with the algorithm specified in alg.
160 return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1;
161 }
162
163
164 /**
165 * returns next part of ByteBuffer
166 * @param ByteBuffer $buffer
167 * @param int $offset
168 * @return ByteBuffer
169 */
170 protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) {
171 $len = $buffer->getUint16Val($offset);
172 $data = $buffer->getBytes($offset + 2, $len);
173 $offset += (2 + $len);
174
175 return new ByteBuffer($data);
176 }
177
178}
179