web-eid/web-eid-authtoken-validation-php

PHP的Web eID身份验证令牌验证库

1.2.1 2024-03-15 12:32 UTC

This package is auto-updated.

Last update: 2024-09-02 13:03:47 UTC


README

European Regional Development Fund

web-eid-authtoken-validation-php 是一个用于在Web应用程序中安全地使用电子身份(eID)智能卡进行身份验证时,发行挑战非ces和验证Web eID身份验证令牌的PHP库。

该存储库的逻辑基于Java库,并从中汲取灵感。我们还借鉴了由 Petr Muzikant 创建的 PHP库,并使用了他的部分代码。

关于Web eID项目的更多信息可以在项目网站上找到。

快速入门

完成以下步骤,将支持使用eID卡进行安全身份验证的功能添加到您的PHP Web应用程序后端。前端说明请参阅此处

运行此快速入门需要使用Composer管理的PHP Web应用程序。

1. 将库添加到您的项目

使用Composer安装以将Web eID身份验证令牌验证库包含到您的项目中

composer require web-eid/web-eid-authtoken-validation-php

日志记录

对于日志记录,您必须创建 LoggerInterface 并在 AuthTokenValidatorBuilder 初始化时使用它。

使用Monolog的示例日志记录器接口

use Monolog\Level;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

...
$log = new Logger("general");
$log->pushHandler(new StreamHandler("/some/path/app.log", Level::Debug));

return (new AuthTokenValidatorBuilder($log))
      ->withSiteOrigin(new Uri("https://example.org"))
      ->withTrustedCertificateAuthorities(...self::trustedIntermediateCACertificates())
      ->build();

有关 LoggerInterface 的更多信息,请参阅 example 目录中的使用示例。

2. 配置挑战非ces存储

验证库需要生成身份验证挑战非ces并将其存储在挑战非ces存储中,以便稍后验证。挑战非ces的概述请参阅Web eID系统架构文档。挑战非ces生成器将在发行挑战的REST端点中使用。

use web_eid\web_eid_authtoken_validation_php\challenge\ChallengeNonceGenerator;
use web_eid\web_eid_authtoken_validation_php\challenge\ChallengeNonceGeneratorBuilder;

...
public function generator(): ChallengeNonceGenerator
{
    return (new ChallengeNonceGeneratorBuilder())
      ->withNonceTtl(300) // challenge nonce TTL in seconds, default is 300 (5 minutes)
      ->build();
}
...

使用PHP会话存储挑战非ces。

3. 添加受信任的证书颁发机构证书

您必须明确指定哪些 中间 证书颁发机构(CA)被信任颁发eID身份验证和OCSP响应者证书。CA证书可以从资源中加载。

首先,将受信任的证书,例如 ESTEID2018.der.cer,复制到 certificates/ 文件夹,然后按以下方式加载证书

use web_eid\web_eid_authtoken_validation_php\certificate\CertificateLoader;

...
public function trustedIntermediateCACertificates(): array
{
    return CertificateLoader::loadCertificatesFromResources(
        __DIR__ . "/../certificates/ESTEID2018.cer"
    );
}
...

4. 配置身份验证令牌验证器

一旦满足先决条件,就可以配置身份验证令牌验证器本身。必需参数是网站来源和受信任的证书颁发机构。身份验证令牌验证器将用于Web应用程序身份验证框架的登录处理组件中。

use GuzzleHttp\Psr7\Uri;
use web_eid\web_eid_authtoken_validation_php\validator\AuthTokenValidator;
use web_eid\web_eid_authtoken_validation_php\validator\AuthTokenValidatorBuilder;

...
public function tokenValidator(): AuthTokenValidator
{
    return (new AuthTokenValidatorBuilder())
      ->withSiteOrigin(new Uri("https://example.org"))
      ->withTrustedCertificateAuthorities(...self::trustedIntermediateCACertificates())
      ->build();
}
...

5. 添加用于发行挑战非ces的REST端点

认证需要使用发行挑战非ces的REST端点。该端点必须支持 GET 请求。

以下示例中,我们使用AltoRouter实现端点

class Router
{
    public function init()
    {

        $router = new AltoRouter();
        $router->setBasePath("");
        
        $router->map("GET", "/", ["controller" => "Pages", "method" => "login"]);
        $router->map("GET", "/nonce", ["controller" => "Auth", "method" => "getNonce"]);
        
        $match = $router->match();

        if (!$match) {
            // Redirect to main
            header('Location: /');
            return;
        }


        $controller = new $match["target"]["controller"];
        $method = $match["target"]["method"];

        call_user_func([$controller, $method], $match["params"], []);

    }
}

class Auth
{
    ...
    public function getNonce()
    {

        try {
            header("Content-Type: application/json; charset=utf-8");
            $generator = $this->generator();
            $challengeNonce = $generator->generateAndStoreNonce();
            $responseArr["nonce" => $challengeNonce->getBase64EncodedNonce()];
            echo json_encode($responseArr);
        } catch (Exception $e) {
            header("HTTP/1.0 500 Internal Server Error");
            echo $e->getMessage();
        }
    }
    ...
}

6. 实现身份验证

身份验证包括调用身份验证令牌验证器的validate()方法。验证过程的内部实现将在以下部分和Web eID系统架构文档中更详细地描述。

use web_eid\web_eid_authtoken_validation_php\authtoken\WebEidAuthToken;
use web_eid\web_eid_authtoken_validation_php\certificate\CertificateData;
use web_eid\web_eid_authtoken_validation_php\challenge\ChallengeNonceStore;
use web_eid\web_eid_authtoken_validation_php\exceptions\ChallengeNonceExpiredException;
...

private function getPrincipalNameFromCertificate(X509 $userCertificate): string
{
    try {
        return CertificateData::getSubjectGivenName($userCertificate) . " " . CertificateData::getSubjectSurname($userCertificate);
    } catch (Exception $e) {
        return CertificateData::getSubjectCN($userCertificate);
    }
}
...

try {

    /* Get and remove nonce from store */
    $challengeNonce = (new ChallengeNonceStore())->getAndRemove();

    try {

        // Build token validator
        $tokenValidator = $this->tokenValidator();

        // Validate token
        $cert = $tokenValidator->validate(new WebEidAuthToken($authToken), $challengeNonce->getBase64EncodedNonce());

        session_regenerate_id();

        $subjectName = $this->getPrincipalNameFromCertificate($cert);
        $result = [
            'sub' => $subjectName
        ];

        echo json_encode($result);

    } catch (Exception $e) {
        // Handle exception
    }

} catch (ChallengeNonceExpiredException $e) {
    // Handle exception
}
...

请参阅位于 example 目录中的完整示例。

目录

简介

PHP 的 Web eID 身份验证令牌验证库包含了 Web eID 身份验证令牌验证过程的完整实现,以确保由 Web eID 浏览器扩展发送的身份验证令牌包含有效、一致且未被第三方修改的数据。它还实现了 Web eID 身份验证协议要求的安全挑战随机数生成。它易于配置和集成到您的身份验证服务中。

身份验证协议、身份验证令牌格式、验证要求和挑战随机数使用在 Web eID 系统架构文档 中有更详细的描述。

身份验证令牌验证

身份验证令牌验证过程分为两个阶段

  • 首先,用户证书验证:验证器解析令牌并从 unverifiedCertificate 字段中提取用户证书。然后,它检查证书的到期时间、用途和政策。接下来,它检查证书是否由受信任的 CA 签署,并使用 OCSP 检查证书状态。
  • 其次,令牌签名验证:验证器通过重新构造签名数据 hash(origin)+hash(challenge) 并使用证书中的公钥来验证 signature 字段中的签名,以验证令牌签名是否使用提供的用户证书创建。如果签名验证成功,则隐式且正确地验证了原始数据和挑战随机数,无需实现任何额外的安全检查。

网站后端必须使用特定于浏览器会话的标识符从其本地存储中查找挑战随机数,以保证接收到的身份验证令牌来自发出相应挑战随机数的同一浏览器。网站后端必须保证挑战随机数的生存期有限,并检查其到期,并且只能在验证过程中使用一次,通过在验证过程中将其从存储中删除。

基本用法

如第 4. 配置身份验证令牌验证器 节所述,必需的身份验证令牌验证器配置参数是网站原始地址和受信任证书颁发机构。

原始地址 必须是提供 Web 应用的 URL。原始 URL 必须采用 "https://" <hostname> [ ":" <port> ] 的形式,如 MDN 中定义,且不包含路径或查询组件。请注意,origin URL 不应以斜杠 / 结尾。

受信任证书颁发机构证书 用于验证身份验证令牌中的用户证书和 OCSP 响应者证书是否由受信任的证书颁发机构签署。必须使用中间 CA 证书而不是根 CA 证书,以便可以删除已吊销的 CA 证书。受信任证书颁发机构证书配置在 3. 添加受信任证书颁发机构证书 节中有更详细的描述。

在验证之前,必须使用特定于浏览器会话的标识符从存储中查找之前发行的 挑战随机数。必须将挑战随机数传递给相应参数中的 validate() 方法。在第 2. 配置挑战随机数存储 节中更详细地描述了设置挑战随机数存储。

认证令牌验证器的配置和构建在第 4. 配置认证令牌验证器 节中更详细地描述。一旦构建了验证器对象,就可以如下用于验证认证令牌

$challengeNonce = (new ChallengeNonceStore())->getAndRemove()->getBase64EncodedNonce();
$token = new WebEidAuthToken($tokenString);

$tokenValidator = (new AuthTokenValidatorBuilder)
  ->withSiteOrigin(new Uri(...))
  ->withTrustedCertificateAuthorities(...)
  ->build();

$userCertificate = $tokenValidator->validate($token, $challengeNonce);

validate() 方法在验证成功时返回验证后的用户证书对象,或者在验证失败时抛出异常(如以下 可能的验证错误 所述)。可以使用 CertificateData 类和 ucwords 函数从用户证书对象中提取用户信息

use web_eid\web_eid_authtoken_validation_php\certificate\CertificateData;
...
    
CertificateData::getSubjectCN($userCertificate); // "JÕEORG\\,JAAK-KRISTJAN\\,38001085718"
CertificateData::getSubjectIdCode($userCertificate); // "PNOEE-38001085718"
CertificateData::getSubjectCountryCode($userCertificate); // "EE"

ucwords(CertificateData::getSubjectGivenName($userCertificate), "-"); // "Jaak-Kristjan"
ucwords(CertificateData::getSubjectSurname(userCertificate)); // "Jõeorg"

扩展配置

AuthTokenValidatorBuilder 中有以下额外的配置选项

  • withoutUserCertificateRevocationCheckWithOcsp() – 关闭使用 OCSP 的用户证书吊销检查。默认情况下启用 OCSP 检查,除非激活了指定的 OCSP 服务,否则将从用户证书的 AIA 扩展中提取 OCSP 响应者访问位置 URL。

  • withDesignatedOcspServiceConfiguration(DesignatedOcspServiceConfiguration serviceConfiguration) – 激活提供的指定 OCSP 响应者服务配置,用于使用 OCSP 进行用户证书吊销检查。指定的服务仅用于检查服务支持的证书发行者的证书状态,对于其他证书,将使用默认的 AIA 扩展服务访问位置。请参阅 testutil/OcspServiceMaker.php 中的配置示例 - getDesignatedOcspServiceConfiguration()

  • withOcspRequestTimeout(int $ocspRequestTimeout) – 设置用户证书吊销检查 OCSP 请求的连接和响应超时。默认为 5 秒。

  • withDisallowedCertificatePolicies(string ...$policies) – 将给定的策略添加到不允许的用户证书策略列表中。为了使用户证书被视为有效,它不得包含此列表中存在的任何策略。默认包含爱沙尼亚移动-ID 策略,因为当期望使用 eID 智能卡时,不应使用移动-ID 证书进行身份验证。

  • withNonceDisabledOcspUrls(URI ...$urls) – 将给定的 URL 添加到禁用 nonce 协议扩展的 OCSP 响应者访问位置 URL 列表中。一些 OCSP 响应者不支持 nonce 扩展。

扩展配置示例

$validator = new AuthTokenValidatorBuilder()
  ->withSiteOrigin("https://example.org")
  ->withTrustedCertificateAuthorities(trustedCertificateAuthorities())
  ->withoutUserCertificateRevocationCheckWithOcsp()
  ->withDisallowedCertificatePolicies(["1.2.3"])
  ->withNonceDisabledOcspUrls(new Uri("http://aia.example.org/cert"))
  ->build();

证书的权威信息访问(AIA)扩展

除非使用指定的 OCSP 响应者服务,否则必须在用户证书中存在包含证书的 OCSP 响应者访问位置的 AIA 扩展。将使用 AIA OCSP URL 来检查证书的吊销状态。

请注意,使用 AIA URL 可能有限制,因为这些 URL 后面的服务提供的安全性和 SLA 保证可能与专门的 OCSP 响应者服务不同。如果您需要 SLA 保证,请使用指定的 OCSP 响应者服务。

可能的验证错误

AuthTokenValidatorvalidate() 方法在验证成功时返回验证后的用户证书对象,或者在验证失败时抛出异常。所有在验证过程中可能发生的异常都源自 AuthTokenException,可用的异常列表可在以下链接中找到:这里。每个异常文件都包含一个文档注释,描述了在哪些条件下抛出异常。

有状态和无状态身份验证

在上面的代码示例中,我们使用了基于PHP会话的身份验证机制,其中在登录成功后设置包含用户会话ID的cookie,并将会话数据存储在服务器端。基于cookie的身份验证必须保护免受跨站请求伪造(CSRF)攻击,并采取额外措施通过仅通过HTTPS提供cookie以及设置HttpOnlySecureSameSite属性来保护cookie。

对有状态身份验证的一个常见替代方案是无状态身份验证,使用JSON Web Tokens(JWT)或安全的cookie会话,其中会话数据驻留在客户端浏览器中,并且是签名或加密的。安全的cookie会话在RFC 6896中有描述,以及以下关于安全cookie-based Spring Security会话的文章中。在使用匿名会话和缓存来存储挑战随机数及其发放时间之前,必须先进行用户身份验证。匿名会话必须用于防止伪造登录攻击,保证认证令牌是从发放对应挑战随机数的同一浏览器接收的。缓存必须用于防止重放攻击,保证每个认证令牌只能使用一次。

挑战随机数生成

认证协议需要支持生成挑战随机数,这些是只能使用一次的大随机数,并在之后的使用中进行存储。验证库使用PHP内置的random_bytes(或openssl_random_pseudo_bytes)函数作为安全的随机源,并使用ChallengeNonceStore接口存储已发放的挑战随机数。

认证协议需要一个REST端点,如第5. 添加一个用于发放挑战随机数的REST端点部分所述,用于发放挑战随机数。

随机数的用法在Web eID系统架构文档中进行了更详细的描述。

基本用法

如第2. 配置随机数生成器部分所述,挑战随机数生成器没有强制配置参数。它使用PHP会话作为默认存储。

挑战随机数存储用于保存随机数及其过期时间。必须能够使用浏览器会话的特定标识符从存储中查找挑战随机数数据结构。存储中的值由令牌验证器使用,如认证令牌验证 > 基本用法部分所述,该部分还包含存储使用和配置的建议。

随机数生成器的配置和构建在第3. 配置随机数生成器部分中进行了更详细的描述。一旦构建了生成器对象,就可以按如下方式使用它来生成随机数

$generator = (new ChallengeNonceGeneratorBuilder())->build();
$challengeNonce = $generator->generateAndStoreNonce();  

generateAndStoreNonce()方法既生成随机数又将其保存到存储中。

扩展配置

ChallengeNonceGeneratorBuilder提供了以下附加配置选项

  • withNonceTtl(int $seconds) – 覆盖默认的挑战随机数生存时间。当生存时间到期时,随机数被视为过期。默认的挑战随机数生存时间是5分钟。
  • withSecureRandom(SecureRandom) - 允许指定自定义的SecureRandom实例。

扩展配置示例

$generator = (new ChallengeNonceGeneratorBuilder())
  ->withNonceTtl(300) // 5 minutes
  ->withSecureRandom(customSecureRandom)  
  ->build();

示例实现

示例实现位于example目录中。请在运行之前,在tokenValidator()函数中更新站点源。

示例实现使用AltoRouter (https://dannyvankooten.github.io/AltoRouter/),并配备mod_rewrite模块在Apache服务器上直接运行。如果您想使用不同的Web服务器,请参考example/public/.htaccesshttps://dannyvankooten.github.io/AltoRouter/usage/rewrite-requests.html

请从example文件夹中获取文件。您可以重命名此文件夹,但在本文档中我们仍然将其称为example文件夹。

example文件夹中创建新的certificates文件夹。

https://www.skidsolutions.eu/en/repository/certs下载ESTEID2018证书(DER格式),并将它们放入certificates文件夹。

执行以下composer命令来安装依赖项

composer install
composer dump-autoload

通过更改example/src/app.conf.php中的数组键origin_url,将令牌验证器使用的源URL(默认设置为https://)更改为您运行示例的URL。您还可以使用由设置名称大写后缀'WEB_EID_SAMPLE_'构成的环境外部变量覆盖设置。这在像docker这样的容器化环境中非常有用。

例如,要覆盖origin_url,设置环境变量

WEB_EID_SAMPLE_ORIGIN_URL

将Apache Web服务器的Document Root指向/example/public文件夹。

依赖项版本策略

从版本1.2.0开始,我们采用了灵活的版本策略来处理phpseclibguzzlehttp,并指定依赖项版本为x.y.*。这种方法允许我们的库集成者快速整合安全补丁和依赖项的次要更新。

为什么包含composer.lock

虽然将composer.lock文件包含在应用程序中是常见的做法,以锁定特定版本的依赖项,但对于库来说则不太常见。然而,我们选择在我们的仓库中包含composer.lock,以清楚地表明我们测试过的确切依赖项版本。

尽管我们的库被设计为可以与指定范围内的任何次要版本的依赖项一起工作,但composer.lock文件确保集成者了解我们认为稳定和安全的特定版本。提供的composer.lock旨在作为参考,而不是严格的要求。

代码格式化

我们使用Prettier进行代码格式化。要安装Prettier,使用以下命令

npm install --global prettier @prettier/plugin-php

运行代码格式化命令

composer fix-php

测试

在根目录中运行phpunit以运行所有单元测试。

./vendor/bin/phpunit tests