Skip to content

Commit

Permalink
Impletest remote signer for GOST 2012 algos (inspired by @SergeySidorov)
Browse files Browse the repository at this point in the history
* Inject remote signer into token
* Update usage info
* Pass algo into OpenSslCliJwtSigner: RS256, GOST3410_2012_256
* Add gost engine dinamically to cli params
* Test possible combintaions of signer and remote signer with different algos
  • Loading branch information
garex committed Sep 29, 2020
1 parent 0716149 commit 8bc0c7c
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 35 deletions.
19 changes: 18 additions & 1 deletion .travis.yml
Expand Up @@ -8,14 +8,31 @@ matrix:
include:
- php: "5.6"
env: ESIA_CLIENT_AUTH_METHOD=get
ESIA_CLIENT_ID=EKAP01
ESIA_SIGNER_CLASS=Ekapusta\\OAuth2Esia\\Security\\Signer\\OpensslCli
ESIA_CERTIFICATE=ekapusta.gost.test.cer
ESIA_PRIVATE_KEY=ekapusta.gost.test.key
ESIA_REMOTE_PUBLIC_KEY=esia.test.public.key
ESIA_REMOTE_ALGORYTHM_ID=RS256
ESIA_REMOTE_SIGNER_CLASS=Lcobucci\\JWT\\Signer\\Rsa\\Sha256
- php: "7.1"
env: ESIA_CLIENT_AUTH_METHOD=post
ESIA_CLIENT_ID=EKAP01
ESIA_SIGNER_CLASS=Ekapusta\\OAuth2Esia\\Security\\Signer\\OpensslPkcs7
ESIA_CERTIFICATE=ekapusta.rsa.test.cer
ESIA_PRIVATE_KEY=ekapusta.rsa.test.key
ESIA_REMOTE_PUBLIC_KEY=esia.test.public.key
ESIA_REMOTE_ALGORYTHM_ID=RS256
ESIA_REMOTE_SIGNER_CLASS=Ekapusta\\OAuth2Esia\\Security\\JWTSigner\\OpenSslCliJwtSigner
- php: "7.3"
env: ESIA_CLIENT_AUTH_METHOD=get
ESIA_CLIENT_ID=500201
ESIA_SIGNER_CLASS=Ekapusta\\OAuth2Esia\\Security\\Signer\\OpensslCli
ESIA_CERTIFICATE=ekapusta.gost2012.test.cer
ESIA_PRIVATE_KEY=ekapusta.gost2012.test.key
ESIA_REMOTE_PUBLIC_KEY=esia.gost.test.public.key
ESIA_REMOTE_ALGORYTHM_ID=GOST3410_2012_256
ESIA_REMOTE_SIGNER_CLASS=Ekapusta\\OAuth2Esia\\Security\\JWTSigner\\OpenSslCliJwtSigner

services:
- docker
Expand All @@ -32,7 +49,7 @@ install:
- (cd tests/AuthenticationBot && npm install --no-progress)

before_script:
- export ESIA_CLIENT_OPENSSL_TOOL_PATH="docker run --rm -i -v $(pwd):$(pwd) -w $(pwd) rnix/openssl-gost openssl"
- export ESIA_CLIENT_OPENSSL_TOOL_PATH="docker run --rm -i -v $(pwd):$(pwd) -v /tmp:/tmp -w $(pwd) rnix/openssl-gost openssl"
- export ESIA_LOGIN_ATTEMPTS=3

script:
Expand Down
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -29,6 +29,7 @@ Usage is the same as the normal client, using `Ekapusta\OAuth2Esia\Provider\Esia

```php
use Ekapusta\OAuth2Esia\Provider\EsiaProvider;
use Ekapusta\OAuth2Esia\Security\JWTSigner\OpenSslCliJwtSigner;
use Ekapusta\OAuth2Esia\Security\Signer\OpensslPkcs7;

$provider = new EsiaProvider([
Expand All @@ -38,8 +39,12 @@ $provider = new EsiaProvider([
// For work with test portal version
// 'remoteUrl' => 'https://esia-portal1.test.gosuslugi.ru',
// 'remotePublicKey' => EsiaProvider::RESOURCES.'esia.test.public.key',
// For work with GOST3410_2012_256 signatures (instead of default RS256)
// 'remoteCertificatePath' => EsiaProvider::RESOURCES.'esia.gost.prod.public.key',
], [
'signer' => new OpensslPkcs7('/path/to/public/certificate.cer', '/path/to/private.key')
'signer' => new OpensslPkcs7('/path/to/public/certificate.cer', '/path/to/private.key'),
// For work with GOST3410_2012_256 signatures (instead of default RS256)
// 'remoteSigner' => new OpenSslCliJwtSigner('/path/to/openssl'),
]);
```

Expand All @@ -51,6 +56,12 @@ $provider = new EsiaProvider([
* If you use GOST keys and you are docker-addict, then you can use `'toolpath' => 'docker run --rm -i -v $(pwd):$(pwd) -w $(pwd) rnix/openssl-gost openssl'`.


## Which remote signer to use?

* If your system electronic signature algorythm is default RS256, then do nothing.
Under the hood it uses Sha256 remote signer.
* If you use GOST3410_2012_256 signature, then use `OpenSslCliJwtSigner`, passing to it path to `openssl` tool. For dockers pass to it something like `docker run --rm -i -v $(pwd):$(pwd) -v /tmp/tmp -w $(pwd) rnix/openssl-gost openssl'`. `/tmp ` volume is important there!

### Auth flow

Auth flow is standard.
Expand Down
14 changes: 13 additions & 1 deletion src/Provider/EsiaProvider.php
Expand Up @@ -8,6 +8,8 @@
use Ekapusta\OAuth2Esia\Token\EsiaAccessToken;
use InvalidArgumentException;
use Lcobucci\JWT\Parsing\Encoder;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use League\OAuth2\Client\Grant\AbstractGrant;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
Expand Down Expand Up @@ -39,6 +41,11 @@ class EsiaProvider extends AbstractProvider implements ProviderInterface
*/
private $encoder;

/**
* @var Signer
*/
private $remoteSigner;

public function __construct(array $options = [], array $collaborators = [])
{
// Backward compatibility as of rename remoteCertificatePath -> remotePublicKey
Expand All @@ -60,6 +67,11 @@ public function __construct(array $options = [], array $collaborators = [])
} else {
throw new InvalidArgumentException('Signer is not provided!');
}

$this->remoteSigner = new Sha256();
if (isset($collaborators['remoteSigner']) && $collaborators['remoteSigner'] instanceof Signer) {
$this->remoteSigner = $collaborators['remoteSigner'];
}
}

public function getBaseAuthorizationUrl()
Expand Down Expand Up @@ -194,7 +206,7 @@ protected function checkResponse(ResponseInterface $response, $data)

protected function createAccessToken(array $response, AbstractGrant $grant)
{
return new EsiaAccessToken($response, $this->remotePublicKey);
return new EsiaAccessToken($response, $this->remotePublicKey, $this->remoteSigner);
}

protected function createResourceOwner(array $response, AccessToken $token)
Expand Down
57 changes: 57 additions & 0 deletions src/Security/JWTSigner/OpenSslCliJwtSigner.php
@@ -0,0 +1,57 @@
<?php

namespace Ekapusta\OAuth2Esia\Security\JWTSigner;

use Ekapusta\OAuth2Esia\Transport\Process;
use Lcobucci\JWT\Signer\BaseSigner;
use Lcobucci\JWT\Signer\Key;

final class OpenSslCliJwtSigner extends BaseSigner
{
private $toolPath;
private $algorythmId;
private $postParams = '';

public function __construct($toolPath = 'openssl', $algorythmId = 'GOST3410_2012_256')
{
$this->toolPath = $toolPath;
$this->algorythmId = $algorythmId;

if (false !== stristr($this->getAlgorithmId(), 'gost')) {
$this->postParams = '-engine gost';
}
}

public function getAlgorithmId()
{
return $this->algorythmId;
}

public function doVerify($expected, $payload, Key $key)
{
$verify = new TmpFile($key->getContent());
$signature = new TmpFile($expected);

Process::fromArray([
$this->toolPath,
'dgst',
'-verify '.escapeshellarg($verify),
'-signature '.escapeshellarg($signature),
$this->postParams,
], $payload);

return true;
}

public function createHash($payload, Key $key)
{
$sign = new TmpFile($key->getContent());

return (string) Process::fromArray([
$this->toolPath,
'dgst',
'-sign '.escapeshellarg($sign),
$this->postParams,
], $payload);
}
}
22 changes: 22 additions & 0 deletions src/Security/JWTSigner/TmpFile.php
@@ -0,0 +1,22 @@
<?php

namespace Ekapusta\OAuth2Esia\Security\JWTSigner;

final class TmpFile
{
private $path;

public function __construct($content)
{
$this->handle = tmpfile();
fwrite($this->handle, $content);
fseek($this->handle, 0);

$this->path = stream_get_meta_data($this->handle)['uri'];
}

public function __toString()
{
return $this->path;
}
}
10 changes: 3 additions & 7 deletions src/Token/EsiaAccessToken.php
Expand Up @@ -5,16 +5,16 @@
use Ekapusta\OAuth2Esia\Interfaces\Token\ScopedTokenInterface;
use InvalidArgumentException;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\ValidationData;
use League\OAuth2\Client\Token\AccessToken;

class EsiaAccessToken extends AccessToken implements ScopedTokenInterface
{
private $parsedToken;

public function __construct(array $options = [], $publicKeyPath = null)
public function __construct(array $options, $publicKeyPath, Signer $signer)
{
parent::__construct($options);

Expand All @@ -25,11 +25,7 @@ public function __construct(array $options = [], $publicKeyPath = null)
throw new InvalidArgumentException('Access token is invalid: '.var_export($options, true));
}

if (null == $publicKeyPath) {
return;
}

if (!$this->parsedToken->verify(new Sha256(), new Key(file_get_contents($publicKeyPath)))) {
if (!$this->parsedToken->verify($signer, new Key(file_get_contents($publicKeyPath)))) {
throw new InvalidArgumentException('Access token can not be verified: '.var_export($options, true));
}
}
Expand Down
28 changes: 25 additions & 3 deletions tests/Factory.php
Expand Up @@ -5,8 +5,10 @@
use Bramus\Monolog\Formatter\ColoredLineFormatter;
use Bramus\Monolog\Formatter\ColorSchemes\TrafficLight;
use Ekapusta\OAuth2Esia\Provider\EsiaProvider;
use Ekapusta\OAuth2Esia\Security\JWTSigner\OpenSslCliJwtSigner;
use Ekapusta\OAuth2Esia\Token\EsiaAccessToken;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Monolog\Handler\NullHandler;
Expand Down Expand Up @@ -58,17 +60,37 @@ public static function createAuthenticationBot()
/**
* @return EsiaAccessToken
*/
public static function createAccessToken($privateKeyPath, $publicKeyPath = null)
public static function createAccessToken($privateKeyPath, $publicKeyPath, Signer $signer = null)
{
if (null == $signer) {
$signer = new Sha256();
}

$accessToken = (new Builder())
->setIssuedAt(time())
->setNotBefore(time())
->setExpiration(time() + 3600)
->set('urn:esia:sbj_id', 1)
->set('scope', 'one?oid=123 two?oid=456 three?oid=789')
->sign(new Sha256(), new Key(file_get_contents($privateKeyPath)))
->sign($signer, new Key(file_get_contents($privateKeyPath)))
->getToken();

return new EsiaAccessToken(['access_token' => (string) $accessToken], $publicKeyPath);
return new EsiaAccessToken(['access_token' => (string) $accessToken], $publicKeyPath, $signer);
}

/**
* @return EsiaAccessToken
*/
public static function createGostAccessToken($privateKeyPath, $publicKeyPath)
{
return self::createAccessToken($privateKeyPath, $publicKeyPath, new OpenSslCliJwtSigner(getenv('ESIA_CLIENT_OPENSSL_TOOL_PATH') ?: 'openssl'));
}

/**
* @return EsiaAccessToken
*/
public static function createRsaAccessToken($privateKeyPath, $publicKeyPath)
{
return self::createAccessToken($privateKeyPath, $publicKeyPath, new OpenSslCliJwtSigner(getenv('ESIA_CLIENT_OPENSSL_TOOL_PATH') ?: 'openssl', 'RS256'));
}
}
49 changes: 28 additions & 21 deletions tests/Provider/EsiaProviderTest.php
Expand Up @@ -3,6 +3,7 @@
namespace Ekapusta\OAuth2Esia\Tests\Provider;

use Ekapusta\OAuth2Esia\Provider\EsiaProvider;
use Ekapusta\OAuth2Esia\Security\JWTSigner\OpenSslCliJwtSigner;
use Ekapusta\OAuth2Esia\Security\Signer\OpensslCli;
use Ekapusta\OAuth2Esia\Tests\Factory;
use Ekapusta\OAuth2Esia\Token\EsiaAccessToken;
Expand Down Expand Up @@ -34,9 +35,13 @@ protected function setUp()

$this->redirectUri = 'https://system.dev/esia/auth';

$clientId = getenv('ESIA_CLIENT_ID') ?: '500201';
$signerClass = getenv('ESIA_SIGNER_CLASS') ?: OpensslCli::class;
$certificate = getenv('ESIA_CERTIFICATE') ?: 'ekapusta.gost.test.cer';
$privateKey = getenv('ESIA_PRIVATE_KEY') ?: 'ekapusta.gost.test.key';
$certificate = getenv('ESIA_CERTIFICATE') ?: 'ekapusta.gost2012.test.cer';
$privateKey = getenv('ESIA_PRIVATE_KEY') ?: 'ekapusta.gost2012.test.key';
$remoteSignerClass = getenv('ESIA_REMOTE_SIGNER_CLASS') ?: OpenSslCliJwtSigner::class;
$remotePublicKey = getenv('ESIA_REMOTE_PUBLIC_KEY') ?: 'esia.gost.test.public.key';
$remoteAlgorythmId = getenv('ESIA_REMOTE_ALGORYTHM_ID') ?: 'GOST3410_2012_256';

$this->signer = new $signerClass(
Factory::KEYS.$certificate,
Expand All @@ -46,37 +51,38 @@ protected function setUp()
'-engine gost'
);
$this->provider = new EsiaProvider([
'clientId' => 'EKAP01',
'clientId' => $clientId,
'redirectUri' => $this->redirectUri,
'remoteUrl' => 'https://esia-portal1.test.gosuslugi.ru',
'remotePublicKey' => EsiaProvider::RESOURCES.'esia.test.public.key',
'remotePublicKey' => EsiaProvider::RESOURCES.$remotePublicKey,
'defaultScopes' => [
// needed for authenticating
'openid',

// root entity
'fullname',
'birthdate',
'gender',
// 'birthdate',
// 'gender',
'snils',
'inn',
'birthplace',
// 'inn',
// 'birthplace',

// docs collections
// // docs collections
'id_doc',
'drivers_licence_doc',
// 'drivers_licence_doc',

// vehicles collection
'vehicles',
// // vehicles collection
// 'vehicles',

// contacts collection
'email',
'mobile',
'contacts',
// // contacts collection
// 'email',
// 'mobile',
// 'contacts',
],
], [
'httpClient' => new HttpClient(['handler' => $httpStack]),
'signer' => $this->signer,
'remoteSigner' => new $remoteSignerClass(getenv('ESIA_CLIENT_OPENSSL_TOOL_PATH') ?: 'openssl', $remoteAlgorythmId),
]);
}

Expand Down Expand Up @@ -126,7 +132,7 @@ public function testAccessTokenRequested($authUrl)
'code' => $url['code'],
]);

return $accessToken->getToken();
return $accessToken;
}

/**
Expand Down Expand Up @@ -185,10 +191,8 @@ public function testRemoteCertificateIsRenamedToPublicKey()
/**
* @depends testAccessTokenRequested
*/
public function testPersonGeneralInfoRequested($accessToken)
public function testPersonGeneralInfoRequested(EsiaAccessToken $accessToken)
{
$accessToken = new EsiaAccessToken(['access_token' => $accessToken]);

$resourceOwner = $this->provider->getResourceOwner($accessToken);

$this->assertEquals('1000404446', $resourceOwner->getId());
Expand All @@ -210,7 +214,10 @@ public function testPersonGeneralInfoRequested($accessToken)
*/
public function testPersonGeneralInfoFailsAsOfBadSignedToken()
{
$accessToken = Factory::createAccessToken(Factory::KEYS.'ekapusta.rsa.test.key');
$accessToken = Factory::createAccessToken(
Factory::KEYS.'ekapusta.rsa.test.key',
Factory::KEYS.'ekapusta.rsa.test.cer'
);

$this->provider->getResourceOwner($accessToken);
}
Expand Down

0 comments on commit 8bc0c7c

Please sign in to comment.