$parts[1], 'secret' => $secret, ); } /** * Lowercase hex HMAC-SHA256 of the lines joined with "\n". * * @param string $secret Key. * @param string[] $lines Lines. * @return string */ private static function mac( string $secret, array $lines ): string { return hash_hmac( 'sha256', implode( "\n", $lines ), $secret ); } /** * The request signature (section 3.1), hex without the "v1=" prefix. * * @param string $secret Key. * @param string $method HTTP method. * @param string $target Path and query exactly as on the wire. * @param int $ts Unix seconds. * @param string $nonce Nonce. * @param string $body Raw body. * @return string */ public static function request_signature( string $secret, string $method, string $target, int $ts, string $nonce, string $body ): string { return self::mac( $secret, array( 'CSEDGE1-REQ', strtoupper( $method ), $target, (string) $ts, $nonce, hash( 'sha256', $body ) ) ); } /** * The response signature (section 3.2), hex without the "v1=" prefix. * * @param string $secret Key. * @param string $nonce The request's nonce. * @param int $status HTTP status. * @param int $ts Unix seconds. * @param string $body Raw body as sent. * @return string */ public static function response_signature( string $secret, string $nonce, int $status, int $ts, string $body ): string { return self::mac( $secret, array( 'CSEDGE1-RESP', $nonce, (string) $status, (string) $ts, hash( 'sha256', $body ) ) ); } /** * The X-CloudScale-Edge value for one forwarded request (section 3.3). * * @param string $secret Key. * @param string $key_id Key id. * @param int $ts Unix seconds. * @param string $method HTTP method. * @param string $host Host header. * @param string $target Request target as forwarded. * @return string */ public static function forward_header( string $secret, string $key_id, int $ts, string $method, string $host, string $target ): string { $sig = self::mac( $secret, array( 'CSEDGE1-FWD', (string) $ts, strtoupper( $method ), strtolower( $host ), $target ) ); return 'v1.' . $key_id . '.' . $ts . '.' . $sig; } /** * The key id a forward header names, '' when it is not a v1 header. * * @param string $header Header value. * @return string */ public static function forward_key_id( string $header ): string { $parts = explode( '.', $header ); return ( 4 === count( $parts ) && 'v1' === $parts[0] ) ? $parts[1] : ''; } /** * Verifies a forward header: '' when genuine and current, else malformed, unknown_key, * bad_timestamp or bad_signature (the same codes as the Go verifier). * * @param string $secret Key. * @param string $key_id Paired key id. * @param string $header Header value. * @param string $method HTTP method. * @param string $host Host header. * @param string $target Request target. * @param int $now Verifier's clock. * @return string */ public static function verify_forward( string $secret, string $key_id, string $header, string $method, string $host, string $target, int $now ): string { $parts = explode( '.', $header ); if ( 4 !== count( $parts ) || 'v1' !== $parts[0] ) { return 'malformed'; } if ( $parts[1] !== $key_id ) { return 'unknown_key'; } $ts = self::parse_ts( $parts[2], $now ); if ( null === $ts ) { return 'bad_timestamp'; } return hash_equals( self::forward_header( $secret, $key_id, $ts, $method, $host, $target ), $header ) ? '' : 'bad_signature'; } /** * Verifies a signed request: '' when valid, else bad_timestamp or bad_signature. Nonce * replay is the caller's job, because only it holds the nonce store. * * @param string $secret Key. * @param string $method HTTP method as received. * @param string $target Raw REQUEST_URI. * @param string $ts_header X-CSEdge-Timestamp. * @param string $nonce X-CSEdge-Nonce. * @param string $sig X-CSEdge-Signature. * @param string $body Raw body. * @param int $now Verifier's clock. * @return string */ public static function verify_request( string $secret, string $method, string $target, string $ts_header, string $nonce, string $sig, string $body, int $now ): string { $ts = self::parse_ts( $ts_header, $now ); if ( null === $ts ) { return 'bad_timestamp'; } if ( 1 !== preg_match( self::NONCE_RE, $nonce ) ) { return 'bad_signature'; } return hash_equals( 'v1=' . self::request_signature( $secret, $method, $target, $ts, $nonce, $body ), $sig ) ? '' : 'bad_signature'; } /** * Verifies a signed response: '' when valid, else bad_timestamp or bad_signature. Used by * the tests and the local interop client; the plugin only signs responses. * * @param string $secret Key. * @param string $nonce The request's nonce. * @param int $status HTTP status. * @param string $ts_header X-CSEdge-Timestamp. * @param string $sig X-CSEdge-Signature. * @param string $body Raw body. * @param int $now Verifier's clock. * @return string */ public static function verify_response( string $secret, string $nonce, int $status, string $ts_header, string $sig, string $body, int $now ): string { $ts = self::parse_ts( $ts_header, $now ); if ( null === $ts ) { return 'bad_timestamp'; } return hash_equals( 'v1=' . self::response_signature( $secret, $nonce, $status, $ts, $body ), $sig ) ? '' : 'bad_signature'; } /** * A timestamp in canonical decimal within WINDOW of now, or null. "0123" or "+5" are refused, * as Go refuses them. * * @param string $s Text. * @param int $now Clock. * @return int|null */ public static function parse_ts( string $s, int $now ): ?int { if ( 1 !== preg_match( '/^(?:0|-?[1-9][0-9]{0,17})$/', $s ) ) { return null; } $ts = (int) $s; return abs( $now - $ts ) > self::WINDOW ? null : $ts; } /** * The canonical form of an address (section 3.4): lowercase compressed, IPv4-mapped IPv6 * unmapped to plain IPv4. '' when it is not an address. * * @param string $ip Address. * @return string */ public static function canonical_ip( string $ip ): string { $ip = trim( $ip ); if ( '' === $ip || false === filter_var( $ip, FILTER_VALIDATE_IP ) ) { return ''; } $bin = inet_pton( $ip ); if ( false === $bin ) { return ''; } if ( 16 === strlen( $bin ) && 0 === strncmp( $bin, str_repeat( "\0", 10 ) . "\xff\xff", 12 ) ) { $bin = substr( $bin, 12 ); } $out = inet_ntop( $bin ); return is_string( $out ) ? strtolower( $out ) : ''; } /** * The hash sent in place of an administrator's address. Null when it is not an address. * * @param string $secret Key. * @param string $ip Address. * @return string|null */ public static function admin_ip_hash( string $secret, string $ip ): ?string { $c = self::canonical_ip( $ip ); return '' === $c ? null : self::mac( $secret, array( 'ip:' . $c ) ); } /** * A fresh nonce: 16 random bytes, base64url. * * @return string */ public static function new_nonce(): string { return self::b64url( random_bytes( 16 ) ); } }