jasny/http-signature

IETF HTTP Signatures 草案 RFC 的实现

v1.1.3 2019-06-03 17:32 UTC

This package is auto-updated.

Last update: 2024-09-15 00:07:33 UTC


README

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

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

安装

composer require jasny/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 头部。这使得无法使用它进行签名。作为解决方案,可以使用覆盖 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客户端不支持双遍历中间件,而是使用单遍历。然而,更通用的PSR-7中间件库,如Relay,则支持双遍历。

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(),
    ]
);