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