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

Add interfaces for data structures #13

Merged
merged 11 commits into from
May 30, 2019
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;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should, in principle, be identical across all formats, and liable to be adjusted or removed (this should always be authData | sha256(clientData), where authData is sha256(rpID) | userPresence | counter)

}
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
98 changes: 53 additions & 45 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,45 @@ 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->validateChallenge($response->getChallengeProvider(), $this->registerRequest);
// Check the Application Parameter
// 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->registerRequest->getApplicationParameter(),
$resp->getClientData()->getChallengeParameter(),
$resp->getKeyHandleBinary(),
$resp->getPublicKeyBinary()
);
$response->getRpIdHash()
)) {
throw new SE(SE::SIGNATURE_INVALID);
}

$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 +223,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 @@ -418,4 +404,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
34 changes: 0 additions & 34 deletions tests/AttestationCertificateTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,40 +52,6 @@ public function testGetAttestationCertificatePem()
);
}

/**
* @covers ::verifyIssuerAgainstTrustedCAs
*/
public function testSuccessfulCAVerification()
{
$class = $this->getObjectWithYubicoCert();
$certs = [dirname(__DIR__).'/CAcerts/yubico.pem'];
$this->assertTrue($class->verifyIssuerAgainstTrustedCAs($certs));
}

/**
* @covers ::verifyIssuerAgainstTrustedCAs
*/
public function testFailedCAVerification()
{
$class = $this->getObjectWithYubicoCert();
$certs = [__DIR__.'/verisign_only_for_unit_tests.pem'];
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::NO_TRUSTED_CA);
$class->verifyIssuerAgainstTrustedCAs($certs);
}

/**
* @covers ::verifyIssuerAgainstTrustedCAs
*/
public function testFailedCAVerificationFromNoCAs()
{
$class = $this->getObjectWithYubicoCert();
$certs = [];
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::NO_TRUSTED_CA);
$class->verifyIssuerAgainstTrustedCAs($certs);
}

// -(Helper methods)-------------------------------------------------------

/**
Expand Down