sk-id-solutions/mobile-id-php-client

Mobile-ID 客户端 PHP API

v1.2.1 2023-04-06 14:41 UTC

This package is auto-updated.

Last update: 2024-09-11 20:10:14 UTC


README

Build Status Coverage Status License: MIT

本地运行

运行 composer install 获取所有依赖。然后你可以运行测试 php vendor/phpunit/phpunit/phpunit

演示应用程序

有一个你可以本地运行的 演示应用程序

功能

  • 简单接口进行 Mobile-ID 认证
  • 拉取用户的签名证书

由于没有像 DigiDoc4J 这样的 PHP 库,此 PHP 客户端不能用于创建数字签名的容器。

要求

安装

安装 Mobile-ID PHP 客户端的首选方式是通过 Composer

composer require sk-id-solutions/mobile-id-php-client "<VERSION>"

如何使用它

以下是一些使用 Mobile-ID PHP 客户端进行认证的示例

您需要确保您的应用程序中启用了 Composer 自动加载

require_once __DIR__ . '/vendor/autoload.php';

认证示例

有关 'use' 的类列表,请参阅 ReadmeTest.php

    
    // step #1 - validate user input

    try {
        $phoneNumber = MidInputUtil::getValidatedPhoneNumber($this->userData['phoneNumber']);
        $nationalIdentityNumber = MidInputUtil::getValidatedNationalIdentityNumber($this->userData['nationalIdentityNumber']);
    }
    catch (MidInvalidPhoneNumberException $e) {
        echo 'The phone number you entered is invalid';
    }
    catch (MidInvalidNationalIdentityNumberException $e) {
        echo 'The national identity number you entered is invalid';
    }

    // step #2 - create client with long-polling.
    // withSslPinnedPublicKeys() is explained later in this document

    $client = MobileIdClient::newBuilder()
            ->withRelyingPartyUUID($this->config['relyingPartyUUID'])
            ->withRelyingPartyName($this->config['relyingPartyName'])
            ->withHostUrl($this->config['hostUrl'])
            ->withLongPollingTimeoutSeconds(60)
            ->withSslPinnedPublicKeys("sha256//k/w7/9MIvdN6O/rE1ON+HjbGx9PRh/zSnNJ61pldpCs=;sha256//some-future-ssl-host-key")
            ->build();


    // step #3 - generate hash & calculate verification code and display to user

    $authenticationHash = MobileIdAuthenticationHashToSign::generateRandomHashOfDefaultType();
    $verificationCode = $authenticationHash->calculateVerificationCode();

    // step #4 - display $verificationCode (4 digit code) to user

    echo 'Verification code: '.$verificationCode."\n";

    // step #5 - create request to be sent to user's phone

    $request = AuthenticationRequest::newBuilder()
            ->withPhoneNumber($phoneNumber)
            ->withNationalIdentityNumber($nationalIdentityNumber)
            ->withHashToSign($authenticationHash)
            ->withLanguage(ENG::asType())
            ->withDisplayText("Log into self-service?")
            ->withDisplayTextFormat(DisplayTextFormat::GSM7)
            ->build();

    // step #6 - send request to user's phone and catch possible errors

    try {
        $response = $client->getMobileIdConnector()->initAuthentication($request);
    }
    catch (MidNotMidClientException $e) {
        echo "User is not a MID client or user's certificates are revoked.";
    }
    catch (MidUnauthorizedException $e) {
        echo 'Integration error with Mobile-ID. Invalid MID credentials';
    }
    catch (MissingOrInvalidParameterException $e) {
        echo 'Problem with MID integration';
    }
    catch (MidInternalErrorException $e) {
        echo 'MID internal error';
    }

    // step #7 - keep polling for session status until we have a final status from phone

    $finalSessionStatus = $client
            ->getSessionStatusPoller()
            ->fetchFinalSessionStatus($response->getSessionID());

    // step #8 - get authenticationResult

    try {
        $authenticationResult = $client
            ->createMobileIdAuthentication($finalSessionStatus, $authenticationHash);

    }
    catch (MidUserCancellationException $e) {
        echo "User cancelled operation from his/her phone.";
    }
    catch (MidNotMidClientException $e) {
        echo "User is not a MID client or user's certificates are revoked.";
    }
    catch (MidSessionTimeoutException $e) {
        echo "User did not type in PIN code or communication error.";
    }
    catch (MidPhoneNotAvailableException $e) {
        echo "Unable to reach phone/SIM card. User needs to check if phone has coverage.";
    }
    catch (MidDeliveryException $e) {
        echo "Error communicating with the phone/SIM card.";
    }
    catch (MidInvalidUserConfigurationException $e) {
        echo "Mobile-ID configuration on user's SIM card differs from what is configured on service provider's side. User needs to contact his/her mobile operator.";
    }
    catch (MidSessionNotFoundException | MissingOrInvalidParameterException | MidUnauthorizedException | MidSslException $e) {
        throw new RuntimeException("Integrator-side error with MID integration or configuration. Error code:". $e->getCode());
    }
    catch (MidServiceUnavailableException $e) {
        echo "MID service is currently unavailable. User shold try again later.";
    }
    catch (MidInternalErrorException $internalError) {
        echo "Something went wrong with Mobile-ID service";
    }

    # step #9 - validate returned result (to protect yourself from man-in-the-middle attack)
    $validator = AuthenticationResponseValidator::newBuilder()
        ->withTrustedCaCertificatesFolder(__DIR__ . "/test_numbers_ca_certificates/")
        ->build();

    $validator->validate($authenticationResult);


    # step #10 - read out authenticated person details

    $authenticatedPerson = $authenticationResult->constructAuthenticationIdentity();

    echo 'Welcome, '.$authenticatedPerson->getGivenName().' '.$authenticatedPerson->getSurName().' ';
    echo ' (ID code '.$authenticatedPerson->getIdentityCode().') ';
    echo 'from '. $authenticatedPerson->getCountry(). '!';

有关更详细的实际示例,请参阅 mid-rest-php-demo

长轮询配置

询问认证会话状态时,您有两种选择。您可以配置长轮询,这意味着服务器不会立即对会话状态请求做出响应,而是等待用户输入(用户输入了 PIN1 或取消)或超时。然而,这会在调用方一侧阻塞线程,可能是不希望的。为此,还有一个选项是 withPollingSleepTimeoutSeconds(2),这意味着客户端每 2 秒向服务器发送一次请求。

如果您未设置 longPollingTimeoutSeconds 或 pollingSleepTimeoutSeconds 的正值,则 pollingSleepTimeoutSeconds 将默认为 3 秒。

使用长轮询

    $this->client = MobileIdClient::newBuilder()
        ->withHostUrl("https://...")
        ->withRelyingPartyUUID("...")
        ->withRelyingPartyName("...")
        ->withSslPinnedPublicKeys("sha256//...")
        ->withLongPollingTimeoutSeconds(60)
        ->build();

不使用长轮询

    $this->client = MobileIdClient::newBuilder()
        ->withHostUrl("https://...")
        ->withRelyingPartyUUID("...")
        ->withRelyingPartyName("...")
        ->withSslPinnedPublicKeys("sha256//...")
        ->withPollingSleepTimeoutSeconds(2)
        ->build();

检查 MID API 主机是否受信任

当与 MID API 协商 SSL 连接时,MID 服务器发送一个证书以表明其身份。从这个证书中提取一个公钥,并计算公钥的 sha256 哈希。这个哈希必须与提供给此库的哈希之一完全匹配

    $this->client = MobileIdClient::newBuilder()
        ->withHostUrl("https://...")
        ->withRelyingPartyUUID("...")
        ->withRelyingPartyName("...")
        ->withSslPinnedPublicKeys("sha256//hash-of-current-mid-api-ssl-host-public-key;sha256//hash-of-future-mid-api-ssl-host-public-key")
        ->build();
        

否则,在发送或接收任何数据之前,将终止与 MID API 的连接。

库内部使用 https://curl.se/libcurl/c/CURLOPT_PINNEDPUBLICKEY.html 进行此操作。

获取生产 API 端点证书的摘要

打开 https://www.skidsolutions.eu/en/repository/certs/ 下载 mid.sk.ee 证书(PEM 格式)并将其保存为 "mid_sk_ee.PEM.cer"。

openssl x509 -in mid_sk_ee.PEM.cer -pubkey -noout > mid.sk.ee.pubkey.pem
openssl asn1parse -noout -inform pem -in mid.sk.ee.pubkey.pem -out mid.sk.ee.pubkey.der
openssl dgst -sha256 -binary mid.sk.ee.pubkey.der | openssl base64

复制输出(类似于 "fqp7yWK7iGGKj+3unYdm2DA3VCPDkwtyX+DrdZYSC6o=" 并在前面添加 "sha256//"),结果如下: "sha256//fqp7yWK7iGGKj+3unYdm2DA3VCPDkwtyX+DrdZYSC6o="

添加未来的生产证书

大约每年一次,服务器的 SSL 证书会更换。当发生这种情况时,SK 会通过电子邮件通知所有 RP。下载新的证书,并根据上述说明计算其 sha-256 摘要,并将摘要添加到列表中,用分号分隔。因此,值可能如下所示

"sha256//fqp7yWK7iGGKj+3unYdm2DA3VCPDkwtyX+DrdZYSC6o=;sha256//future-prod-certificate 的摘要"

获取演示API端点证书摘要

演示服务器(tsp.demo.sk.ee)证书可在以下位置获取:https://www.skidsolutions.eu/en/Repository/certs/certificates-for-testing,或您可以直接从服务器下载。

openssl s_client -servername tsp.demo.sk.ee -connect tsp.demo.sk.ee:443 < /dev/null | sed -n "/-----BEGIN/,/-----END/p" > tsp.demo.sk.ee.pem
openssl x509 -in tsp.demo.sk.ee.pem -pubkey -noout > tsp.demo.sk.ee.pubkey.pem
openssl asn1parse -noout -inform pem -in tsp.demo.sk.ee.pubkey.pem -out tsp.demo.sk.ee.pubkey.der
openssl dgst -sha256 -binary tsp.demo.sk.ee.pubkey.der | openssl base64

设置公共IP或接口

有时服务器有多个网络接口或IP地址,客户端需要指定用于MID请求的哪个接口。这可以通过使用withNetworkInterface()参数来完成。

    $this->client = MobileIdClient::newBuilder()
        ->withHostUrl("https://...")
        ->withRelyingPartyUUID("...")
        ->withRelyingPartyName("...")
        ->withSslPinnedPublicKeys("sha256//...")
        ->withNetworkInterface("10.11.12.13")
        ->build();

在内部,这将设置CURLOPT_INTERFACE标志

拉取用户的签名证书

此客户端还支持下载用户的移动-ID签名证书。

  $client = MobileIdClient::newBuilder()
           ->withRelyingPartyUUID(TestData::DEMO_RELYING_PARTY_UUID)
           ->withRelyingPartyName(TestData::DEMO_RELYING_PARTY_NAME)
           ->withHostUrl(TestData::DEMO_HOST_URL)
           ->withSslPinnedPublicKeys("sha256//k/w7/9MIvdN6O/rE1ON+HjbGx9PRh/zSnNJ61pldpCs=;sha256//some-future-ssl-host-key")
           ->build();

   $request = CertificateRequest::newBuilder()
       ->withPhoneNumber("+37200000766")
       ->withNationalIdentityNumber("60001019906")
       ->build();

   try {
       $response = $client->getMobileIdConnector()->pullCertificate($request);
       $person = $client->parseMobileIdIdentity($response);

       echo 'This is a Mobile-ID user.';
       echo 'Name, '.$person->getGivenName().' '.$person->getSurName().' ';
       echo ' (ID code '.$person->getIdentityCode().') ';
       echo 'from '. $person->getCountry(). '!';
   }
   catch (MidNotMidClientException $e) {
       // if user is not MID client then this exception is thrown and caught already during first request (see above)
       echo "You are not a Mobile-ID client or your Mobile-ID certificates are revoked. Please contact your mobile operator.";
   }
   catch (MissingOrInvalidParameterException | MidUnauthorizedException $e) {
       throw new RuntimeException("Client side error with mobile-ID integration. Error code:". $e->getCode());
   }
   catch (MidInternalErrorException $internalError) {
       echo "Something went wrong with Mobile-ID service";
   }

签名

PHP库不支持签名。

设置日志记录

查看src/Util/Logger.php 文件。最基本的选择是将以下内容添加到debug_to_console()方法中。

    echo $message."\n";