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