charlesportwoodii / ncryptf
Requires
- php: ^7.1 || ^7.2 || ^7.3
- ext-sodium: >=2.0.2
- middlewares/utils: ^2.1
- psr/http-server-middleware: ^1.0
- psr/simple-cache: ^1.0
Requires (Dev)
README
这是一个库,用于简化基于散列的KDF签名认证和与兼容API的端到端加密通信。
HMAC+HKDF认证
HMAC+HKDF认证是一种认证方法,确保请求在传输过程中未被篡改。这不仅可以抵御网络层的操作,还可以抵御中间人攻击。
在较高层面,基于原始请求体、HTTP方法、URI(如果存在,则包含查询参数)和当前日期创建HMAC签名。除了确保请求在传输过程中不能被操作外,还确保请求有时间限制,有效地防止重放攻击。
此库通过composer提供
composer require ncryptf/ncryptf-php
支持API将返回以下有效负载,至少包含以下信息。
{ "access_token": "7XF56VIP7ZQQOLGHM6MRIK56S2QS363ULNB5UKNFMJRQVYHQH7IA", "refresh_token": "MA2JX5FXWS57DHW4OIHHQDCJVGS3ZKKFCL7XM4GNOB567I6ER4LQ", "ikm": "bDEyECRvKKE8w81fX4hz/52cvHsFPMGeJ+a9fGaVvWM=", "signing": "7v/CdiGoEI7bcj7R2EyDPH5nrCd2+7rHYNACB+Kf2FMx405und2KenGjNpCBPv0jOiptfHJHiY3lldAQTGCdqw==", "expires_at": 1472678411 }
提取元素后,我们可以通过以下方式创建签名请求
use DateTime; use ncryptf\Token; use ncryptf\Authorization; $date = new DateTime; $token = new Token( $accessToken, $refreshToken, \base64_decode($ikm), // IKM must be in it's byte form, as oppose to the base64 representation returned by the server \base64_decode($signature), // Signature is the same, $expiresAt ); $auth = new Authorization( $httpMethod, $uri, $token, new DateTime, $payload ); $header = $auth->getHeader();
以下是一个简单的完整示例
use DateTime; use ncryptf\Token; use ncryptf\Authorization; $date = new DateTime; $token = new Token( '7XF56VIP7ZQQOLGHM6MRIK56S2QS363ULNB5UKNFMJRQVYHQH7IA', 'MA2JX5FXWS57DHW4OIHHQDCJVGS3ZKKFCL7XM4GNOB567I6ER4LQ', \base64_decode('bDEyECRvKKE8w81fX4hz/52cvHsFPMGeJ+a9fGaVvWM='), \base64_decode('7v/CdiGoEI7bcj7R2EyDPH5nrCd2+7rHYNACB+Kf2FMx405und2KenGjNpCBPv0jOiptfHJHiY3lldAQTGCdqw=='), 1472678411 ); $auth = new Authorization( 'POST', '/api/v1/test', $token, new DateTime, [ 'foo' => 'bar' ] ); $header = $auth->getHeader();
注意,在调用
new Authorization
时,应预先偏移$date
参数以防止时间偏移。
Authorization::__construct
中的$payload
参数应该是一个可序列化的JSON数组,但也可以传递序列化的JSON字符串。
版本2 HMAC头
对于支持版本2 HMAC头的API,可以通过调用获取版本2 HMAC头
$header = $auth->getHeader();
版本1 HMAC头
对于使用版本1 HMAC头的API,请在调用new Authorization
时,将可选的version
参数设置为1
作为第6个参数。
$auth = new Authorization( $httpMethod, $uri, $token, new DateTime, $payload, 1 ); $auth->getHeader(),
此字符串可以用作Authorization
头
日期头
版本1 HMAC头需要额外的X-Date
头。可以通过调用authorization.getDateString()
获取X-Date
头。
验证
此库还可以验证客户端生成的HMAC。以下提供了一个高级示例(伪代码)
use DateTime; use ncryptf\Authorization; use ncryptf\Token as NcryptfToken; public function authenticate($user, $request, $response) { // Extract the parameters from the header string $params = Authorization::extractParamsFromHeaderString($request->getHeaders()->get('Authorization')); if ($params) { // Your API should implement a method to fetch all token data from the access token // Typically this is stored in a cache of some kind, such as Redis if ($token = $this->getTokenFromAccessToken($params['access_token'])) { try { // Determine the appropriate date to use, depending upon the version $date = new DateTime($params['date'] ?? $request->getHeaders()->get('X-Date')); // Construct a new server side Authorization object $auth = new Authorization( $request->getHttpMethod(), // GET, POST, PUT... etc $request->getUrl(), // The URI with query parameters $token->getNcryptfToken(), // Your token object should support data extraction to an ncryptf/Token type $date, $request->getRawBody(), // The raw JSON in the request. If you're using encrypted requests, this should be decrypted $params['v'], // The version of the HMAC header to validate \base64_decode($params['salt']) // The salt value from the parameters ); // Verify the HMAC submitted against the newly generated auth object if ($auth->verify(\base64_decode($params['hmac']), $auth)) { // Do your login here // // } } catch (\Exception $e) { // Handle exceptions here } } } // Handle authentication failures }
加密请求和响应
此库使PHP 7.1+的开发者能够在TLS层上建立信任的加密会话,同时(并且独立地)提供通过HMAC+HKDF样式认证验证和识别客户端的能力。
此功能的原因包括但不限于
- 需要额外的安全层
- 对网络或TLS本身缺乏信任(参见https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/)
- 需要确保服务器提供的初始密钥材料(IKM)在HMAC+HKDF认证中的机密性
- 需要确保API认证中用户提交的凭证的机密性
您可能想要与API本身建立加密会话的主要原因是为了确保IKM的机密性,防止数据在不受信任的网络中泄露,以避免在类似Cloudflare事件(或任何中间人攻击)中信息被暴露。加密会话使您在内存泄漏再次发生时,有信心IKM和其他安全数据不会被暴露,从而可以利用像Cloudflare这样的服务。
加密请求数据
负载可以按照以下方式加密
use ncryptf\Request; use ncryptf\Utils; use ncryptf\exceptions\EncryptionFailedException; try { // Generate your request keypair for your local device. $keypair = Utils::generateKeypair(); $signatureKp = Utils::generateSigningKeypair() // Create a new request object with your private key // and the servers private key $request = new Request( $privateKeypair->getSecretKey(), $signatureKp->getSecretKey ); // Encrypt JSON $encryptedRequest = $request->encrypt( '{ "foo": "bar" }', $remotePublicKey ); } catch (EncryptionFailedException $e) { // Encrypting the body failed }
注意,这里只展示了v2加密。
注意,您需要有一个预引导的公钥来加密数据。对于v1 API,这通常是
/api/v1/server/otk
返回的内容。
解密响应
服务器的响应可以按照以下方式解密
use ncryptf\Response; use ncryptf\exceptions\DecryptionFailedException; use ncryptf\exceptions\InvalidChecksumException; use ncryptf\exceptions\InvalidSignatureException; // Represents the httpResponse try { // Create a new request object with your private key // and the servers private key $response = new Response( \sodium_crypto_box_secretkey($privateKeypair['secret']), ); // Extract the raw body from the response $rawBody = \base64_decode($httpResponse->getBody()); $jsonResponse = $response->decrypt( $rawBody, $remotePublicKey ); } catch (DecryptionFailedException $e) { // Decryption failed } catch (InvalidChecksumException $e) { // Request checksum failed } catch (InvalidSignatureException $e) { // Signature verification failed }
V2加密负载
版本2与版本1负载功能相同,不同之处在于所有解密消息所需的组件都包含在负载本身中,而不是分开到单独的标题中。这减轻了开发者需要管理多个标题的担忧。
版本2负载描述如下。每个组件连接在一起。
段 | 长度 |
---|---|
4字节标题DE259002 ,二进制格式 |
4字节 |
随机数 | 24字节 |
与私钥关联的公钥 | 32字节 |
加密体 | X字节 + 16字节MAC |
签名公钥 | 32字节 |
签名或原始请求数据 | 64字节 |
先前元素连接在一起的总和校验和 | 64字节 |
PSR-15中间件
认证
Ncryptf通过ncryptf\middleware\AbstractAuthentication
支持PSR-15中间件,只需扩展以提取令牌和检索用户即可。
use ncryptf\middleware\AbstractAuthentication; final class Authentication extends AbstractAuthentication { /** * Given an access token, return an `ncryptf\Token` instance. */ protected function getTokenFromAccessToken(string $accessToken) :? Token { // Search for token in database return \ncryptf\Token(...); } protected function getUserFromToken(Token $token) { // Convert a token to a user. return User::find() ->where(['access_token' => $token['access_token']]) ->one(); } }
以下是一个简单的示例
use Authentication; use Middlewares\Utils\Dispatcher; $response = Dispatcher::run([ new Authentication, function ($request, $next) { // This is your user, do whatever you need to do here. $user = $request->getAttribute('ncryptf-user'); return $next->handle($request); } ], $request);
安全请求解析
提供了一个PSR-15中间件来解密使用application/vnd.ncryptf+json
加密的请求。请求解密可以独立于认证请求执行,并在需要传输敏感数据但返回HTTP 204或非元数据泄露响应的情况下非常有用。
然而,理想情况下,这始终与认证请求和相应的加密响应相结合。
为了确保消息可以解密,需要三个组件
-
一个PSR-16缓存实例,其中存储您的加密密钥。本指南建议使用Redis或memcache等分布式缓存以方便长期存储。
-
一个表示可缓存的加密密钥的
ncryptf\middleware\EncryptionKeyInterface
类。 -
在请求数据操作之前,在派发器开头注入
ncryptf\middleware\RequestParser
。
use ncryptf\middleware\RequestParser; use Middlewares\Utils\Dispatcher; $PSR16CacheInterface = new class implements \Psr\SimpleCache\CacheInterface {}; $response = Dispatcher::run([ new RequestParser($PSR16CacheInterface), function ($request, $next) { // This is the plain-text decrypted body $decryptedBody = $request->getAttribute('ncryptf-decrypted-body'); // The parsed body $params = $request->getParsedBody(); return $next->handle($request); } ], $request);
安全响应格式化
当与认证的ncryptf请求结合时,ncryptf\middleware\ResponseFormatter
可以将给定的响应格式化为application/vnd.ncryptf+json
响应。格式化器目前只能处理JSON负载。
此实现必须与一个 ncryptf\middleware\AbstractAuthentication
实例一起使用,并建议与由 ncryptf\middleware\RequestParser
处理的安全请求一起使用,以确保消息的端到端加密。
ncryptf\middleware\ResponseFormatter
构造函数接收一个 Psr\SimpleCache\CacheInterface
实例来存储新生成的 ncryptf\middleware\EncryptionKeyInterface
,以及一个 ncryptf\middleware\EncryptionKeyInterface
实例来构造一个新的密钥对,以确保前向安全。
use Authentication; use ncryptf\middleware\EncryptionKeyInterface; use ncryptf\middleware\ResponseFormatter; use ncryptf\middleware\RequestParser; use Middlewares\Utils\Dispatcher; $PSR16CacheInterface = new class implements \Psr\SimpleCache\CacheInterface {}; $response = Dispatcher::run([ new RequestParser($PSR16CacheInterface), new Authentication, new ResponseFormatter($PSR16CacheInterface, $EncryptionKeyInterface::class) function ($request, $next) { return new JsonResponse(['hello' => 'world']) } ], $request);
请参考
tests
目录以获取完整的端到端实现示例。