PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 All 503 releases
jetpack / vendor / wp-php-toolkit / reprint-server / src / class-hmac-client.php

class-hmac-client.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at vendor/wp-php-toolkit/reprint-server/src/class-hmac-client.php

140 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use function WordPress\Reprint\Server\generate_random_bytes;
4
5 require_once __DIR__ . '/utils.php';
6
7 /**
8 * HMAC Client for the Site Export API.
9 *
10 * This class generates the required HMAC signatures for authenticating
11 * requests to the Site Export API. The importing side uses this to sign
12 * all outgoing requests.
13 *
14 * Usage:
15 * $client = new Site_Export_HMAC_Client($shared_secret);
16 * $headers = $client->get_auth_headers($request_body);
17 * // Add $headers to your HTTP request
18 *
19 */
20 class Site_Export_HMAC_Client {
21
22 /**
23 * Value of the X-Auth-Content-Hash header when the request body is
24 * deliberately not signed: this literal string stands where a body hash
25 * would otherwise be. Must match Site_Export_HMAC_Server::UNSIGNED_PAYLOAD.
26 */
27 public const UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD';
28
29 /** @var string */
30 private $secret;
31
32 public function __construct(string $secret) {
33 $this->secret = $secret;
34 }
35
36 /** @return string Hex-encoded 16-byte nonce. */
37 public function generate_nonce(): string {
38 return bin2hex(generate_random_bytes(16));
39 }
40
41 /** @return string Microsecond-precision Unix timestamp. */
42 public function get_timestamp(): string {
43 return sprintf('%.6f', microtime(true));
44 }
45
46 /**
47 * Compute the HMAC signature for a request.
48 *
49 * The signature covers a SHA-256 hash of the body rather than the raw
50 * bytes. This avoids having to predict the exact encoding that libcurl
51 * will produce for multipart/form-data uploads while still binding the
52 * request to a digest: the server verifies the timestamp, nonce, and HMAC
53 * over X-Auth-Content-Hash before it computes or compares any body hash.
54 *
55 * This is intended for small command requests such as preflight or plan
56 * confirmation. Large data transfers
57 * should use an authenticated session and per-chunk hashes instead of
58 * HMAC-signing one large request body.
59 *
60 * Signature = HMAC-SHA256(nonce + timestamp + SHA256(body), secret)
61 *
62 * @param string $nonce Random nonce for this request
63 * @param string $timestamp Request timestamp
64 * @param string $content_hash Hex SHA-256 hash of the request body
65 * @return string Hex-encoded HMAC signature
66 */
67 public function compute_signature(string $nonce, string $timestamp, string $content_hash = ''): string {
68 if ($content_hash === '') {
69 $content_hash = hash('sha256', '');
70 }
71 $message = $nonce . $timestamp . $content_hash;
72 return hash_hmac('sha256', $message, $this->secret);
73 }
74
75 /** Returns all four X-Auth-* headers for a single request. */
76 public function get_auth_headers(string $body = ''): array {
77 $nonce = $this->generate_nonce();
78 $timestamp = $this->get_timestamp();
79 $content_hash = hash('sha256', $body);
80 $signature = $this->compute_signature($nonce, $timestamp, $content_hash);
81
82 return [
83 'X-Auth-Signature' => $signature,
84 'X-Auth-Nonce' => $nonce,
85 'X-Auth-Timestamp' => $timestamp,
86 'X-Auth-Content-Hash' => $content_hash,
87 ];
88 }
89
90 /**
91 * Returns X-Auth-* headers for a request whose body is not signed.
92 *
93 * The signature covers the nonce, the timestamp, the method, and the
94 * request target instead of a body hash, so a body of any size streams
95 * through without either side hashing it, and captured auth headers still cannot be reused for a
96 * different endpoint or method. Protecting the body from tampering is
97 * TLS's job — over --force-http a tampered body would be accepted,
98 * which is what that flag's help text warns about.
99 *
100 * Signature = HMAC-SHA256(nonce + timestamp + "UNSIGNED-PAYLOAD\n" + METHOD + "\n" + target, secret)
101 *
102 * @param string $method Uppercased into the signature (GET, POST, ...).
103 * @param string $url Full request URL; only path and query are signed.
104 */
105 public function get_envelope_auth_headers(string $method, string $url): array {
106 $nonce = $this->generate_nonce();
107 $timestamp = $this->get_timestamp();
108 $message = $nonce . $timestamp . self::UNSIGNED_PAYLOAD . "\n" . strtoupper($method) . "\n" . self::request_target($url);
109
110 return [
111 'X-Auth-Signature' => hash_hmac('sha256', $message, $this->secret),
112 'X-Auth-Nonce' => $nonce,
113 'X-Auth-Timestamp' => $timestamp,
114 'X-Auth-Content-Hash' => self::UNSIGNED_PAYLOAD,
115 ];
116 }
117
118 /**
119 * Normalizes a URL to the "path?query" form both sides sign — the same
120 * shape PHP exposes as $_SERVER['REQUEST_URI'] on the receiving end.
121 */
122 public static function request_target(string $url): string {
123 $path = parse_url($url, PHP_URL_PATH);
124 $query = parse_url($url, PHP_URL_QUERY);
125 $target = is_string($path) && $path !== '' ? $path : '/';
126
127 return is_string($query) && $query !== '' ? $target . '?' . $query : $target;
128 }
129
130 /** Returns auth headers formatted for CURLOPT_HTTPHEADER (["Name: value", ...]). */
131 public function get_curl_headers(string $body = ''): array {
132 $headers = $this->get_auth_headers($body);
133 $curl_headers = [];
134 foreach ($headers as $name => $value) {
135 $curl_headers[] = "{$name}: {$value}";
136 }
137 return $curl_headers;
138 }
139 }
140