copernica / webhook-security
PHP库,用于验证webhook安全性
Requires
- php: >=5.2.0
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);