Skip to content

Commit 1fa6958

Browse files
author
Peter
committed
update
1 parent ef76575 commit 1fa6958

File tree

1 file changed

+150
-140
lines changed

1 file changed

+150
-140
lines changed
Lines changed: 150 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,63 @@
11
<?php
22

3-
namespace Onetech\WebAuthn\Attestation;
43

5-
use Onetech\WebAuthn\Attestation\Format\FormatBase;
6-
use Onetech\WebAuthn\WebAuthnException;
7-
use Onetech\WebAuthn\CBOR\CborDecoder;
8-
use Onetech\WebAuthn\Binary\ByteBuffer;
9-
10-
/**
11-
* @author Lukas Buchs
12-
* @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
13-
*/
14-
class AttestationObject
15-
{
16-
private AuthenticatorData $_authenticatorData;
17-
private FormatBase $_attestationFormat;
18-
private string $_attestationFormatName;
19-
20-
/**
21-
* @throws WebAuthnException
22-
*/
23-
public function __construct($binary, $allowedFormats)
24-
{
25-
$enc = CborDecoder::decode($binary);
26-
// validation
27-
if (!\is_array($enc) || !\array_key_exists('fmt', $enc) || !is_string($enc['fmt'])) {
28-
throw new WebAuthnException('invalid attestation format', WebAuthnException::INVALID_DATA);
29-
}
30-
31-
if (!\array_key_exists('attStmt', $enc) || !\is_array($enc['attStmt'])) {
32-
throw new WebAuthnException('invalid attestation format (attStmt not available)', WebAuthnException::INVALID_DATA);
33-
}
34-
35-
if (!\array_key_exists('authData', $enc) || !\is_object($enc['authData']) || !($enc['authData'] instanceof ByteBuffer)) {
36-
throw new WebAuthnException('invalid attestation format (authData not available)', WebAuthnException::INVALID_DATA);
37-
}
38-
39-
$this->_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString());
40-
$this->_attestationFormatName = $enc['fmt'];
41-
42-
// Format ok?
43-
if (!in_array($this->_attestationFormatName, $allowedFormats)) {
44-
throw new WebAuthnException('invalid attestation format: ' . $this->_attestationFormatName, WebAuthnException::INVALID_DATA);
45-
}
4+
namespace Onetech\WebAuthn\Attestation\Format;
465

6+
use Onetech\WebAuthn\WebAuthnException;
7+
use Onetech\WebAuthn\Attestation\AuthenticatorData;
8+
use stdClass;
9+
use function count;
4710

48-
$this->_attestationFormat = match ($this->_attestationFormatName) {
49-
'android-key' => new Format\AndroidKey($enc, $this->_authenticatorData),
50-
'android-safetynet' => new Format\AndroidSafetyNet($enc, $this->_authenticatorData),
51-
'apple' => new Format\Apple($enc, $this->_authenticatorData),
52-
'fido-u2f' => new Format\U2f($enc, $this->_authenticatorData),
53-
'none' => new Format\None($enc, $this->_authenticatorData),
54-
'packed' => new Format\Packed($enc, $this->_authenticatorData),
55-
'tpm' => new Format\Tpm($enc, $this->_authenticatorData),
56-
default => throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA),
57-
};
58-
}
5911

60-
/**
61-
* returns the attestation format name
62-
* @return string
63-
*/
64-
public function getAttestationFormatName(): string
65-
{
66-
return $this->_attestationFormatName;
67-
}
12+
abstract class FormatBase
13+
{
14+
protected ?array $_attestationObject;
15+
protected ?AuthenticatorData $_authenticatorData;
16+
protected array $_x5c_chain;
17+
protected $_x5c_tempFile = null;
6818

6919
/**
70-
* returns the attestation format class
71-
* @return FormatBase
20+
*
21+
* @param array $AttentionObject
22+
* @param AuthenticatorData $authenticatorData
7223
*/
73-
public function getAttestationFormat(): FormatBase
24+
public function __construct(array $AttentionObject, AuthenticatorData $authenticatorData)
7425
{
75-
return $this->_attestationFormat;
26+
$this->_attestationObject = $AttentionObject;
27+
$this->_authenticatorData = $authenticatorData;
7628
}
7729

7830
/**
79-
* returns the attestation public key in PEM format
80-
* @return AuthenticatorData
31+
*
8132
*/
82-
public function getAuthenticatorData(): AuthenticatorData
33+
public function __destruct()
8334
{
84-
return $this->_authenticatorData;
35+
// delete X.509 chain certificate file after use
36+
if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
37+
\unlink($this->_x5c_tempFile);
38+
}
8539
}
8640

8741
/**
88-
* returns the certificate chain as PEM
42+
* returns the certificate chain in PEM format
8943
* @return string|null
9044
*/
9145
public function getCertificateChain(): ?string
9246
{
93-
return $this->_attestationFormat->getCertificateChain();
94-
}
95-
96-
/**
97-
* return the certificate issuer as string
98-
* @return string
99-
*/
100-
public function getCertificateIssuer(): string
101-
{
102-
$pem = $this->getCertificatePem();
103-
$issuer = '';
104-
if ($pem) {
105-
$certInfo = \openssl_x509_parse($pem);
106-
if (\is_array($certInfo) && \array_key_exists('issuer', $certInfo) && \is_array($certInfo['issuer'])) {
107-
108-
$cn = $certInfo['issuer']['CN'] ?? '';
109-
$o = $certInfo['issuer']['O'] ?? '';
110-
$ou = $certInfo['issuer']['OU'] ?? '';
111-
112-
if ($cn) {
113-
$issuer .= $cn;
114-
}
115-
if ($issuer && ($o || $ou)) {
116-
$issuer .= ' (' . trim($o . ' ' . $ou) . ')';
117-
} else {
118-
$issuer .= trim($o . ' ' . $ou);
119-
}
120-
}
47+
if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
48+
return \file_get_contents($this->_x5c_tempFile);
12149
}
122-
123-
return $issuer;
50+
return null;
12451
}
12552

12653
/**
127-
* return the certificate subject as string
128-
* @return string
129-
*/
130-
public function getCertificateSubject(): string
131-
{
132-
$pem = $this->getCertificatePem();
133-
$subject = '';
134-
if ($pem) {
135-
$certInfo = \openssl_x509_parse($pem);
136-
if (\is_array($certInfo) && \array_key_exists('subject', $certInfo) && \is_array($certInfo['subject'])) {
137-
138-
$cn = $certInfo['subject']['CN'] ?? '';
139-
$o = $certInfo['subject']['O'] ?? '';
140-
$ou = $certInfo['subject']['OU'] ?? '';
141-
142-
if ($cn) {
143-
$subject .= $cn;
144-
}
145-
if ($subject && ($o || $ou)) {
146-
$subject .= ' (' . trim($o . ' ' . $ou) . ')';
147-
} else {
148-
$subject .= trim($o . ' ' . $ou);
149-
}
150-
}
151-
}
152-
153-
return $subject;
154-
}
155-
156-
/**
157-
* returns the key certificate in PEM format
158-
* @return null|string
54+
* returns the key X.509 certificate in PEM format
55+
* @return string|null
15956
*/
16057
public function getCertificatePem(): ?string
16158
{
162-
return $this->_attestationFormat->getCertificatePem();
59+
// need to be overwritten
60+
return null;
16361
}
16462

16563
/**
@@ -170,7 +68,8 @@ public function getCertificatePem(): ?string
17068
*/
17169
public function validateAttestation(string $clientDataHash): bool
17270
{
173-
return $this->_attestationFormat->validateAttestation($clientDataHash);
71+
// need to be overwritten
72+
return false;
17473
}
17574

17675
/**
@@ -181,16 +80,127 @@ public function validateAttestation(string $clientDataHash): bool
18180
*/
18281
public function validateRootCertificate(array $rootCas): bool
18382
{
184-
return $this->_attestationFormat->validateRootCertificate($rootCas);
83+
// need to be overwritten
84+
return false;
18585
}
18686

87+
18788
/**
188-
* checks if the RpId-Hash is valid
189-
* @param string $rpIdHash
190-
* @return bool
89+
* create a PEM encoded certificate with X.509 binary data
90+
* @param string $x5c
91+
* @return string
19192
*/
192-
public function validateRpIdHash(string $rpIdHash): bool
93+
protected function _createCertificatePem(string $x5c): string
19394
{
194-
return $rpIdHash === $this->_authenticatorData->getRpIdHash();
95+
$pem = '-----BEGIN CERTIFICATE-----' . "\n";
96+
$pem .= \chunk_split(\base64_encode($x5c), 64, "\n");
97+
$pem .= '-----END CERTIFICATE-----' . "\n";
98+
return $pem;
99+
}
100+
101+
/**
102+
* creates a PEM encoded chain file
103+
* @return string|null
104+
*/
105+
protected function _createX5cChainFile(): ?string
106+
{
107+
$content = '';
108+
if (count($this->_x5c_chain) > 0) {
109+
foreach ($this->_x5c_chain as $x5c) {
110+
$certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c));
111+
112+
// check if certificate is self-signed
113+
if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) {
114+
$selfSigned = false;
115+
116+
$subjectKeyIdentifier = $certInfo['extensions']['subjectKeyIdentifier'] ?? null;
117+
$authorityKeyIdentifier = $certInfo['extensions']['authorityKeyIdentifier'] ?? null;
118+
119+
if ($authorityKeyIdentifier && str_starts_with($authorityKeyIdentifier, 'keyid:')) {
120+
$authorityKeyIdentifier = substr($authorityKeyIdentifier, 6);
121+
}
122+
if ($subjectKeyIdentifier && str_starts_with($subjectKeyIdentifier, 'keyid:')) {
123+
$subjectKeyIdentifier = substr($subjectKeyIdentifier, 6);
124+
}
125+
126+
if (($subjectKeyIdentifier && !$authorityKeyIdentifier) || ($authorityKeyIdentifier && $authorityKeyIdentifier === $subjectKeyIdentifier)) {
127+
$selfSigned = true;
128+
}
129+
130+
if (!$selfSigned) {
131+
$content .= "\n" . $this->_createCertificatePem($x5c) . "\n";
132+
}
133+
}
134+
}
135+
}
136+
137+
if ($content) {
138+
$this->_x5c_tempFile = \tempnam(\sys_get_temp_dir(), 'x5c_');
139+
if (\file_put_contents($this->_x5c_tempFile, $content) !== false) {
140+
return $this->_x5c_tempFile;
141+
}
142+
}
143+
144+
return null;
145+
}
146+
147+
148+
/**
149+
* returns the name and openssl key for provided cose number.
150+
* @param int $coseNumber
151+
* @return stdClass|null
152+
*/
153+
protected function _getCoseAlgorithm(int $coseNumber): ?stdClass
154+
{
155+
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
156+
$coseAlgorithms = array(
157+
array(
158+
'hash' => 'SHA1',
159+
'openssl' => OPENSSL_ALGO_SHA1,
160+
'cose' => array(
161+
-65535 // RS1
162+
)),
163+
164+
array(
165+
'hash' => 'SHA256',
166+
'openssl' => OPENSSL_ALGO_SHA256,
167+
'cose' => array(
168+
-257, // RS256
169+
-37, // PS256
170+
-7, // ES256
171+
5 // HMAC256
172+
)),
173+
174+
array(
175+
'hash' => 'SHA384',
176+
'openssl' => OPENSSL_ALGO_SHA384,
177+
'cose' => array(
178+
-258, // RS384
179+
-38, // PS384
180+
-35, // ES384
181+
6 // HMAC384
182+
)),
183+
184+
array(
185+
'hash' => 'SHA512',
186+
'openssl' => OPENSSL_ALGO_SHA512,
187+
'cose' => array(
188+
-259, // RS512
189+
-39, // PS512
190+
-36, // ES512
191+
7 // HMAC512
192+
))
193+
);
194+
195+
foreach ($coseAlgorithms as $coseAlgorithm) {
196+
if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) {
197+
$return = new stdClass();
198+
$return->hash = $coseAlgorithm['hash'];
199+
$return->openssl = $coseAlgorithm['openssl'];
200+
return $return;
201+
}
202+
}
203+
204+
return null;
195205
}
196206
}

0 commit comments

Comments
 (0)