[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));
+ }
}