array( 'type' => OPENSSL_KEYTYPE_RSA, 'algo' => OPENSSL_ALGO_SHA256, ), 'rsa-v1_5-sha384' => array( 'type' => OPENSSL_KEYTYPE_RSA, 'algo' => OPENSSL_ALGO_SHA384, ), 'rsa-v1_5-sha512' => array( 'type' => OPENSSL_KEYTYPE_RSA, 'algo' => OPENSSL_ALGO_SHA512, ), // RSA PSS (note: not supported in openssl_verify() until PHP 8.1). 'rsa-pss-sha256' => array( 'type' => OPENSSL_KEYTYPE_RSA, 'algo' => OPENSSL_ALGO_SHA256, ), 'rsa-pss-sha384' => array( 'type' => OPENSSL_KEYTYPE_RSA, 'algo' => OPENSSL_ALGO_SHA384, ), 'rsa-pss-sha512' => array( 'type' => OPENSSL_KEYTYPE_RSA, 'algo' => OPENSSL_ALGO_SHA512, ), // ECDSA. 'ecdsa-p256-sha256' => array( 'type' => OPENSSL_KEYTYPE_EC, 'algo' => OPENSSL_ALGO_SHA256, ), 'ecdsa-p384-sha384' => array( 'type' => OPENSSL_KEYTYPE_EC, 'algo' => OPENSSL_ALGO_SHA384, ), 'ecdsa-p521-sha512' => array( 'type' => OPENSSL_KEYTYPE_EC, 'algo' => OPENSSL_ALGO_SHA512, ), ); /** * Digest algorithms. * * @var string[] */ private $digest_algorithms = array( 'sha-256' => 'sha256', 'sha-512' => 'sha512', ); /** * Generate RFC-9421 compliant Signature-Input and Signature headers for an outgoing HTTP request. * * @param array $args The request arguments. * @param string $url The request URL. * * @return array Request arguments with signature headers. */ public function sign( $args, $url ) { // Standard components to sign. $components = array( '"@method"' => \strtoupper( $args['method'] ), '"@target-uri"' => $url, '"@authority"' => \wp_parse_url( $url, PHP_URL_HOST ), ); if ( isset( $args['headers']['Collection-Synchronization'] ) ) { $components['"collection-synchronization"'] = $args['headers']['Collection-Synchronization']; } $identifiers = \array_keys( $components ); // Add digest if provided. if ( isset( $args['body'] ) ) { $components['"content-digest"'] = $this->generate_digest( $args['body'] ); $identifiers = \array_keys( $components ); $args['headers']['Content-Digest'] = $components['"content-digest"']; } $params = array( 'created' => \strtotime( $args['headers']['Date'] ), 'keyid' => $args['key_id'], 'alg' => 'rsa-v1_5-sha256', ); // Build the signature base string as per RFC-9421. $signature_base = $this->get_signature_base_string( $components, $params ); $signature = null; \openssl_sign( $signature_base, $signature, $args['private_key'], \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); $args['headers']['Signature-Input'] = 'wp=(' . \implode( ' ', $identifiers ) . ')' . $this->get_params_string( $params ); $args['headers']['Signature'] = 'wp=:' . $signature . ':'; return $args; } /** * Verify the HTTP Signature against a request. * * @param array $headers The HTTP headers. * @param string|null $body The request body, if applicable. * @return bool|\WP_Error True, if the signature is valid, WP_Error on failure. */ public function verify( array $headers, $body = null ) { $parsed = $this->parse_signature_labels( $headers ); if ( \is_wp_error( $parsed ) ) { return $parsed; } $errors = new \WP_Error(); foreach ( $parsed as $data ) { $result = $this->verify_signature_label( $data, $headers, $body ); if ( true === $result ) { return true; } if ( \is_wp_error( $result ) ) { $errors->add( $result->get_error_code(), $result->get_error_message() ); } } // No valid signature found. $errors->add_data( array( 'status' => 401 ) ); return $errors; } /** * Generate a digest for the request body. * * @param string $body The request body. * * @return string The digest. */ public function generate_digest( $body ) { return 'sha-256=:' . \base64_encode( \hash( 'sha256', $body, true ) ) . ':'; } /** * Parse the Signature-Input and Signature headers. * * @param array $headers The HTTP headers. * @return array|\WP_Error Parsed signature labels or WP_Error on failure. */ private function parse_signature_labels( array $headers ) { $parsed_inputs = array(); \preg_match_all( '/(?P