true, 'accepts_key_id' => true ]; } /** Signature over the capabilities and the token issued with them, so neither can be swapped alone. */ public static function sign( string $pairing_secret, string $local_token, array $capabilities ): string { return hash_hmac( 'sha256', self::payload( $local_token, $capabilities ), $pairing_secret, false ); } public static function verify( string $pairing_secret, string $local_token, array $capabilities, string $signature ): bool { return hash_equals( self::sign( $pairing_secret, $local_token, $capabilities ), $signature ); } /** Pins each announced capability for the peer's site. Nothing here ever removes one. */ public static function pin( string $site_url, array $capabilities ): void { if ( true === ( $capabilities['signs_responses'] ?? false ) ) { HttpClient::expect_signed_responses( $site_url ); } if ( true === ( $capabilities['accepts_key_id'] ?? false ) ) { HttpClient::expect_key_id( $site_url ); } } /** * Whether a peer pinned for these capabilities has stopped announcing one. * * @param array $announced What a ping or handshake now says. */ public static function was_lowered( string $site_url, array $announced ): bool { return ( HttpClient::peer_accepts_key_id( $site_url ) && true !== ( $announced['accepts_key_id'] ?? false ) ) || ( HttpClient::peer_signs_responses( $site_url ) && true !== ( $announced['signs_responses'] ?? false ) ); } private static function payload( string $local_token, array $capabilities ): string { $on = array_values( array_filter( self::PINNED, static fn( string $name ): bool => true === ( $capabilities[ $name ] ?? false ) ) ); sort( $on ); return "CAPABILITIES\n" . $local_token . "\n" . implode( ',', $on ); } }