Skip to content
This repository has been archived by the owner on Mar 25, 2024. It is now read-only.

Commit

Permalink
Add interfaces for data structures (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Firehed committed May 30, 2019
1 parent 37656ad commit e83f4dd
Show file tree
Hide file tree
Showing 18 changed files with 291 additions and 104 deletions.
8 changes: 8 additions & 0 deletions src/AppIdTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ public function getApplicationParameter(): string
{
return hash('sha256', $this->appId, true);
}

/**
* @return string The raw SHA-256 hash of the Relying Party ID
*/
public function getRpIdHash(): string
{
return hash('sha256', $this->appId, true);
}
}
13 changes: 0 additions & 13 deletions src/AttestationCertificateTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,4 @@ public function setAttestationCertificate(string $cert): self
$this->attest = $cert;
return $this;
}

public function verifyIssuerAgainstTrustedCAs(array $trusted_cas): bool
{
$result = openssl_x509_checkpurpose(
$this->getAttestationCertificatePem(),
\X509_PURPOSE_ANY,
$trusted_cas
);
if ($result !== true) {
throw new SecurityException(SecurityException::NO_TRUSTED_CA);
}
return $result;
}
}
1 change: 0 additions & 1 deletion src/ChallengeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@

interface ChallengeProvider
{

public function getChallenge(): string;
}
5 changes: 5 additions & 0 deletions src/ClientData.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public static function fromJson(string $json)
return $ret;
}

public function getApplicationParameter(): string
{
return hash('sha256', $this->origin, true);
}

/**
* Checks the 'typ' field against the allowed types in the U2F spec (sec.
* 7.1)
Expand Down
17 changes: 17 additions & 0 deletions src/LoginResponseInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);

namespace Firehed\U2F;

interface LoginResponseInterface
{
public function getChallengeProvider(): ChallengeProvider;

public function getCounter(): int;

public function getKeyHandleBinary(): string;

public function getSignature(): string;

public function getSignedData(): string;
}
20 changes: 19 additions & 1 deletion src/RegisterResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use Firehed\U2F\InvalidDataException as IDE;

class RegisterResponse
class RegisterResponse implements RegistrationResponseInterface
{
use AttestationCertificateTrait;
use ECPublicKeyTrait;
Expand Down Expand Up @@ -108,4 +108,22 @@ protected function parseResponse(array $response): self

return $this;
}

public function getSignedData(): string
{
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#fig-authentication-request-message
return sprintf(
'%s%s%s%s%s',
chr(0),
$this->getClientData()->getApplicationParameter(),
$this->getClientData()->getChallengeParameter(),
$this->getKeyHandleBinary(),
$this->getPublicKeyBinary()
);
}

public function getRpIdHash(): string
{
return $this->getClientData()->getApplicationParameter();
}
}
23 changes: 23 additions & 0 deletions src/RegistrationResponseInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);

namespace Firehed\U2F;

interface RegistrationResponseInterface
{
public function getAttestationCertificateBinary(): string;

public function getAttestationCertificatePem(): string;

public function getChallengeProvider(): ChallengeProvider;

public function getKeyHandleBinary(): string;

public function getPublicKeyBinary(): string;

public function getRpIdHash(): string;

public function getSignature(): string;

public function getSignedData(): string;
}
5 changes: 5 additions & 0 deletions src/ResponseTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public function getClientData(): ClientData
return $this->clientData;
}

public function getChallengeProvider(): ChallengeProvider
{
return $this->clientData;
}

protected function setSignature(string $signature): self
{
$this->signature = $signature;
Expand Down
2 changes: 2 additions & 0 deletions src/SecurityException.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class SecurityException extends Exception
const CHALLENGE_MISMATCH = 3;
const KEY_HANDLE_UNRECOGNIZED = 4;
const NO_TRUSTED_CA = 5;
const WRONG_RELYING_PARTY = 6;

const MESSAGES = [
self::SIGNATURE_INVALID => 'Signature verification failed',
Expand All @@ -23,6 +24,7 @@ class SecurityException extends Exception
self::CHALLENGE_MISMATCH => 'Response challenge does not match request',
self::KEY_HANDLE_UNRECOGNIZED => 'Key handle has not been registered',
self::NO_TRUSTED_CA => 'The attestation certificate was not signed by any trusted Certificate Authority',
self::WRONG_RELYING_PARTY => 'Relying party invalid for this server',
];

public function __construct(int $code)
Expand Down
101 changes: 55 additions & 46 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class Server
/**
* Holds a list of paths to PEM-formatted CA certificates. Unless
* verification has been explicitly disabled with `disableCAVerification()`,
* the Attestation Certificate in the `RegisterResponse` will be validated
* against the provided CAs.
* the Attestation Certificate in the `RegistrationResponseInterface` will
* be validated against the provided CAs.
*
* This means that you *must* either a) provide a list of trusted
* certificates, or b) explicitly disable verifiation. By default, it will
Expand Down Expand Up @@ -61,19 +61,19 @@ public function __construct()
// @codeCoverageIgnoreEnd
}
/**
* This method authenticates a `SignResponse` against outstanding
* This method authenticates a `LoginResponseInterface` against outstanding
* registrations and their corresponding `SignRequest`s. If the response's
* signature validates and the counter hasn't done anything strange, the
* registration will be returned with an updated counter value, which *must*
* be persisted for the next authentication. If any verification component
* fails, a `SE` will be thrown.
*
* @param SignResponse $response the parsed response from the user
* @param LoginResponseInterface $response the parsed response from the user
* @return RegistrationInterface if authentication succeeds
* @throws SE if authentication fails
* @throws BadMethodCallException if a precondition is not met
*/
public function authenticate(SignResponse $response): RegistrationInterface
public function authenticate(LoginResponseInterface $response): RegistrationInterface
{
if (!$this->registrations) {
throw new BadMethodCallException(
Expand Down Expand Up @@ -117,28 +117,15 @@ public function authenticate(SignResponse $response): RegistrationInterface
// match the one in the signing request, the client signed the
// wrong thing. This could possibly be an attempt at a replay
// attack.
$this->validateChallenge($response->getClientData(), $request);
$this->validateChallenge($response->getChallengeProvider(), $request);

$pem = $registration->getPublicKeyPem();

// U2F Spec:
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success
$to_verify = sprintf(
'%s%s%s%s',
$request->getApplicationParameter(),
chr($response->getUserPresenceByte()),
pack('N', $response->getCounter()),
// Note: Spec says this should be from the request, but that's not
// actually available via the JS API. Because we assert the
// challenge *value* from the Client Data matches the trusted one
// from the SignRequest and that value is included in the Challenge
// Parameter, this is safe unless/until SHA-256 is broken.
$response->getClientData()->getChallengeParameter()
);
$toVerify = $response->getSignedData();

// Signature must validate against
$sig_check = openssl_verify(
$to_verify,
$toVerify,
$response->getSignature(),
$pem,
\OPENSSL_ALGO_SHA256
Expand Down Expand Up @@ -189,46 +176,37 @@ public function authenticate(SignResponse $response): RegistrationInterface
}

/**
* This method authenticates a RegisterResponse against its corresponding
* RegisterRequest by verifying the certificate and signature. If valid, it
* returns a registration; if not, a SE will be thrown and attempt to
* register the key must be aborted.
* This method authenticates a RegistrationResponseInterface against its
* corresponding RegisterRequest by verifying the certificate and signature.
* If valid, it returns a registration; if not, a SE will be thrown and
* attempt to register the key must be aborted.
*
* @param RegisterResponse $resp The response to verify
* @param RegistrationResponseInterface $response The response to verify
* @return RegistrationInterface if the response is proven authentic
* @throws SE if the response cannot be proven authentic
* @throws BadMethodCallException if a precondition is not met
*/
public function register(RegisterResponse $resp): RegistrationInterface
public function register(RegistrationResponseInterface $response): RegistrationInterface
{
if (!$this->registerRequest) {
throw new BadMethodCallException(
'Before calling register(), provide a RegisterRequest '.
'with setRegisterRequest()'
);
}
$this->validateChallenge($resp->getClientData(), $this->registerRequest);
// Check the Application Parameter?
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#registration-response-message-success
$signed_data = sprintf(
'%s%s%s%s%s',
chr(0),
$this->registerRequest->getApplicationParameter(),
$resp->getClientData()->getChallengeParameter(),
$resp->getKeyHandleBinary(),
$resp->getPublicKeyBinary()
);
$this->validateChallenge($response->getChallengeProvider(), $this->registerRequest);
// Check the Application Parameter
$this->validateRelyingParty($response->getRpIdHash());

$pem = $resp->getAttestationCertificatePem();
if ($this->verifyCA) {
$resp->verifyIssuerAgainstTrustedCAs($this->trustedCAs);
$this->verifyAttestationCertAgainstTrustedCAs($response);
}

// Signature must validate against device issuer's public key
$pem = $response->getAttestationCertificatePem();
$sig_check = openssl_verify(
$signed_data,
$resp->getSignature(),
$response->getSignedData(),
$response->getSignature(),
$pem,
\OPENSSL_ALGO_SHA256
);
Expand All @@ -237,10 +215,10 @@ public function register(RegisterResponse $resp): RegistrationInterface
}

return (new Registration())
->setAttestationCertificate($resp->getAttestationCertificateBinary())
->setAttestationCertificate($response->getAttestationCertificateBinary())
->setCounter(0) // The response does not include this
->setKeyHandle($resp->getKeyHandleBinary())
->setPublicKey($resp->getPublicKeyBinary());
->setKeyHandle($response->getKeyHandleBinary())
->setPublicKey($response->getPublicKeyBinary());
}

/**
Expand Down Expand Up @@ -394,6 +372,15 @@ private function generateChallenge(): string
return toBase64Web(\random_bytes(16));
}

private function validateRelyingParty(string $rpIdHash): void
{
// Note: this is a bit delicate at the moment, since different
// protocols have different rules around the handling of Relying Party
// verification. Expect this to be revised.
if (!hash_equals($this->getRpIdHash(), $rpIdHash)) {
throw new SE(SE::WRONG_RELYING_PARTY);
}
}
/**
* Compares the Challenge value from a known source against the
* user-provided value. A mismatch will throw a SE. Future
Expand All @@ -418,4 +405,26 @@ private function validateChallenge(
// TOOD: generate and compare timestamps
return true;
}

/**
* Asserts that the attestation cert provided by the registration is issued
* by the set of trusted CAs.
*
* @param RegistrationResponseInterface $response The response to validate
* @throws SecurityException upon failure
* @return void
*/
private function verifyAttestationCertAgainstTrustedCAs(RegistrationResponseInterface $response): void
{
$pem = $response->getAttestationCertificatePem();

$result = openssl_x509_checkpurpose(
$pem,
\X509_PURPOSE_ANY,
$this->trustedCAs
);
if ($result !== true) {
throw new SE(SE::NO_TRUSTED_CA);
}
}
}
21 changes: 20 additions & 1 deletion src/SignResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use Firehed\U2F\InvalidDataException as IDE;

class SignResponse
class SignResponse implements LoginResponseInterface
{
use ResponseTrait;

Expand All @@ -17,11 +17,30 @@ public function getCounter(): int
{
return $this->counter;
}

public function getUserPresenceByte(): int
{
return $this->user_presence;
}

public function getSignedData(): string
{
// U2F Spec:
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success
return sprintf(
'%s%s%s%s',
$this->getClientData()->getApplicationParameter(),
chr($this->getUserPresenceByte()),
pack('N', $this->getCounter()),
// Note: Spec says this should be from the request, but that's not
// actually available via the JS API. Because we assert the
// challenge *value* from the Client Data matches the trusted one
// from the SignRequest and that value is included in the Challenge
// Parameter, this is safe unless/until SHA-256 is broken.
$this->getClientData()->getChallengeParameter()
);
}

protected function parseResponse(array $response): self
{
$this->validateKeyInArray('keyHandle', $response);
Expand Down
17 changes: 17 additions & 0 deletions tests/AppIdTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,21 @@ public function testGetApplicationParameter()
'getApplicationParamter should return the raw SHA256 hash of the application id'
);
}

/**
* @covers ::getRpIdHash
*/
public function testGetRpIdHash()
{
$obj = new class {
use AppIdTrait;
};
$appId = 'https://u2f.example.com';
$obj->setAppId($appId);
$this->assertSame(
hash('sha256', $appId, true),
$obj->getRpIdHash(),
'getRpIdHash should return the raw SHA256 hash of the application id'
);
}
}
Loading

0 comments on commit e83f4dd

Please sign in to comment.