[SSHD-1297] Provide KeyUtils.loadPublicKey()

Provide a utility method to read a public key file.
diff --git a/CHANGES.md b/CHANGES.md
index 19dc878..4bef0be 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -32,6 +32,8 @@
 
 ## Minor code helpers
 
+* New utility method `KeyUtils.loadPublicKey()` to read a public key file.
+
 ## Behavioral changes and enhancements
 
 * Netty I/O back-end: respect configurations for `CoreModuleProperties.SOCKET_BACKLOG` and `CoreModuleProperties.SOCKET_REUSEADDR`.
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
index 5d5502e..e9ad39e 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java
@@ -319,6 +319,25 @@
     }
 
     /**
+     * Reads a single {@link PublicKey} from a public key file.
+     *
+     * @param  path                     {@link Path} of the file to read; must not be {@code null}
+     * @return                          the {@link PublicKey}, may be {@code null} if the file is empty
+     * @throws IOException              if the file cannot be read or parsed
+     * @throws GeneralSecurityException if the file contents cannot be read as a single {@link PublicKey}
+     */
+    public static PublicKey loadPublicKey(Path path) throws IOException, GeneralSecurityException {
+        List<AuthorizedKeyEntry> keys = AuthorizedKeyEntry.readAuthorizedKeys(Objects.requireNonNull(path));
+        if (GenericUtils.isEmpty(keys)) {
+            return null;
+        }
+        if (keys.size() > 1) {
+            throw new InvalidKeySpecException("Public key file contains multiple entries: " + path);
+        }
+        return keys.get(0).resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
+    }
+
+    /**
      * @param  keyType                  The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss}
      * @param  keySize                  The key size (in bits)
      * @return                          A {@link KeyPair} of the specified type and size
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsTest.java
index e8f9e2b..ad5e0ad 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsTest.java
@@ -19,13 +19,19 @@
 
 package org.apache.sshd.common.config.keys;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.PosixFilePermission;
 import java.security.DigestException;
+import java.security.PublicKey;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Map;
@@ -42,8 +48,10 @@
 import org.apache.sshd.util.test.NoIoTestCase;
 import org.junit.Assume;
 import org.junit.FixMethodOrder;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runners.MethodSorters;
 
 /**
@@ -52,6 +60,10 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Category({ NoIoTestCase.class })
 public class KeyUtilsTest extends JUnitTestSupport {
+
+    @Rule
+    public TemporaryFolder testDir = new TemporaryFolder();
+
     public KeyUtilsTest() {
         super();
     }
@@ -171,4 +183,58 @@
                     KeyUtils.getCanonicalKeyType(alias));
         }
     }
+
+    @Test
+    public void testLoadPublicKey() throws Exception {
+        Path testFile = testDir.newFile().toPath();
+        try (InputStream testContent = this.getClass().getClassLoader().getResourceAsStream(
+                this.getClass().getPackage().getName().replace('.', '/') + "/loader/openssh/RSA-KeyPair.pub")) {
+            Files.copy(testContent, testFile, StandardCopyOption.REPLACE_EXISTING);
+        }
+        PublicKey key = KeyUtils.loadPublicKey(testFile);
+        assertNotNull(key);
+        assertEquals("ssh-rsa", KeyUtils.getKeyType(key));
+    }
+
+    @Test
+    public void testLoadPublicKeyNonExisting() throws Exception {
+        Path testFile = testDir.getRoot().toPath().resolve("does_not_exist");
+        assertFalse(Files.exists(testFile, LinkOption.NOFOLLOW_LINKS));
+        assertThrows(IOException.class, () -> KeyUtils.loadPublicKey(testFile));
+    }
+
+    @Test
+    public void testLoadPublicKeyEmpty() throws Exception {
+        Path testFile = testDir.newFile().toPath();
+        PublicKey key = KeyUtils.loadPublicKey(testFile);
+        assertNull(key);
+    }
+
+    @Test
+    public void testLoadPublicKeyMultiple() throws Exception {
+        Path testFile = testDir.newFile().toPath();
+        byte[] data;
+        try (InputStream testContent = this.getClass().getClassLoader().getResourceAsStream(
+                this.getClass().getPackage().getName().replace('.', '/') + "/loader/openssh/RSA-KeyPair.pub")) {
+            data = IoUtils.toByteArray(testContent);
+        }
+        int size = data.length;
+        data = Arrays.copyOf(data, 2 * size + 1);
+        data[size] = '\n';
+        System.arraycopy(data, 0, data, size + 1, size);
+        try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
+            Files.copy(in, testFile, StandardCopyOption.REPLACE_EXISTING);
+        }
+        assertThrows(Exception.class, () -> KeyUtils.loadPublicKey(testFile));
+    }
+
+    @Test
+    public void testLoadPublicKeyCorrupt() throws Exception {
+        Path testFile = testDir.newFile().toPath();
+        byte[] data = new byte[42];
+        Arrays.fill(data, (byte) 'a');
+        Files.write(testFile, data);
+        assertEquals(42, Files.size(testFile));
+        assertThrows(Exception.class, () -> KeyUtils.loadPublicKey(testFile));
+    }
 }