$v, 'tenant_id' => Options::tenant_id(), 'store_url' => Config::store_url(), 'ts' => time(), ); } /** * @param array $fields Sent after the envelope. * @param int $v Contract version of the route; 1 unless the route says otherwise. */ public static function body( array $fields, int $v = 1 ): string { return (string) wp_json_encode( array_merge( self::envelope( $v ), $fields ) ); } /** * Whether $signature is base64(HMAC-SHA256(RESPONSE_SIGNATURE_PREFIX . $raw_body, webhook_secret)). Compared in * constant time; false when the store has no secret. */ public static function verify_answer( string $raw_body, string $signature ): bool { $secret = Options::webhook_secret(); if ( '' === $secret || '' === $signature ) { return false; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- HMAC signature encoding, as in contract §5d. $expected = base64_encode( hash_hmac( 'sha256', self::RESPONSE_SIGNATURE_PREFIX . $raw_body, $secret, true ) ); return hash_equals( $expected, trim( $signature ) ); } public static function post( string $path, string $body, int $timeout = self::TIMEOUT ): SignedResponse { $response = wp_remote_post( Config::api_base() . $path, array( 'timeout' => $timeout, 'redirection' => 0, 'sslverify' => true, 'headers' => array( 'Content-Type' => 'application/json', // The backend picks the secret from this header and checks the signature before parsing the body. 'X-Cortex-Tenant' => Options::tenant_id(), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- HMAC signature encoding, as in contract §4. 'X-Cortex-Signature' => base64_encode( hash_hmac( 'sha256', $body, Options::webhook_secret(), true ) ), ), 'body' => $body, ) ); if ( is_wp_error( $response ) ) { return new SignedResponse( 0, null, $response->get_error_message() ); } $raw = (string) wp_remote_retrieve_body( $response ); $json = json_decode( $raw, true ); $signature = self::signature_header( wp_remote_retrieve_header( $response, self::RESPONSE_SIGNATURE_HEADER ) ); return new SignedResponse( (int) wp_remote_retrieve_response_code( $response ), is_array( $json ) ? $json : null, '', $raw, $signature ); } /** * The signature header as one string. Sent more than once, it is used when every copy is the same; copies that * differ give a value that never verifies, so the answer is refused rather than read as unsigned. * * @param string|array $header What wp_remote_retrieve_header() gave. */ private static function signature_header( $header ): string { if ( is_string( $header ) ) { return $header; } $copies = array_values( array_unique( array_map( 'trim', array_filter( (array) $header, 'is_string' ) ) ) ); if ( array() === $copies ) { return ''; } return 1 === count( $copies ) ? $copies[0] : 'conflicting-signatures'; } }