diff --git a/inc/class-command.php b/inc/class-command.php
index 6b753c7..e5d7fd6 100644
--- a/inc/class-command.php
+++ b/inc/class-command.php
@@ -9,6 +9,7 @@
 namespace WP\OAuth2;
 
 use WP\JWT\JWT;
+use function cli\prompt;
 use function WP_CLI\Utils\get_flag_value;
 
 class Command {
@@ -30,6 +31,9 @@ class Command {
 	 * --redirect_uri=<redirect_uri>
 	 * : The URI users will be redirected to after connecting.
 	 *
+	 * [--sign=<sign>]
+	 * : Path to key file to sign the software statement with.
+	 *
 	 * [--<field>=<value>]
 	 * : Additional claims.
 	 *
@@ -47,6 +51,7 @@ public function create_software_statement( $args, $assoc_args ) {
 
 		$name         = get_flag_value( $assoc_args, 'client_name' );
 		$redirect_uri = get_flag_value( $assoc_args, 'redirect_uri' );
+		$sign         = get_flag_value( $assoc_args, 'sign' );
 
 		$statement = array(
 			'client_uri'    => $client_uri,
@@ -55,7 +60,7 @@ public function create_software_statement( $args, $assoc_args ) {
 			'client_name'   => $name,
 		);
 
-		unset( $assoc_args['client_name'], $assoc_args['redirect_uri'] );
+		unset( $assoc_args['client_name'], $assoc_args['redirect_uri'], $assoc_args['sign'] );
 		$statement = array_merge( $assoc_args, $statement );
 
 		$valid = DynamicClient::validate_statement( (object) $statement );
@@ -64,7 +69,22 @@ public function create_software_statement( $args, $assoc_args ) {
 			\WP_CLI::error( $valid );
 		}
 
-		$signed = JWT::encode( $statement, '', 'none' );
+		if ( $sign ) {
+			$passphrase = prompt( 'Passphrase', '', ': ', true );
+			$key        = openssl_pkey_get_private( 'file://' . $sign, $passphrase );
+
+			if ( ! is_resource( $key ) ) {
+				\WP_CLI::error( 'Invalid private key: ' . openssl_error_string() );
+			}
+
+			if ( ! isset( $statement['iss'] ) ) {
+				$statement['iss'] = $client_uri;
+			}
+
+			$signed = JWT::encode( $statement, $key, 'RS256' );
+		} else {
+			$signed = JWT::encode( $statement, '', 'none' );
+		}
 
 		if ( is_wp_error( $signed ) ) {
 			\WP_CLI::error( $signed );
diff --git a/inc/class-dynamicclient.php b/inc/class-dynamicclient.php
index 0be51ab..b5561c5 100644
--- a/inc/class-dynamicclient.php
+++ b/inc/class-dynamicclient.php
@@ -23,12 +23,13 @@ class DynamicClient implements ClientInterface {
 
 	const SOFTWARE_ID_KEY = '_oauth2_software_id_';
 	const SOFTWARE_STATEMENT_KEY = '_oauth2_software_statement';
+	const VERIFIED_KEY = '_oauth2_verified_statement';
 	const SCHEMA = array(
 		'type'       => 'object',
 		'properties' => array(
 			'software_id'   => array(
 				'type'     => 'string',
-				'format'   => 'uuid', // Todo support in rest_validate
+				'format'   => 'uuid',
 				'required' => true,
 			),
 			'client_name'   => array(
@@ -58,6 +59,9 @@ class DynamicClient implements ClientInterface {
 	/** @var \stdClass */
 	private $statement;
 
+	/** @var bool */
+	private $verified;
+
 	/** @var Client|WP_Error */
 	private $persisted = false;
 
@@ -65,9 +69,11 @@ class DynamicClient implements ClientInterface {
 	 * DynamicClient constructor.
 	 *
 	 * @param \stdClass $statement Software Statement.
+	 * @param bool      $verified  Whether the statement was verified.
 	 */
-	protected function __construct( $statement ) {
+	protected function __construct( $statement, $verified ) {
 		$this->statement = $statement;
+		$this->verified  = $verified;
 	}
 
 	/**
@@ -78,14 +84,85 @@ protected function __construct( $statement ) {
 	 * @return DynamicClient|WP_Error
 	 */
 	public static function from_jwt( $jwt ) {
-		$statement = JWT::decode( $jwt, '', array( 'none' ), 'unsecure' );
-		$valid     = static::validate_statement( $statement );
+		$iss = JWT::get_claim( $jwt, 'iss' );
+
+		if ( ! is_wp_error( $iss ) ) {
+			$key = self::get_signing_key( $iss );
+
+			if ( is_wp_error( $key ) ) {
+				return $key;
+			}
+
+			$statement = JWT::decode( $jwt, $key, array( 'RS256' ) );
+			$verified  = true;
+		} else {
+			$statement = JWT::decode( $jwt, '', array( 'none' ), 'unsecure' );
+			$verified  = false;
+		}
+
+		$valid = static::validate_statement( $statement );
 
 		if ( is_wp_error( $valid ) ) {
 			return $valid;
 		}
 
-		return new static( $statement );
+		return new static( $statement, $verified );
+	}
+
+	/**
+	 * Gets the signing key for a JWT based on its ISS.
+	 *
+	 * @param string $iss
+	 *
+	 * @return resource|WP_Error
+	 */
+	protected static function get_signing_key( $iss ) {
+		$host = parse_url( $iss, PHP_URL_HOST );
+
+		if ( ! $host ) {
+			return new WP_Error( 'invalid_host', __( 'Could not get a valid host.', 'oauth2' ) );
+		}
+
+		$body = self::fetch_signing_key( $host );
+
+		if ( ! $body ) {
+			return new WP_Error( 'empty_body', __( 'Empty body returned by at the well known URL.', 'oauth2' ) );
+		}
+
+		$key = openssl_pkey_get_public( $body );
+
+		if ( ! is_resource( $key ) ) {
+			return new WP_Error( 'invalid_key', sprintf( __( 'Invalid public key: %s.', 'oauth2' ), openssl_error_string() ?: 'unknown' ) );
+		}
+
+		return $key;
+	}
+
+	/**
+	 * Fetch the signing key from the given hostname.
+	 *
+	 * @param string $host
+	 *
+	 * @return string|WP_Error
+	 */
+	protected static function fetch_signing_key( $host ) {
+		$transient = 'oauth2_key_' . $host;
+
+		if ( false === ( $body = get_site_transient( $transient ) ) || ! is_string( $body ) ) {
+			$url = 'https://' . $host . '/.well-known/wp-api/oauth2.pem';
+
+			$response = wp_safe_remote_get( $url );
+
+			if ( is_wp_error( $response ) ) {
+				$body = '';
+			} else {
+				$body = trim( wp_remote_retrieve_body( $response ) );
+			}
+
+			set_site_transient( $transient, $body, 5 * MINUTE_IN_SECONDS );
+		}
+
+		return $body;
 	}
 
 	/**
@@ -112,6 +189,14 @@ public static function validate_statement( $statement ) {
 			}
 		}
 
+		if ( isset( $statement->iss ) ) {
+			$iss_host = parse_url( $statement->iss, PHP_URL_HOST );
+
+			if ( ! $iss_host || $iss_host !== $client_host ) {
+				return new WP_Error( 'client_uri_mismatch', __( 'The statement issuing URI is not on the same domain as the client URI.', 'oauth2' ) );
+			}
+		}
+
 		return true;
 	}
 
@@ -233,6 +318,15 @@ public function get_software_statement() {
 		return $this->statement;
 	}
 
+	/**
+	 * Checks if the software statement was verified as being signed by the client_uri.
+	 *
+	 * @return bool
+	 */
+	public function is_verified() {
+		return $this->verified;
+	}
+
 	/**
 	 * Persists a dynamic client to a real client.
 	 *
@@ -295,6 +389,7 @@ protected function create_persisted_dynamic_client() {
 
 		update_post_meta( $client->get_post_id(), static::SOFTWARE_ID_KEY . $this->get_id(), 1 );
 		update_post_meta( $client->get_post_id(), static::SOFTWARE_STATEMENT_KEY, $this->statement );
+		update_post_meta( $client->get_post_id(), static::VERIFIED_KEY, $this->is_verified() );
 
 		if ( current_user_can( 'publish_post', $client->get_post_id() ) ) {
 			$approved = $client->approve();
diff --git a/theme/oauth2-authorize.php b/theme/oauth2-authorize.php
index a33c7d9..ae05a68 100644
--- a/theme/oauth2-authorize.php
+++ b/theme/oauth2-authorize.php
@@ -67,16 +67,25 @@
 		float: left;
 	}
 
-	.new-client-warning {
+	#login .notice {
 		margin: 5px 0 15px;
-		background-color: #fff8e5;
+		background-color: #fff;
 		border: 1px solid #ccd0d4;
-		border-left-color: #ffb900;
 		border-left-width: 4px;
 		padding: 1px 12px;
 	}
 
-	#login .new-client-warning p {
+	#login .notice-warning {
+		background-color: #fff8e5;
+		border-left-color: #ffb900;
+	}
+
+	#login .notice-success {
+		background-color: #ecf7ed;
+		border-left-color: #46b450;
+	}
+
+	#login .notice p {
 		margin: 0.5em 0;
 		padding: 2px;
 	}
@@ -105,15 +114,27 @@
 	);
 
 	if ( $client instanceof \WP\OAuth2\DynamicClient ) {
-		printf(
-			'<p class="client-description">%s</p>',
-			sprintf(
+		if ( $client->is_verified() ) {
+			printf(
+				'<div class="notice notice-success notice-alt"><p>%s</p></div>',
+				sprintf(
 				/* translators: %1$s: client name. %2$s: the app URI. */
-				__( '%1$s is an application by %2$s.', 'oauth2' ),
-				esc_html( $client->get_name() ),
-				sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer"><code>%1$s</code></a>', esc_url( $client->get_software_statement()->client_uri ) )
-			)
-		);
+					__( '%1$s is verified to be an application by %2$s.', 'oauth2' ),
+					esc_html( $client->get_name() ),
+					sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer"><code>%1$s</code></a>', esc_url( $client->get_software_statement()->client_uri ) )
+				)
+			);
+		} else {
+			printf(
+				'<p class="client-description">%s</p>',
+				sprintf(
+				/* translators: %1$s: client name. %2$s: the app URI. */
+					__( '%1$s is an application by %2$s.', 'oauth2' ),
+					esc_html( $client->get_name() ),
+					sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer"><code>%1$s</code></a>', esc_url( $client->get_software_statement()->client_uri ) )
+				)
+			);
+		}
 	}
 	?>