RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1021751
Accepted
Skywave
Skywave
Asked:2020-09-09 20:22:01 +0000 UTC2020-09-09 20:22:01 +0000 UTC 2020-09-09 20:22:01 +0000 UTC

通过公共服务授权

  • 772

我正在尝试通过国家服务部门进行授权。网站的连接顺序很明确。目前尚不清楚如何使用证书。

我查看了几个 PHP(例如https://github.com/fr05t1k/esia)和其他语言的实现示例。

我们到处都在谈论证书(1 个文件)、私钥(1 个文件)和私钥的密码(字符串)。

我从中得到了什么:

  • 私钥的密码

  • 一些证书在链接http://esia.gosuslugi.ru/public/esia.zip上公开可用 我知道我只需要其中一个,具体取决于签名的类型。

  • 客户发送的文件(他还在国家服务中心注册了该网站)。

来自客户的文件包含一个带扩展名的文件cer(另一个证书?)和一个文件夹,其中包含多个带扩展名的文件key(header.key、mask.key、primary.key 等)

问题:我如何使用这些数据?我需要什么证书?哪个密钥文件是私钥?还是我需要执行一些操作并从中生成私钥?

php
  • 3 3 个回答
  • 10 Views

3 个回答

  • Voted
  1. Skywave
    2020-09-30T14:17:01Z2020-09-30T14:17:01Z

    了解情况。

    因此,客户端发送的密钥(6 个私钥和 1 个证书)只能导入 cryptopro。您只能使用 cryptopro 软件 ( https://www.cryptopro.ru/ ) 来使用它们。使用他们的软件,您只能签署文件、请求 - 不。另外它是免费的。

    结果,我们找到了一个可以使用 GOST 加密的 docker 容器(https://github.com/rnixik/docker-openssl-gost/tree/master/php-fpm-gost)。我们为它制作了一个简单的 rest api,以使用库 ( https://github.com/ekapusta/oauth2-esia )代理对 ESIA 的请求

    要使用此库,您需要转换密钥和证书。

    使用此https://github.com/garex/nodejs-gost-crypto转换了密钥 使用此https://ssl4less.ru/faq/ehniceskie-voprosy/onvertirovanie-sertifikatov-pem-p7b-pfx 转换了证书-der .html

    • 5
  2. Dmitry Kozlov
    2020-09-09T22:08:37Z2020-09-09T22:08:37Z

    我们到处都在谈论证书(1 个文件)、私钥(1 个文件)和私钥的密码(字符串)

    与 ESIA 交换需要加密,因此您必须拥有自己的 RSA 密钥进行签名。私钥随身携带,公共证书上传到 ESIA 柜。

    一些证书可在http://esia.gosuslugi.ru/public/esia.zip公开获得

    由于 ESIA 还会向您发送加密信息,因此您将需要一个公共 ESIA 证书来解密此信息。

    来自客户的文件包含一个扩展名为 cer 的文件

    很可能是公共证书。您可以尝试打开它。Windows 应打开一个标准窗口,其中包含有关证书的信息。

    和一个文件夹,其中包含多个带有密钥扩展名的文件(header.key、masks.key、primary.key 等)

    但这里很难说。看起来客户端使用crypto-pro或类似的东西在闪存驱动器上生成了密钥,然后简单地复制了目录。在这里他错了。需要了解客户对集成的具体要求

    对于通过公共服务进行授权,您可以使用例如自签名证书:

    首先生成私钥和证书请求

    openssl req -newkey rsa:2048 -nodes -keyout domain.key -out domain.csr -sha256
    

    在生成过程中,您将被要求输入私钥的密码。

    然后为它颁发一个公共证书

    openssl x509 -signkey domain.key -in domain.csr -req -days 1825 -out domain.crt -sha256
    

    您将有两个文件 domain.key(私钥)和 domain.crt(证书)。然后需要将证书上传到 ESIA 办公室。

    我如何使用这些数据?

    包https://github.com/ekapusta/oauth2-esia对与 ESIA 的集成有很大帮助 。使用它的描述https://habr.com/ru/post/358834/

    顺便说一下,如果你看一下github上的测试,你会发现那里有一个测试组织的助记词和它的现成密钥。将可以进行测试。

    • 2
  3. Best Answer
    Skywave
    2022-06-15T13:23:34Z2022-06-15T13:23:34Z

    以前的答案现在无关紧要(至少对我们而言)。要求已经改变,我们不得不使用 cryptopro。

    我们使用图像https://hub.docker.com/r/required/cryptopro解决了这个问题。仔细查看如何安装它并导入密钥。您必须自己下载存储库并从本地文件构建,而不仅仅是指定图像名称。

    在图像内部,您需要以任何可能的方式引发一个 apishka,它将一个字符串作为输入,并为输出提供一个签名字符串。这就是这张图片的全部用途。

    签名命令:

    #!/bin/bash
    
    cryptcp -signf -dir "/import" -der -strict -cert -detached -thumbprint "$1" -pin "$2" "/import/message" > /dev/null 2>&1
    signResult=$?
    if [ "$signResult" != "0" ]; then
      exit $signResult
    fi
    cat "/import/message.sgn"
    rm -f "/import/message" "/import/message.sgn"
    

    阿帕奇脚本:

    <?php
    
    $request = $_SERVER['REQUEST_URI'];
    list($path,) = explode('?', $request);
    
    switch ($path) {
        case '/sign':
            $content = file_get_contents('php://input');
            $content = str_replace("\n", '', $content);
            $content = str_replace("\r", '', $content);
            file_put_contents('/import/message', $content);
            $cmd = '/import/scripts/signf-cryptcp ' . $_GET['query'] . ' ' . $_GET['pin'];
            $data = [
                'status' => 'ok',
                'cmd' => $cmd,
                'signedContent' => safeBase64(shell_exec($cmd))
            ];
            echo json_encode($data);
            break;
    }
    
    function safeBase64($string)
    {
        file_put_contents('/import/sig', $string);
        $encoded = $string;
        $encoded = base64_encode($encoded);
        $encoded = str_replace(array('+', '/', '='), array('-', '_', ''), $encoded);
        file_put_contents('/import/sig-encoded', $encoded);
        return $encoded;
    }
    

    Apishka 像这样抽搐:

    $query = 'find_type='.$findType.'&query='.$certQuery.'&pin='.$keyPin;
    $cmd = 'echo "'.$message.'" | curl -sS -X POST --data-binary @- "'.$apiUrl.'/sign?'.$query.'"';
    
    • $findType = 'sha1': 在容器中查找证书的方法
    • $certQuery:从cryptopro(标识符类型)加载到容器中的证书的哈希值。
    • $keyPin:引脚,或密钥,或助记符。我不记得在公共服务或 cryptopro 方面的确切情况。
    • $message:签名字符串
    • $apiUrl:cryptopro 容器中 apish 的 URL

    因此,我们能够签署线路。下一步是什么?

    我们使用https://github.com/ekapusta/oauth2-esia作为 OAuth 客户端

    但最后我不得不重写一些东西

    <?php
    
    /* ПРИШЛОСЬ ПОЛНОСТЬЮ СКОПИПАСТИТЬ КЛАСС Ekapusta\OAuth2Esia\Provider\EsiaProvider, т.к. некоторые вещи private */
    
    namespace app\components\oauth;
    
    use Ekapusta\OAuth2Esia\Interfaces\Provider\ProviderInterface;
    use Ekapusta\OAuth2Esia\Interfaces\Security\SignerInterface;
    use Ekapusta\OAuth2Esia\Interfaces\Token\ScopedTokenInterface;
    use Ekapusta\OAuth2Esia\Token\EsiaAccessToken;
    use InvalidArgumentException;
    use Lcobucci\JWT\Parsing\Encoder;
    use League\OAuth2\Client\Grant\AbstractGrant;
    use League\OAuth2\Client\Provider\AbstractProvider;
    use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
    use League\OAuth2\Client\Provider\GenericResourceOwner;
    use League\OAuth2\Client\Token\AccessToken;
    use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
    use Psr\Http\Message\ResponseInterface;
    use Ramsey\Uuid\Uuid;
    
    class EsiaProvider extends AbstractProvider implements ProviderInterface
    {
        use BearerAuthorizationTrait;
    
        const RESOURCES = __DIR__.'/../../resources/';
    
        protected $defaultScopes = ['openid', 'fullname'];
    
        protected $remoteUrl = 'https://esia.gosuslugi.ru';
    
        protected $remoteCertificatePath = self::RESOURCES.'esia.prod.cer';
    
        /**
         * @var SignerInterface
         */
        private $signer;
    
        /**
         * @var Encoder
         */
        private $encoder;
    
        public function __construct(array $options = [], array $collaborators = [])
        {
            parent::__construct($options, $collaborators);
            if (!filter_var($this->remoteUrl, FILTER_VALIDATE_URL)) {
                throw new InvalidArgumentException('Remote URL is not provided!');
            }
            if (!file_exists($this->remoteCertificatePath)) {
                throw new InvalidArgumentException('Remote certificate is not provided!');
            }
    
            if (isset($collaborators['signer']) && $collaborators['signer'] instanceof SignerInterface) {
                $this->signer = $collaborators['signer'];
                $this->encoder = new Encoder();
            } else {
                throw new InvalidArgumentException('Signer is not provided!');
            }
        }
    
        public function getBaseAuthorizationUrl()
        {
            return $this->getUrl('/aas/oauth2/ac');
        }
    
        protected function getAuthorizationParameters(array $options)
        {
            $options = [
                'access_type' => 'online',
                'approval_prompt' => null,
                'timestamp' => $this->getTimeStamp(),
            ] + parent::getAuthorizationParameters($options);
    
            return $this->withClientSecret($options);
        }
    
        /**
         * @param array $params
         *
         * @return array
         */
        private function withClientSecret(array $params)
        {
            $message = $params['scope'].$params['timestamp'].$params['client_id'].$params['state'];
            $signature = $this->signer->sign($message);
            // $params['client_secret'] = $this->encoder->base64UrlEncode($signature);
        $params['client_secret'] = $signature; // т.к. криптопро возвращает уже кодированную строку
    
            return $params;
        }
    
        protected function getRandomState($length = 32)
        {
            return Uuid::uuid4()->toString();
        }
    
        public function generateState()
        {
            return $this->getRandomState();
        }
    
        private function getTimeStamp()
        {
            return date('Y.m.d H:i:s O');
        }
    
        public function getBaseAccessTokenUrl(array $params)
        {
            return $this->getUrl('/aas/oauth2/te');
        }
    
        public function getResourceOwnerDetailsUrl(AccessToken $token)
        {
            $embeds = $this->getResourceOwnerEmbeds($token);
    
            return $this->getUrl('/rs/prns/'.$token->getResourceOwnerId().'?embed=('.implode(',', $embeds).')');
        }
    
        private function getResourceOwnerEmbeds(ScopedTokenInterface $token)
        {
            $allowedScopes = $token->getScopes();
    
            $embedsToScopes = [
                'contacts.elements' => [
                    'contacts',
                    'email',
                    'mobile',
                ],
                'addresses.elements' => [
                    'contacts',
                ],
                'documents.elements' => [
                    'id_doc',
                    'medical_doc',
                    'military_doc',
                    'foreign_passport_doc',
                    'drivers_licence_doc',
                    'birth_cert_doc',
                    'residence_doc',
                    'temporary_residence_doc',
                ],
                'vehicles.elements' => [
                    'vehicles',
                ],
                'organizations.elements' => [
                    'usr_org',
                ],
            ];
    
            $allowedEmbeds = [];
            foreach ($embedsToScopes as $embed => $scopes) {
                if (count(array_intersect($allowedScopes, $scopes))) {
                    $allowedEmbeds[] = $embed;
                }
            }
    
            return $allowedEmbeds;
        }
    
        private function getUrl($path)
        {
            return $this->remoteUrl.$path;
        }
    
        protected function getDefaultScopes()
        {
            return $this->defaultScopes;
        }
    
        protected function getScopeSeparator()
        {
            return ' ';
        }
    
        protected function getAccessTokenRequest(array $params)
        {
            $params = $params + [
                'scope' => 'openid',
                'state' => $this->getRandomState(),
                'timestamp' => $this->getTimeStamp(),
                'token_type' => 'Bearer',
            ];
    
            return parent::getAccessTokenRequest($this->withClientSecret($params));
        }
    
        protected function checkResponse(ResponseInterface $response, $data)
        {
            if ($response->getStatusCode() >= 400 || isset($data['error'])) {
                throw new IdentityProviderException(
                    isset($data['error']) ? $data['error'] : $response->getReasonPhrase(),
                    $response->getStatusCode(),
                    (string) $response->getBody()
                );
            }
        }
    
        protected function createAccessToken(array $response, AbstractGrant $grant)
        {
            return new EsiaAccessToken($response, $this->remoteCertificatePath);
        }
    
        protected function createResourceOwner(array $response, AccessToken $token)
        {
            $response = ['resourceOwnerId' => $token->getResourceOwnerId()] + $response;
    
            return new GenericResourceOwner($response, 'resourceOwnerId');
        }
    }
    
    
    <?php
    
    namespace app\components\oauth;
    
    use Ekapusta\OAuth2Esia\Security\Signer;
    
    class CryptoProApiSigner extends Signer
    {
        public function __construct($params)
        {
            $this->keyPin = $params['keyPin'] ?? null;
            $this->findType = $params['findType'] ?? 'sha1';
            $this->certQuery = $params['certQuery'] ?? null;
            $this->apiUrl = $params['apiUrl'] ?? null;
        }
    
        public function sign($message)
        {
            $response = $this->request($message);
            $response = json_decode($response, true);
            $sig = $response['signedContent'] ?? null;
            file_put_contents(__DIR__ . '/message', $message);
            file_put_contents(__DIR__ . '/sig.json', print_r($response, true));
            file_put_contents(__DIR__ . '/sig', $sig);
            return $sig;
        }
    
        public function request($message)
        {
            $certQuery = 'find_type=' . $this->findType . '&query=' . $this->certQuery . '&pin=' . $this->keyPin;
            $cmd = 'echo "' . $message . '" | curl -sS -X POST --data-binary @- "' . $this->apiUrl . '/sign?' . $certQuery . '"';
            file_put_contents(__DIR__ . '/cmd', $cmd);
            return shell_exec($cmd);
        }
    }
    

    如何使用

    <?php
    
    $provider = new EsiaProvider([
        'clientId'      => $this->config['client_id'] ?? null,
        'redirectUri'   => $redirectUrl ?? $this->config['redirect_uri'] ?? null,
        'defaultScopes' => $this->config['default_scope'] ?? null,
        'remoteUrl' => $this->config['portal_url'] ?? null,
        'remoteCertificatePath' => $this->config['remote_cert_path'] ?? null,
    ], [
        'signer' => new CryptoProApiSigner(
            [
                'keyPin' => $this->config['pin'],
                'certQuery' => $this->config['cert_sha1'],
                'apiUrl' => $this->config['signer_api_url']
            ]
        )
    ]);
    
    // получение урлика для редиректа на госуслуги
    $redirectUrl = $_POST['redirectUrl'];
    $provider = $this->getProvider($redirectUrl);
    $state = $provider->generateState();
    $params = [
        'state' => $state,
    ];
    try {
        $authUrl = $provider->getAuthorizationUrl($params);
    } catch (\Exception $e) {
        echo $e->getMessage();
    }
    return [
        'authUrl' => $authUrl,
        'state' => $state
    ];
    
    // получение токена и данных
    $redirectUrl = $_POST['redirectUrl'];
    $provider = $this->getProvider($redirectUrl);
    $accessToken = $provider->getAccessToken('authorization_code', ['code' => $_POST['code'] ?? null]);
    if (!$accessToken) {
        return null;
    }
    $esiaPersonData = $provider->getResourceOwner($accessToken);
    if (!$esiaPersonData) {
        return null;
    }
    $userDataArray = $esiaPersonData->toArray();
    return $userDataArray;
    

    PS 代码是从项目中拉出来并更正的,所以它并没有声称它是 100% 功能的,但是脚本是可见的。

    • 1

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    根据浏览器窗口的大小调整背景图案的大小

    • 2 个回答
  • Marko Smith

    理解for循环的执行逻辑

    • 1 个回答
  • Marko Smith

    复制动态数组时出错(C++)

    • 1 个回答
  • Marko Smith

    Or and If,elif,else 构造[重复]

    • 1 个回答
  • Marko Smith

    如何构建支持 x64 的 APK

    • 1 个回答
  • Marko Smith

    如何使按钮的输入宽度?

    • 2 个回答
  • Marko Smith

    如何显示对象变量的名称?

    • 3 个回答
  • Marko Smith

    如何循环一个函数?

    • 1 个回答
  • Marko Smith

    LOWORD 宏有什么作用?

    • 2 个回答
  • Marko Smith

    从字符串的开头删除直到并包括一个字符

    • 2 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5