platine-php /
webauthn
| 1 | <?php |
||
| 2 | |||
| 3 | /** |
||
| 4 | * Platine Webauth |
||
| 5 | * |
||
| 6 | * Platine Webauthn is the implementation of webauthn specifications |
||
| 7 | * |
||
| 8 | * This content is released under the MIT License (MIT) |
||
| 9 | * |
||
| 10 | * Copyright (c) 2020 Platine Webauth |
||
| 11 | * Copyright (c) Jakob Bennemann <[email protected]> |
||
| 12 | * |
||
| 13 | * Permission is hereby granted, free of charge, to any person obtaining a copy |
||
| 14 | * of this software and associated documentation files (the "Software"), to deal |
||
| 15 | * in the Software without restriction, including without limitation the rights |
||
| 16 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
| 17 | * copies of the Software, and to permit persons to whom the Software is |
||
| 18 | * furnished to do so, subject to the following conditions: |
||
| 19 | * |
||
| 20 | * The above copyright notice and this permission notice shall be included in all |
||
| 21 | * copies or substantial portions of the Software. |
||
| 22 | * |
||
| 23 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||
| 24 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
| 25 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||
| 26 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||
| 27 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||
| 28 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||
| 29 | * SOFTWARE. |
||
| 30 | */ |
||
| 31 | |||
| 32 | declare(strict_types=1); |
||
| 33 | |||
| 34 | namespace Platine\Webauthn\Attestation\Format; |
||
| 35 | |||
| 36 | use Platine\Webauthn\Attestation\AuthenticatorData; |
||
| 37 | use Platine\Webauthn\Exception\WebauthnException; |
||
| 38 | use Platine\Webauthn\Helper\ByteBuffer; |
||
| 39 | |||
| 40 | /** |
||
| 41 | * @class Tpm |
||
| 42 | * @package Platine\Webauthn\Attestation\Format |
||
| 43 | */ |
||
| 44 | class Tpm extends BaseFormat |
||
| 45 | { |
||
| 46 | public const TPM_GENERATED_VALUE = "\xFF\x54\x43\x47"; |
||
| 47 | public const TPM_ST_ATTEST_CERTIFY = "\x80\x17"; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * The algorithm used |
||
| 51 | * @var int |
||
| 52 | */ |
||
| 53 | protected int $algo; |
||
| 54 | |||
| 55 | /** |
||
| 56 | * The signature |
||
| 57 | * @var string |
||
| 58 | */ |
||
| 59 | protected string $signature; |
||
| 60 | |||
| 61 | /** |
||
| 62 | * The certificate information |
||
| 63 | * @var ByteBuffer |
||
| 64 | */ |
||
| 65 | protected ByteBuffer $certInfo; |
||
| 66 | |||
| 67 | /** |
||
| 68 | * The public area information |
||
| 69 | * @var ByteBuffer |
||
| 70 | */ |
||
| 71 | protected ByteBuffer $pubArea; |
||
| 72 | |||
| 73 | |||
| 74 | /** |
||
| 75 | * The X5C information |
||
| 76 | * @var string |
||
| 77 | */ |
||
| 78 | protected string $x5c = ''; |
||
| 79 | |||
| 80 | /** |
||
| 81 | * Create new instance |
||
| 82 | * @param array<string|int, mixed> $attestationData |
||
| 83 | * @param AuthenticatorData $authenticatorData |
||
| 84 | */ |
||
| 85 | public function __construct( |
||
| 86 | array $attestationData, |
||
| 87 | AuthenticatorData $authenticatorData |
||
| 88 | ) { |
||
| 89 | parent::__construct($attestationData, $authenticatorData); |
||
| 90 | |||
| 91 | // check packed data |
||
| 92 | $attestationStatement = $this->attestationData['attStmt']; |
||
| 93 | |||
| 94 | if ( |
||
| 95 | ! array_key_exists('ver', $attestationStatement) || |
||
| 96 | $attestationStatement['ver'] !== '2.0' |
||
| 97 | ) { |
||
| 98 | throw new WebauthnException(sprintf( |
||
| 99 | 'Invalid TPM version [%s]', |
||
| 100 | $attestationStatement['ver'] |
||
| 101 | )); |
||
| 102 | } |
||
| 103 | |||
| 104 | if ( |
||
| 105 | ! array_key_exists('alg', $attestationStatement) || |
||
| 106 | $this->getCoseAlgorithm($attestationStatement['alg']) === null |
||
| 107 | ) { |
||
| 108 | throw new WebauthnException(sprintf( |
||
| 109 | 'Unsupported algorithm provided, got [%d]', |
||
| 110 | $attestationStatement['alg'] |
||
| 111 | )); |
||
| 112 | } |
||
| 113 | |||
| 114 | if ( |
||
| 115 | ! array_key_exists('sig', $attestationStatement) || |
||
| 116 | ! $attestationStatement['sig'] instanceof ByteBuffer |
||
| 117 | ) { |
||
| 118 | throw new WebauthnException('No signature found'); |
||
| 119 | } |
||
| 120 | |||
| 121 | if ( |
||
| 122 | ! array_key_exists('certInfo', $attestationStatement) || |
||
| 123 | ! $attestationStatement['certInfo'] instanceof ByteBuffer |
||
| 124 | ) { |
||
| 125 | throw new WebauthnException('No certificate information found'); |
||
| 126 | } |
||
| 127 | |||
| 128 | if ( |
||
| 129 | ! array_key_exists('pubArea', $attestationStatement) || |
||
| 130 | ! $attestationStatement['pubArea'] instanceof ByteBuffer |
||
| 131 | ) { |
||
| 132 | throw new WebauthnException('No public area information found'); |
||
| 133 | } |
||
| 134 | |||
| 135 | $this->algo = $attestationStatement['alg']; |
||
| 136 | $this->signature = $attestationStatement['sig']->getBinaryString(); |
||
| 137 | $this->certInfo = $attestationStatement['certInfo']; |
||
| 138 | $this->pubArea = $attestationStatement['pubArea']; |
||
| 139 | |||
| 140 | if ( |
||
| 141 | array_key_exists('x5c', $attestationStatement) && |
||
| 142 | is_array($attestationStatement['x5c']) && |
||
| 143 | count($attestationStatement['x5c']) > 0 |
||
| 144 | ) { |
||
| 145 | // The attestation certificate attestnCert MUST be the first element in the array |
||
| 146 | $attestCert = array_shift($attestationStatement['x5c']); |
||
| 147 | if (! $attestCert instanceof ByteBuffer) { |
||
| 148 | throw new WebauthnException('Invalid X5C certificate'); |
||
| 149 | } |
||
| 150 | |||
| 151 | $this->x5c = $attestCert->getBinaryString(); |
||
| 152 | |||
| 153 | // Certificate chains |
||
| 154 | foreach ($attestationStatement['x5c'] as $chain) { |
||
| 155 | if ($chain instanceof ByteBuffer) { |
||
| 156 | $this->x5cChain[] = $chain->getBinaryString(); |
||
| 157 | } |
||
| 158 | } |
||
| 159 | } else { |
||
| 160 | throw new WebauthnException('Invalid X5C certificate'); |
||
| 161 | } |
||
| 162 | } |
||
| 163 | |||
| 164 | /** |
||
| 165 | * {@inheritdoc} |
||
| 166 | */ |
||
| 167 | public function getCertificatePem(): ?string |
||
| 168 | { |
||
| 169 | if (empty($this->x5c)) { |
||
| 170 | return null; |
||
| 171 | } |
||
| 172 | |||
| 173 | return $this->createCertificatePem($this->x5c); |
||
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * {@inheritdoc} |
||
| 178 | */ |
||
| 179 | public function validateAttestation(string $clientData): bool |
||
| 180 | { |
||
| 181 | return $this->validateOverX5C($clientData); |
||
| 182 | } |
||
| 183 | |||
| 184 | /** |
||
| 185 | * {@inheritdoc} |
||
| 186 | */ |
||
| 187 | public function validateRootCertificate(array $rootCertificates): bool |
||
| 188 | { |
||
| 189 | if (empty($this->x5c)) { |
||
| 190 | return false; |
||
| 191 | } |
||
| 192 | |||
| 193 | $chain = $this->createX5cChainFile(); |
||
| 194 | if ($chain !== null) { |
||
| 195 | $rootCertificates[] = $chain; |
||
| 196 | } |
||
| 197 | |||
| 198 | $value = openssl_x509_checkpurpose( |
||
| 199 | // TODO phpstan complains so cast to string |
||
| 200 | (string) $this->getCertificatePem(), |
||
| 201 | -1, |
||
| 202 | $rootCertificates |
||
| 203 | ); |
||
| 204 | |||
| 205 | if ($value === -1) { |
||
| 206 | throw new WebauthnException(sprintf( |
||
| 207 | 'Error when validate root certificate, error message: [%s]', |
||
| 208 | openssl_error_string() |
||
| 209 | )); |
||
| 210 | } |
||
| 211 | |||
| 212 | // TODO phpstan complains so cast to bool |
||
| 213 | return (bool) $value; |
||
| 214 | } |
||
| 215 | |||
| 216 | /** |
||
| 217 | * Validate if x5c is present |
||
| 218 | * @param string $clientData |
||
| 219 | * @return bool |
||
| 220 | */ |
||
| 221 | protected function validateOverX5C(string $clientData): bool |
||
| 222 | { |
||
| 223 | // TODO phpstan complains so cast to string |
||
| 224 | $publicKey = openssl_pkey_get_public((string) $this->getCertificatePem()); |
||
| 225 | |||
| 226 | if ($publicKey === false) { |
||
| 227 | throw new WebauthnException(sprintf( |
||
| 228 | 'Invalid public key used, error: [%s]', |
||
| 229 | openssl_error_string() |
||
| 230 | )); |
||
| 231 | } |
||
| 232 | |||
| 233 | // Verify that sig is a valid signature over the concatenation of authenticatorData |
||
| 234 | // and clientDataHash using the attestation public key in attestnCert |
||
| 235 | // with the algorithm specified in alg. |
||
| 236 | $dataToVerify = $this->authenticatorData->getBinary(); |
||
| 237 | $dataToVerify .= $clientData; |
||
| 238 | |||
| 239 | // Verify that magic is set to TPM_GENERATED_VALUE. |
||
| 240 | if ($this->certInfo->getBytes(0, 4) !== self::TPM_GENERATED_VALUE) { |
||
| 241 | throw new WebauthnException('TPM magic value not the same TPM_GENERATED_VALUE'); |
||
| 242 | } |
||
| 243 | |||
| 244 | // Verify that type is set to TPM_ST_ATTEST_CERTIFY. |
||
| 245 | if ($this->certInfo->getBytes(4, 2) !== self::TPM_ST_ATTEST_CERTIFY) { |
||
| 246 | throw new WebauthnException('TPM type value not the same TPM_ST_ATTEST_CERTIFY'); |
||
| 247 | } |
||
| 248 | |||
| 249 | $offset = 6; |
||
| 250 | /* variable not used */ |
||
| 251 | $qualifiedSigner = $this->getTPMLengthPrefix($this->certInfo, $offset); |
||
|
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
| 252 | $extraData = $this->getTPMLengthPrefix($this->certInfo, $offset); |
||
| 253 | $coseAlgo = $this->getCoseAlgorithm($this->algo); |
||
| 254 | if ($coseAlgo === null) { |
||
| 255 | throw new WebauthnException(sprintf( |
||
| 256 | 'Invalid algorithm [%d]', |
||
| 257 | $this->algo |
||
| 258 | )); |
||
| 259 | } |
||
| 260 | |||
| 261 | if ($extraData->getBinaryString() !== hash($coseAlgo['hash'], $dataToVerify, true)) { |
||
| 262 | throw new WebauthnException('certInfo:extraData hash is invalid'); |
||
| 263 | } |
||
| 264 | |||
| 265 | return openssl_verify( |
||
| 266 | $this->certInfo->getBinaryString(), |
||
| 267 | $this->signature, |
||
| 268 | $publicKey, |
||
| 269 | $coseAlgo['openssl'] |
||
| 270 | ) === 1; |
||
| 271 | } |
||
| 272 | |||
| 273 | /** |
||
| 274 | * Return the TPM Prefix length byte buffer |
||
| 275 | * @param ByteBuffer $buffer |
||
| 276 | * @param int $offset |
||
| 277 | * @return ByteBuffer |
||
| 278 | */ |
||
| 279 | protected function getTPMLengthPrefix(ByteBuffer $buffer, int &$offset): ByteBuffer |
||
| 280 | { |
||
| 281 | $length = $buffer->getUint16Value($offset); |
||
| 282 | $data = $buffer->getBytes($offset + 2, $length); |
||
| 283 | |||
| 284 | $offset += (2 + $length); |
||
| 285 | |||
| 286 | return new ByteBuffer($data); |
||
| 287 | } |
||
| 288 | } |
||
| 289 |