Skip to content

Commit 4cd8657

Browse files
committed
Converted all methods to static, commented authenticate
The original class was intended to be used as an object, but it makes more sense to use it as a class with static methods. The class does not need to store any data of its own, and the constructor's only other purpose was to version check openSSL. The checking of openSSL would only ever be one off set up / debugging function and so for that reason I've moved this functionality to its own static method. Next step is to write some example code and check for any bugs I may have introduced.
1 parent bb9b1fa commit 4cd8657

2 files changed

Lines changed: 52 additions & 47 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# U2F-php-server
2-
Server-side handling of U2F registration and authentication for PHP
2+
Server-side handling of FIDO U2F registration and authentication for PHP
33

44

55
## Installation

src/U2F.php

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,69 +15,61 @@ class U2F
1515
/** @internal */
1616
const PUBKEY_LEN = 65;
1717

18-
/** @var string */
19-
private $appId;
20-
21-
/** @var null|string */
22-
private $attestDir;
23-
2418
/** @internal */
25-
private $FIXCERTS = array(
19+
private static $FIXCERTS = [
2620
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
2721
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
2822
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
2923
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
3024
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
3125
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
32-
);
26+
];
3327

3428
/**
35-
* @param string $appId Application id for the running application
36-
* @param string|null $attestDir Directory where trusted attestation roots may be found
3729
* @throws U2FException If OpenSSL older than 1.0.0 is used
3830
*/
39-
public function __construct($appId, $attestDir = null)
31+
public static function checkOpenSSLVersion()
4032
{
4133
if(OPENSSL_VERSION_NUMBER < 0x10000000) {
4234
throw new U2FException(
4335
'OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT,
4436
U2FException::OLD_OPENSSL
4537
);
4638
}
47-
$this->appId = $appId;
48-
$this->attestDir = $attestDir;
39+
return true;
4940
}
5041

5142
/**
5243
* Called to get a registration request to send to a user.
5344
* Returns an array of one registration request and a array of sign requests.
5445
*
46+
* @param string $appId Application id for the running application, Basically the app's URL
5547
* @param array $registrations List of current registrations for this
5648
* user, to prevent the user from registering the same authenticator several
5749
* times.
5850
* @return array An array of two elements, the first containing a
5951
* RegisterRequest the second being an array of SignRequest
6052
* @throws U2FException
6153
*/
62-
public function makeRegistration(array $registrations = array())
54+
public static function makeRegistration($appId, array $registrations = array())
6355
{
64-
$challenge = $this->createChallenge();
65-
$request = new RegistrationRequest($challenge, $this->appId);
66-
$signatures = $this->makeAuthentication($registrations);
67-
return array($request, $signatures);
56+
$request = new RegistrationRequest(static::createChallenge(), $appId);
57+
$signatures = static::makeAuthentication($registrations, $appId);
58+
return [$request, $signatures];
6859
}
6960

7061
/**
7162
* Called to verify and unpack a registration message.
7263
*
7364
* @param RegistrationRequest $request this is a reply to
7465
* @param object $response response from a user
66+
* @param string $attestDir
7567
* @param bool $includeCert set to true if the attestation certificate should be
7668
* included in the returned Registration object
7769
* @return Registration
7870
* @throws U2FException
7971
*/
80-
public function register(RegistrationRequest $request, $response, $includeCert = true)
72+
public static function register(RegistrationRequest $request, $response, $attestDir = null, $includeCert = true)
8173
{
8274
// Parameter Checks
8375
if( !is_object( $request ) ) {
@@ -100,9 +92,9 @@ public function register(RegistrationRequest $request, $response, $includeCert =
10092
}
10193

10294
// Unpack the registration data coming from the client-side token
103-
$rawRegistration = $this->base64u_decode($response->registrationData);
95+
$rawRegistration = static::base64u_decode($response->registrationData);
10496
$registrationData = array_values(unpack('C*', $rawRegistration));
105-
$clientData = $this->base64u_decode($response->clientData);
97+
$clientData = static::base64u_decode($response->clientData);
10698
$clientToken = json_decode($clientData);
10799

108100
// Check Client's challenge matches the original request's challenge
@@ -120,7 +112,7 @@ public function register(RegistrationRequest $request, $response, $includeCert =
120112
$offset += U2F::PUBKEY_LEN;
121113

122114
// Validate and set the public key
123-
if($this->publicKeyToPem($pubKey) === null) {
115+
if(static::publicKeyToPem($pubKey) === null) {
124116
throw new U2FException(
125117
'Decoding of public key failed',
126118
U2FException::PUBKEY_DECODE
@@ -132,7 +124,7 @@ public function register(RegistrationRequest $request, $response, $includeCert =
132124
$keyHandleLength = $registrationData[$offset++];
133125
$keyHandle = substr($rawRegistration, $offset, $keyHandleLength);
134126
$offset += $keyHandleLength;
135-
$registration->setKeyHandle($this->base64u_encode($keyHandle));
127+
$registration->setKeyHandle(static::base64u_encode($keyHandle));
136128

137129
// Build certificate
138130
// Set certificate length
@@ -142,7 +134,7 @@ public function register(RegistrationRequest $request, $response, $includeCert =
142134
$certLength += $registrationData[$offset + 3];
143135

144136
// Write the certificate from the returning registration data
145-
$rawCert = $this->fixSignatureUnusedBits(substr($rawRegistration, $offset, $certLength));
137+
$rawCert = static::fixSignatureUnusedBits(substr($rawRegistration, $offset, $certLength));
146138
$offset += $certLength;
147139
$pemCert = "-----BEGIN CERTIFICATE-----\r\n";
148140
$pemCert .= chunk_split(base64_encode($rawCert), 64);
@@ -152,8 +144,8 @@ public function register(RegistrationRequest $request, $response, $includeCert =
152144
}
153145

154146
// If we've set the attestDir, check the given certificate can be used.
155-
if($this->attestDir) {
156-
if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
147+
if($attestDir) {
148+
if(openssl_x509_checkpurpose($pemCert, -1, static::get_certs($attestDir)) !== true) {
157149
throw new U2FException(
158150
'Attestation certificate can not be validated',
159151
U2FException::ATTESTATION_VERIFICATION
@@ -194,21 +186,22 @@ public function register(RegistrationRequest $request, $response, $includeCert =
194186
* Called to get an authentication request.
195187
*
196188
* @param array $registrations An array of the registrations to create authentication requests for.
189+
* @param string $appId Application id for the running application, Basically the app's URL
197190
* @return array An array of SignRequest
198191
* @throws \InvalidArgumentException
199192
*/
200-
public function makeAuthentication(array $registrations)
193+
public static function makeAuthentication(array $registrations, $appId)
201194
{
202195
$signatures = [];
203196
foreach ($registrations as $reg) {
204197
if( !is_object( $reg ) ) {
205-
throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
198+
throw new \InvalidArgumentException('$registrations of makeAuthentication() method only accepts array of object.');
206199
}
207200

208201
$signatures[] = new SignRequest([
209-
'appId' => $this->appId,
202+
'appId' => $appId,
210203
'keyHandle' => $reg->keyHandle,
211-
'challenge' => $this->createChallenge(),
204+
'challenge' => static::createChallenge(),
212205
]);
213206
}
214207
return $signatures;
@@ -218,7 +211,7 @@ public function makeAuthentication(array $registrations)
218211
* Called to verify an authentication response
219212
*
220213
* @param array $requests An array of outstanding authentication requests
221-
* @param array $registrations An array of current registrations
214+
* @param array <Registration> $registrations An array of current registrations
222215
* @param object $response A response from the authenticator
223216
* @return Registration
224217
* @throws U2FException
@@ -228,8 +221,9 @@ public function makeAuthentication(array $registrations)
228221
* If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
229222
* token cloning or similar and appropriate action should be taken.
230223
*/
231-
public function authenticate(array $requests, array $registrations, $response)
224+
public static function authenticate(array $requests, array $registrations, $response)
232225
{
226+
// Parameter checks
233227
if( !is_object( $response ) ) {
234228
throw new \InvalidArgumentException('$response of authenticate() method only accepts object.');
235229
}
@@ -241,17 +235,21 @@ public function authenticate(array $requests, array $registrations, $response)
241235
);
242236
}
243237

238+
// Set default values to null, so we get fails by default
244239
/** @var object|null $req */
245240
$req = null;
246241

247242
/** @var object|null $reg */
248243
$reg = null;
249244

250-
$clientData = $this->base64u_decode($response->clientData);
245+
// Extract client response data
246+
$clientData = static::base64u_decode($response->clientData);
251247
$decodedClient = json_decode($clientData);
248+
249+
// Check we have a match among the requests and the response
252250
foreach ($requests as $req) {
253251
if( !is_object( $req ) ) {
254-
throw new \InvalidArgumentException('$requests of authenticate() method only accepts array of object.');
252+
throw new \InvalidArgumentException('$requests of authenticate() method only accepts an array of objects.');
255253
}
256254

257255
if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
@@ -266,9 +264,11 @@ public function authenticate(array $requests, array $registrations, $response)
266264
U2FException::NO_MATCHING_REQUEST
267265
);
268266
}
267+
268+
// Check for a match for the response among a list of registrations
269269
foreach ($registrations as $reg) {
270270
if( !is_object( $reg ) ) {
271-
throw new \InvalidArgumentException('$registrations of authenticate() method only accepts array of object.');
271+
throw new \InvalidArgumentException('$registrations of authenticate() method only accepts an array of objects.');
272272
}
273273

274274
if($reg->keyHandle === $response->keyHandle) {
@@ -282,20 +282,24 @@ public function authenticate(array $requests, array $registrations, $response)
282282
U2FException::NO_MATCHING_REGISTRATION
283283
);
284284
}
285-
$pemKey = $this->publicKeyToPem($this->base64u_decode($reg->publicKey));
285+
286+
// On Success, check we have a valid public key
287+
$pemKey = static::publicKeyToPem(static::base64u_decode($reg->publicKey));
286288
if($pemKey === null) {
287289
throw new U2FException(
288290
'Decoding of public key failed',
289291
U2FException::PUBKEY_DECODE
290292
);
291293
}
292294

293-
$signData = $this->base64u_decode($response->signatureData);
295+
// Build signature and data from response
296+
$signData = static::base64u_decode($response->signatureData);
294297
$dataToVerify = hash('sha256', $req->appId, true);
295298
$dataToVerify .= substr($signData, 0, 5);
296299
$dataToVerify .= hash('sha256', $clientData, true);
297300
$signature = substr($signData, 5);
298301

302+
// Verify the response data against the public key
299303
if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
300304
$ctr = unpack("Nctr", substr($signData, 1, 4));
301305
$counter = $ctr['ctr'];
@@ -318,12 +322,13 @@ public function authenticate(array $requests, array $registrations, $response)
318322
}
319323

320324
/**
325+
* @param string $attestDir
321326
* @return array
322327
*/
323-
private function get_certs()
328+
private static function get_certs($attestDir)
324329
{
325330
$files = [];
326-
$dir = $this->attestDir;
331+
$dir = $attestDir;
327332
if($dir && $handle = opendir($dir)) {
328333
while(false !== ($entry = readdir($handle))) {
329334
if(is_file("$dir/$entry")) {
@@ -339,7 +344,7 @@ private function get_certs()
339344
* @param string $data
340345
* @return string
341346
*/
342-
private function base64u_encode($data)
347+
private static function base64u_encode($data)
343348
{
344349
return trim(strtr(base64_encode($data), '+/', '-_'), '=');
345350
}
@@ -348,7 +353,7 @@ private function base64u_encode($data)
348353
* @param string $data
349354
* @return string
350355
*/
351-
private function base64u_decode($data)
356+
private static function base64u_decode($data)
352357
{
353358
return base64_decode(strtr($data, '-_', '+/'));
354359
}
@@ -357,7 +362,7 @@ private function base64u_decode($data)
357362
* @param string $key
358363
* @return null|string
359364
*/
360-
private function publicKeyToPem($key)
365+
private static function publicKeyToPem($key)
361366
{
362367
if(strlen($key) !== U2F::PUBKEY_LEN || $key[0] !== "\x04") {
363368
return null;
@@ -388,7 +393,7 @@ private function publicKeyToPem($key)
388393
* @return string
389394
* @throws U2FException
390395
*/
391-
private function createChallenge()
396+
private static function createChallenge()
392397
{
393398
$challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
394399
if( $crypto_strong !== true ) {
@@ -398,7 +403,7 @@ private function createChallenge()
398403
);
399404
}
400405

401-
$challenge = $this->base64u_encode( $challenge );
406+
$challenge = static::base64u_encode( $challenge );
402407

403408
return $challenge;
404409
}
@@ -409,9 +414,9 @@ private function createChallenge()
409414
* @param string $cert
410415
* @return mixed
411416
*/
412-
private function fixSignatureUnusedBits($cert)
417+
private static function fixSignatureUnusedBits($cert)
413418
{
414-
if(in_array(hash('sha256', $cert), $this->FIXCERTS)) {
419+
if(in_array(hash('sha256', $cert), static::$FIXCERTS)) {
415420
$cert[strlen($cert) - 257] = "\0";
416421
}
417422
return $cert;

0 commit comments

Comments
 (0)