copernica/webhook-security

PHP库,用于验证webhook安全性

dev-master 2023-01-02 15:14 UTC

This package is not auto-updated.

Last update: 2024-09-23 07:07:30 UTC


README

这是M. Cavage起草的草稿中描述的HTTP消息签名的实现。完整的草稿可以在以下位置找到: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/。这个库可以用来创建新的签名和验证根据草稿规范创建的签名。

Copernica的所有webhook调用都使用本规范中的算法进行签名。如果您使用PHP脚本来处理来自Copernica的webhook,则可以使用此存储库中的类来验证这些传入的webhook请求。但此存储库对其他人来说也很有用,如果您想要签名或验证HTTP请求。

安装

可以通过composer cli执行以下命令来安装此包

composer require copernica/webhook-security

验证传入请求

以下是验证签名的通用示例脚本。如果您有一个处理传入HTTP调用的脚本,并且想要验证这些调用确实来自预期的来源(并且请求未被伪造),您必须执行以下步骤

  • 创建Copernica\Verifier类的实例以从HTTP头中提取签名。
  • 检查签名是否确实覆盖了您预期出现在签名中的HTTP头。例如,Copernica总是至少包括digest、date、host和x-copernica-id头。不覆盖这些头的签名按定义是无效的。
  • 从签名中读取存储的密钥-ID,并从密钥存储(Copernica将密钥存储在DNS中,因此您将必须进行DNS查找,但其他方可能使用不同的技术来共享公钥或密码)中加载适当的密钥。
  • 使用从存储中加载的密钥检查签名是否有效。

几乎在所有情况下,签名还包括“digest”头。要验证调用,您还必须检查传入HTTP请求的消息体是否与digest头匹配。为此,此库包含一个Copernica\Digest类。

请注意,以下示例包含一个适用于验证来自任何来源的传入请求的通用示例。有关Copernica特定示例,请参阅此README文件的下方。

// Include the verifier header file
require_once('Copernica/Verifier.php');

// Include the optional digest verification header
require_once('Copernica/Digest.php');

// Include the optional header normalizer
require_once('Copernica/NormalizedHeaders.php');

try
{
    // get all request headers using helper class
    $headers = new Copernica\NormalizedHeaders(apache_request_headers());

    // new Digest instance for digest verification
    // it is highly recommended to verify digest for message content
    $digest = new Copernica\Digest($headers->getHeader('digest'));

    // for other than GET requests, check if the digest matches the body
    if ($_SERVER['REQUEST_METHOD'] !== 'GET')
    {
        // get request body
        $body = file_get_contents('php://input');

        // check if digest matches
        if (!$digest->matches($body)) throw new Exception("Digest header mismatch");
    }

    // new verifier instance
    $verifier = new Copernica\Verifier(
        $headers->getHeaders(),         // all available headers
        $_SERVER['REQUEST_METHOD'],     // optional request method
        $_SERVER['REQUEST_URI']         // optional request location
    );

    // check if headers is in a signature
    if (!$verifier->contains("digest")) throw new Exception("Signature does not contains digest");

    // pseudo function to get a public key using keyId provided
    $keyPub = $keyStorage->get($verifier->keyId());

    // verify signature correctness
    if (!$verifier->verify($keyPub)) throw new Exception("Signature verification failed");

    // message has been verified
    // @todo process message body
}
catch (Exception $exception)
{
    // the incoming webhook was invalid
    echo("Invalid webhook call: ".$exception->getMessage());

    // @todo add your own handling (like logging)
}

验证Copernica签名

Copernica的签名必须包括(request-target)、host、date、content-length、content-type、digest和x-copernica-id头。最后一个头包含Copernica用于确保调用确实与您的帐户相关的客户ID。验证签名的公钥存储在DNS中,格式与DKIM公钥相同(请检查密钥是否确实存储在copernica.com域中!)。

为了使您的验证脚本更简单,我们在库中包含了一个类,可用于验证Copernica webhook。它负责检查所有头、比较客户-ID并从DNS中获取密钥。

require_once('Copernica/CopernicaRequest.php');

// an exception is thrown if the call did not come from Copernica or is invalid
try
{
    // check if this is a valid request from Copernica (it throws if it isn't)
    $result = new Copernica\CopernicaRequest(
        apache_request_headers(),   // available HTTP headers
        'account_12345',            // Copernica customer ID
        $_SERVER['REQUEST_METHOD'], // request method
        $_SERVER['REQUEST_URI']     // request location
    );

    // get the incoming body data
    $data = $result->getBody();

    // get the content-type
    $type = $result->getHeader('content-type');

    // message has been verified
    // @todo process message body
}
catch (Exception $exception)
{
    // the call did not come from Copernica
    // @todo add your own handling (like logging)
}

签名请求

此库不仅包含验证签名的技术,还包含对输出请求进行签名的技术。如果您想对自己的请求进行签名,这可能会很有用。以下是使用cURL对请求进行签名的通用示例脚本。

// Include the signer header file
require_once('Copernica/Signer.php');

// read a content of a private key
$keyPriv = file_get_contents("test");

// new signature object with "date" header filled in
$signer = new Copernica\Signer(
    $keyPriv,       // private key
    "test",         // keyId signature value
    "RSA-SHA256",   // algorithm signature value
    "POST",         // optional request method
    "/foo"          // optional request location
);

$body = '{"hello": "world"}';

// it is highly recommended to attach digest for message content verification
$digest = "md5=".base64_encode(hash("md5", $body, true));
$date = date(DateTime::RFC822);

// add headers, order in which headers are added will be kept in signature
// if method and location are provided to constructor first header will be (request-target)
$signer
    ->addHeader("host", "example.com")
    ->addHeader("date", $date)
    ->addHeader("digest", $digest);

// check if signature is generated
// signature needs to have a "Date" header as minimum requirements
if (strval($signer) == "") exit("Generated signature is empty, signature requires a \"Date\" as minimum.");

// set request headers and signature
$headers = [
    "Date: $date",
    "Digest: $digest",
    "Signature: $signer",
    "Host: example.com",
    "Content-Type: application/json",
    "Content-Length: ".strlen($body)
];

// cURL request initialization
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "example.com/foo");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_POST, true);

// set headers for request
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

// set body for request
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);

// execute query
$server_output = curl_exec($ch);

// close cURL
curl_close ($ch);