rysonliu/http-signature

IETF HTTP Signatures 草案 RFC 的实现

v1.1.0 2024-01-17 07:34 UTC

This package is auto-updated.

Last update: 2024-09-17 08:58:57 UTC


README

Build Status Scrutinizer Code Quality Code Coverage Packagist Stable Version Packagist License

这个库提供了一个实现IETF HTTP Signatures 草案 RFC的服务的功能。它包括与 PSR-7 兼容的中间件,用于签名请求(通过像 Guzzle 这样的 HTTP 客户端)和验证 http 签名。

安装

composer require rysonliu/http-signature

用法

创建 HttpSignature 服务时,传递一个支持的算法列表、一个签名请求的回调和一个验证签名的回调。

use Jasny\HttpSignature\HttpSignature;

$keys = [
  'hmac-key-1' => 'secret',
  'hmac-key-2' => 'god',
];

$service = new HttpSignature(
    'hmac-sha256',
    function (string $message, string $keyId) use ($keys): string {
        if (!isset($keys[$keyId])) {
            throw new OutOfBoundsException("Unknown sign key '$keyId'");
        }
    
        $key = $keys[$keyId];
        return hash_hmac('sha256', $message, $key, true);
    },
    function (string $message, string $signature, string $keyId) use ($keys): bool {
        if (!isset($keys[$keyId])) {
            return false;        
        }
    
        $key = $keys[$keyId];
        $expected = hash_hmac('sha256', $message, $key, true);
        
        return hash_equals($expected, $signature);
    }
);

签名请求

您可以使用该服务对 PSR-7 请求进行签名。

$request = new Request(); // Any PSR-7 compatible Request object
$signedRequest = $service->sign($request, $keyId);

验证请求

您可以使用该服务验证已签名的 PSR-7 请求的签名。

$request = new Request(); // Any PSR-7 compatible Request object
$service->verify($request);

如果请求未签名、签名无效或请求不符合要求,将抛出 HttpSignatureException

配置服务

多个算法

而不是指定单个算法,可以在构造函数中指定一个支持算法的数组。使用的算法作为额外的参数传递给签名和验证回调。

use Jasny\HttpSignature\HttpSignature;

$service = new HttpSignature(
    ['hmac-sha256', 'rsa', 'rsa-sha256'],
    function (string $message, string $keyId, string $algorithm): string {
        // ...
    },
    function (string $message, string $signature, string $keyId, string $algorithm): bool {
        // ...
    }
);

签名时指定算法;

$signedRequest = $service->sign($request, $keyId, 'hmac-sha256');

或者您可以选择具有所选算法的服务副本。

$signService = $service->withAlgorithm('hmac-sha256');
$signService->sign($request, $keyId);

必需的头部信息

默认情况下,请求目标(包括 HTTP 方法、URL 路径和查询参数)和 Date 头部对于所有类型的请求都需要作为签名消息的一部分。

$service = $service->withRequiredHeaders('POST', ['(request-target)', 'date', 'content-type', 'digest']);

必需的头部信息可以按请求方法或作为 default 指定。

请注意,要求仅适用于将头部信息包含在创建签名的过程中。如果头部信息未用于请求,则它们也不是签名的一部分。检查请求中是否设置了头部信息以及是否有有效的值,超出了此库的范围。

Date 头部

如果指定了 Date 头部,服务将检查请求的年龄。如果它签名的太早,将抛出异常。默认情况下,请求不能超过 300 秒(5 分钟)。

请求签名和验证之间的时间可能由于延迟或客户端和/或服务器的系统时钟可能不正确。

允许的时间可以配置为时钟偏移;

$service = $service->withClockSkew(1800); // Accept requests up to 30 minutes old

X-Date 头部

浏览器会自动设置 AJAX 请求的 Date 头部。这使得无法将其用于签名。作为解决方案,可以使用替代的 X-Date 头部。

服务器中间件

可以使用服务器中间件来验证 PSR-7 请求。

如果请求已签名但签名无效,中间件将返回 401 Unauthorized 响应,并且处理程序将不会调用。

单次传递中间件(PSR-15)

中间件实现了 PSR-15 MiddlewareInterface。作为 PSR 标准,许多新的库支持此类中间件,例如 Zend Stratigility

您需要提供一个 PSR-17 响应工厂,用于为具有无效签名的请求创建 401 Unauthorized 响应。

use Jasny\HttpSignature\HttpSignature;
use Jasny\HttpSignature\ServerMiddleware;
use Zend\Stratigility\MiddlewarePipe;
use Zend\Diactoros\ResponseFactory;

$service = new HttpSignature(/* ... */);
$responseFactory = new ResponseFactory();
$middleware = new ServerMiddleware($service, $responseFactory);

$app = new MiddlewarePipe();
$app->pipe($middleware);

双重传递中间件

我的 PHP 库支持双重传递中间件。这些是具有以下签名的调用:

fn(ServerRequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface

为了使库如 Jasny RouterRelay 使用回调,请使用 asDoublePass() 方法。

当用作双遍历中间件时,提供资源工厂是可选的。如果没有提供,它将使用调用时传递的响应。

use Jasny\HttpSignature\HttpSignature;
use Jasny\HttpSignature\ServerMiddleware;
use Relay\RelayBuilder;

$service = new HttpSignature(/* ... */);
$middleware = new ServerMiddleware($service);

$relayBuilder = new RelayBuilder($resolver);
$relay = $relayBuilder->newInstance([
    $middleware->asDoublePass(),
]);

$response = $relay($request, $baseResponse);

验证请求

如果请求已签名且签名有效,中间件将设置 signature_key_id 请求属性。

对于未签名的请求,中间件不执行任何操作。这意味着您需要始终检查请求是否具有 signature_key_id

$keyId = $request->getAttribute(`signature_key_id`);

if ($keyId === null) {
    $errorResponse = $response
        ->withStatus(401)
        ->withHeader('Content-Type', 'text/plain');
        
    $errorResponse = $service->setAuthenticateResponseHeader($errorResponse);
    $errorResponse->getBody()->write('request not signed');
}

// Request is signed and signature is valid
// ...

客户端中间件

客户端中间件可用于对 PSR-7 兼容的 HTTP 客户端(如 GuzzleHTTPlug)发送的请求进行签名。

use Jasny\HttpSignature\HttpSignature;
use Jasny\HttpSignature\ClientMiddleware;

$service = new HttpSignature(/* ... */);
$middleware = new ClientMiddleware($service, $keyId);

$keyId 用于 Authorization 标头并传递给签名回调。

如果服务支持多种算法,您需要使用 withAlgorithm 方法选择一个。

$middleware = new ClientMiddleware($service->withAlgorithm('hmac-sha256'));

双重传递中间件

客户端中间件可用于任何支持双遍历中间件的客户端。此类中间件是具有以下签名的调用者;

fn(RequestInterface $request, ResponseInterface $response, callable $next): ResponseInterface

大多数 HTTP 客户端不支持双遍历中间件,而是使用单遍历。但是,像 Relay 这样的更通用目的的 PSR-7 中间件库则支持双遍历。

use Relay\RelayBuilder;

$relayBuilder = new RelayBuilder($resolver);
$relay = $relayBuilder->newInstance([
    $middleware->asDoublePass(),
]);

$response = $relay($request, $baseResponse);

客户端中间件不符合 PSR-15(单遍历),因为它是为服务器请求而设计的。

Guzzle

Guzzle 是最流行的 PHP HTTP 客户端。中间件有一个 forGuzzle() 方法,该方法创建一个回调,可以作为 Guzzle 中间件使用。

当为 Guzzle 使用中间件时,不需要将 $keyId 传递给构造函数。相反,使用 Guzzle 选项 signature_key_id。这也允许使用不同的密钥进行每个请求或禁用请求的签名。

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Client;
use Jasny\HttpSignature\HttpSignature;
use Jasny\HttpSignature\ClientMiddleware;

$service = new HttpSignature(/* ... */);
$middleware = new ClientMiddleware($service);

$stack = new HandlerStack();
$stack->push($middleware->forGuzzle());

$client = new Client(['handler' => $stack, 'signature_key_id' => $keyId]);

$client->get('/foo');                                    // Sign with default key
$client->get('/foo', ['signature_key_id' => $altKeyId]); // Sign with other key
$client->get('/foo', ['signature_key_id' => null]);      // Don't sign

或者,您可以默认禁用签名,仅在指定时进行签名;

$client = new Client(['handler' => $stack]);

$client->get('/foo');                                 // Don't sign
$client->get('/foo', ['signature_key_id' => $keyId]); // Sign

选项仅适用于 Guzzle。对于 HTTPlug 和其他客户端,您需要为每个密钥创建一个客户端或无中间件进行签名。

HTTPlug

HTTPlug 是 PHP-HTTP 的 HTTP 客户端。它允许您编写需要 HTTP 客户端但不绑定到特定实现的可重用库和应用程序。

中间件的 forHttplug() 方法创建一个可以用作 HTTPlug 插件的对象。

use Http\Discovery\HttpClientDiscovery;
use Http\Client\Common\PluginClient;
use Jasny\HttpSignature\HttpSignature;
use Jasny\HttpSignature\ClientMiddleware;

$service = new HttpSignature(/* ... */);
$middleware = new ClientMiddleware($service, $keyId);

$pluginClient = new PluginClient(
    HttpClientDiscovery::find(),
    [
        $middleware->forHttplug(),
    ]
);