You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/11/29 17:18:30 UTC

mina-sshd git commit: [SSHD-870] Added hooks and some initial code to allow (limited) usage of OpenPGP key rings for 'authorized_keys' entries resolution

Repository: mina-sshd
Updated Branches:
  refs/heads/master fd99d339d -> b494bbe02


[SSHD-870] Added hooks and some initial code to allow (limited) usage of OpenPGP key rings for 'authorized_keys' entries resolution


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/b494bbe0
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/b494bbe0
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/b494bbe0

Branch: refs/heads/master
Commit: b494bbe020804f66206bb9569fe1924010224856
Parents: fd99d33
Author: Lyor Goldstein <lg...@apache.org>
Authored: Tue Nov 27 12:49:32 2018 +0200
Committer: Lyor Goldstein <lg...@apache.org>
Committed: Thu Nov 29 19:24:22 2018 +0200

----------------------------------------------------------------------
 .gitignore                                      |   5 +
 CHANGES.md                                      |   3 +
 README.md                                       |  24 ++
 .../FileWatcherKeyPairResourceLoader.java       |   2 +-
 .../openpgp/PGPAuthorizedEntriesTracker.java    |  95 +------
 .../openpgp/PGPAuthorizedKeyEntriesLoader.java  | 135 ++++++++++
 .../loader/openpgp/PGPPublicRingWatcher.java    | 264 +++++++++++++++++++
 .../config/keys/loader/openpgp/PGPUtils.java    |  26 ++
 .../openpgp/PGPPublicRingWatcherTest.java       |  88 +++++++
 .../src/test/resources/keyring/pubring.gpg      | Bin 0 -> 2871 bytes
 .../src/test/resources/keyring/random_seed      | Bin 0 -> 600 bytes
 .../src/test/resources/keyring/secring.gpg      | Bin 0 -> 4442 bytes
 .../src/test/resources/keyring/trustdb.gpg      | Bin 0 -> 1360 bytes
 13 files changed, 549 insertions(+), 93 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index f0a63bd..02e2b7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,9 @@ RemoteSystemsTempFiles/
 .idea
 .springBeans
 .externalToolBuilders
+
+# Serialized objects
 *.ser
+
+# Locks and temporary files
+*~

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/CHANGES.md
----------------------------------------------------------------------
diff --git a/CHANGES.md b/CHANGES.md
index 2e44d00..3e190a3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -121,6 +121,9 @@ in order to provide key file(s) location information
 * [SSHD-866](https://issues.apache.org/jira/browse/SSHD-866) - Counting empty challenges separately when enforcing
 max. attempts during `keyboard-interactive` authentication
 
+* [SSHD-870](https://issues.apache.org/jira/browse/SSHD-870) - Added hooks and some initial code to allow (limited) usage
+of [OpenPGP](https://www.openpgp.org/) key rings in `authorized_keys` files
+
 * `SftpCommandMain` shows by default `get/put` command progress using the hash sign (`#`) marker. The marker
 can be enabled/disabled via the `progress` command:
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 14e8c88..19159d4 100644
--- a/README.md
+++ b/README.md
@@ -1975,6 +1975,7 @@ instance with one that is derived from it and overrides the `createDelegateAuthe
 as shown below:
 
 ```java
+// Using PGPAuthorizedEntriesTracker
 public class MyAuthorizedKeysAuthenticatorWithBothPGPAndSsh extends AuthorizedKeysAuthenticator {
     ... constructor(s) ...
 
@@ -1993,8 +1994,31 @@ public class MyAuthorizedKeysAuthenticatorWithBothPGPAndSsh extends AuthorizedKe
         }
     }
 }
+
+// Using PGPPublicRingWatcher
+public class MyAuthorizedKeysAuthenticatorWithBothPGPAndSsh extends AuthorizedKeysAuthenticator {
+    ... constructor(s) ...
+
+    @Override
+    protected PublickeyAuthenticator createDelegateAuthenticator(
+            String username, ServerSession session, Path path,
+            Collection<AuthorizedKeyEntry> entries, PublicKeyEntryResolver fallbackResolver)
+                throws IOException, GeneralSecurityException {
+        PGPPublicRingWatcher watcher = ... obtain an instance ...
+        // Note: need to catch the PGPException and transform it into either an IOException or a GeneralSecurityException
+        Collection<PublicKey> keys = watcher.resolveAuthorizedEntries(session, entries, fallbackResolver);
+        if (GenericUtils.isEmpty(keys)) {
+            return RejectAllPublickeyAuthenticator.INSTANCE;
+        } else {
+            return new KeySetPublickeyAuthenticator(id, keys);
+        }
+    }
+}
+
 ```
 
+**Note:** seems that currently, this capability is limited to v1.x key rings (see [jpgpj - issue 21](https://github.com/justinludwig/jpgpj/issues/21))
+
 ## Useful extra components in _sshd-contrib_
 
 * `InteractivePasswordIdentityProvider` - helps implement a `PasswordIdentityProvider` by delegating calls

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/FileWatcherKeyPairResourceLoader.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/FileWatcherKeyPairResourceLoader.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/FileWatcherKeyPairResourceLoader.java
index b476d13..a731274 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/FileWatcherKeyPairResourceLoader.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/FileWatcherKeyPairResourceLoader.java
@@ -86,7 +86,7 @@ public class FileWatcherKeyPairResourceLoader extends ModifiableFileWatcher impl
             int numKeys = GenericUtils.size(ids);
             if (log.isDebugEnabled()) {
                 log.debug("loadKeyPairs({})[{}] reloaded {} keys from {}",
-                        session, resourceKey, numKeys, path);
+                    session, resourceKey, numKeys, path);
             }
 
             if (numKeys > 0) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedEntriesTracker.java
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedEntriesTracker.java b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedEntriesTracker.java
index 037b24a..eb9df61 100644
--- a/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedEntriesTracker.java
+++ b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedEntriesTracker.java
@@ -20,10 +20,8 @@
 package org.apache.sshd.common.config.keys.loader.openpgp;
 
 import java.io.IOException;
-import java.io.StreamCorruptedException;
 import java.nio.file.Path;
 import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
 import java.security.KeyFactory;
 import java.security.PublicKey;
 import java.security.spec.KeySpec;
@@ -33,15 +31,11 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.TreeSet;
 import java.util.stream.Collectors;
 
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.FilePasswordProviderManager;
 import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
-import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
-import org.apache.sshd.common.keyprovider.KeyTypeIndicator;
 import org.apache.sshd.common.session.SessionContext;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.io.resource.PathResource;
@@ -58,7 +52,8 @@ import org.c02e.jpgpj.Subkey;
  */
 public class PGPAuthorizedEntriesTracker
         extends AbstractLoggingBean
-        implements PGPPublicKeyExtractor, FilePasswordProviderManager, PublicKeyEntryResolver {
+        implements PGPAuthorizedKeyEntriesLoader,
+        FilePasswordProviderManager {
     private FilePasswordProvider filePasswordProvider;
     private final List<PGPPublicKeyFileWatcher> keyFiles;
 
@@ -100,97 +95,13 @@ public class PGPAuthorizedEntriesTracker
         return keyFiles;
     }
 
-    @Override
-    public PublicKey resolve(SessionContext session, String keyType, byte[] keyData)
-            throws IOException, GeneralSecurityException {
-        if (!PGPPublicKeyEntryDataResolver.PGP_KEY_TYPES.contains(keyType)) {
-            return null;
-        }
-
-        String fingerprint = PGPPublicKeyEntryDataResolver.encodeKeyFingerprint(keyData);
-        if (GenericUtils.isEmpty(fingerprint)) {
-            return null;
-        }
-
-        Collection<PublicKey> keys;
-        try {
-            keys = loadMatchingKeyFingerprints(session, Collections.singletonList(fingerprint));
-        } catch (PGPException e) {
-            throw new InvalidKeyException("Failed (" + e.getClass().getSimpleName() + ")"
-                    + " to load key type=" + keyType + " with fingerprint=" + fingerprint
-                    + ": " + e.getMessage(), e);
-        }
-
-        int numKeys = GenericUtils.size(keys);
-        if (numKeys > 1) {
-            throw new StreamCorruptedException("Multiple matches (" + numKeys + ")"
-                + " for " + keyType + " fingerprint=" + fingerprint);
-        }
-
-        return GenericUtils.head(keys);
-    }
-
     public void addWatchedFile(Path p) {
         Objects.requireNonNull(p, "No file provided");
         List<PGPPublicKeyFileWatcher> files = getWatchedFiles();
         files.add(new PGPPublicKeyFileWatcher(p));
     }
 
-    public List<PublicKey> resolveAuthorizedEntries(
-            SessionContext session, Collection<? extends PublicKeyEntry> entries, PublicKeyEntryResolver fallbackResolver)
-                throws IOException, GeneralSecurityException, PGPException {
-        Map<String, ? extends Collection<PublicKeyEntry>> typesMap = KeyTypeIndicator.groupByKeyType(entries);
-        if (GenericUtils.isEmpty(typesMap)) {
-            return Collections.emptyList();
-        }
-
-        List<PublicKey> keys = new ArrayList<>(entries.size());
-        for (Map.Entry<String, ? extends Collection<PublicKeyEntry>> te : typesMap.entrySet()) {
-            String keyType = te.getKey();
-            Collection<PublicKeyEntry> keyEntries = te.getValue();
-            Collection<PublicKey> subKeys = PGPPublicKeyEntryDataResolver.PGP_KEY_TYPES.contains(keyType)
-                ? loadMatchingAuthorizedEntries(session, keyEntries)
-                : PublicKeyEntry.resolvePublicKeyEntries(session, keyEntries, fallbackResolver);
-            if (GenericUtils.isEmpty(subKeys)) {
-                continue;
-            }
-
-            keys.addAll(subKeys);
-        }
-
-        return keys;
-    }
-
-    public List<PublicKey> loadMatchingAuthorizedEntries(
-            SessionContext session, Collection<? extends PublicKeyEntry> entries)
-                throws IOException, GeneralSecurityException, PGPException {
-        int numEntries = GenericUtils.size(entries);
-        if (numEntries <= 0) {
-            return Collections.emptyList();
-        }
-
-        Collection<String> fingerprints = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
-        for (PublicKeyEntry pke : entries) {
-            String keyType = pke.getKeyType();
-            if (GenericUtils.isEmpty(keyType)
-                    || (!PGPPublicKeyEntryDataResolver.PGP_KEY_TYPES.contains(keyType))) {
-                continue;
-            }
-
-            String fp = PGPPublicKeyEntryDataResolver.DEFAULT.encodeEntryKeyData(pke.getKeyData());
-            if (GenericUtils.isEmpty(fp)) {
-                continue;
-            }
-
-            if (!fingerprints.add(fp)) {
-                //noinspection UnnecessaryContinue
-                continue;   // debug breakpoint
-            }
-        }
-
-        return loadMatchingKeyFingerprints(session, fingerprints);
-    }
-
+    @Override
     public List<PublicKey> loadMatchingKeyFingerprints(
             SessionContext session, Collection<String> fingerprints)
                 throws IOException, GeneralSecurityException, PGPException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedKeyEntriesLoader.java
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedKeyEntriesLoader.java b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedKeyEntriesLoader.java
new file mode 100644
index 0000000..3640ed3
--- /dev/null
+++ b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPAuthorizedKeyEntriesLoader.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys.loader.openpgp;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.keyprovider.KeyTypeIndicator;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.GenericUtils;
+import org.bouncycastle.openpgp.PGPException;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PGPAuthorizedKeyEntriesLoader extends PGPPublicKeyExtractor, PublicKeyEntryResolver {
+    @Override
+    default PublicKey resolve(SessionContext session, String keyType, byte[] keyData)
+            throws IOException, GeneralSecurityException {
+        if (!PGPPublicKeyEntryDataResolver.PGP_KEY_TYPES.contains(keyType)) {
+            return null;
+        }
+
+        String fingerprint = PGPPublicKeyEntryDataResolver.encodeKeyFingerprint(keyData);
+        if (GenericUtils.isEmpty(fingerprint)) {
+            return null;
+        }
+
+        Collection<PublicKey> keys;
+        try {
+            keys = loadMatchingKeyFingerprints(session, Collections.singletonList(fingerprint));
+        } catch (PGPException e) {
+            throw new InvalidKeyException("Failed (" + e.getClass().getSimpleName() + ")"
+                    + " to load key type=" + keyType + " with fingerprint=" + fingerprint
+                    + ": " + e.getMessage(), e);
+        }
+
+        int numKeys = GenericUtils.size(keys);
+        if (numKeys > 1) {
+            throw new StreamCorruptedException("Multiple matches (" + numKeys + ")"
+                + " for " + keyType + " fingerprint=" + fingerprint);
+        }
+
+        return GenericUtils.head(keys);
+    }
+
+    default List<PublicKey> resolveAuthorizedEntries(
+            SessionContext session, Collection<? extends PublicKeyEntry> entries, PublicKeyEntryResolver fallbackResolver)
+                throws IOException, GeneralSecurityException, PGPException {
+        Map<String, ? extends Collection<PublicKeyEntry>> typesMap = KeyTypeIndicator.groupByKeyType(entries);
+        if (GenericUtils.isEmpty(typesMap)) {
+            return Collections.emptyList();
+        }
+
+        List<PublicKey> keys = new ArrayList<>(entries.size());
+        for (Map.Entry<String, ? extends Collection<PublicKeyEntry>> te : typesMap.entrySet()) {
+            String keyType = te.getKey();
+            Collection<PublicKeyEntry> keyEntries = te.getValue();
+            Collection<PublicKey> subKeys = PGPPublicKeyEntryDataResolver.PGP_KEY_TYPES.contains(keyType)
+                ? loadMatchingAuthorizedEntries(session, keyEntries)
+                : PublicKeyEntry.resolvePublicKeyEntries(session, keyEntries, fallbackResolver);
+            if (GenericUtils.isEmpty(subKeys)) {
+                continue;
+            }
+
+            keys.addAll(subKeys);
+        }
+
+        return keys;
+    }
+
+    default List<PublicKey> loadMatchingAuthorizedEntries(
+            SessionContext session, Collection<? extends PublicKeyEntry> entries)
+                throws IOException, GeneralSecurityException, PGPException {
+        int numEntries = GenericUtils.size(entries);
+        if (numEntries <= 0) {
+            return Collections.emptyList();
+        }
+
+        Collection<String> fingerprints = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+        for (PublicKeyEntry pke : entries) {
+            String keyType = pke.getKeyType();
+            if (GenericUtils.isEmpty(keyType)
+                    || (!PGPPublicKeyEntryDataResolver.PGP_KEY_TYPES.contains(keyType))) {
+                continue;
+            }
+
+            String fp = PGPPublicKeyEntryDataResolver.DEFAULT.encodeEntryKeyData(pke.getKeyData());
+            if (GenericUtils.isEmpty(fp)) {
+                continue;
+            }
+
+            if (!fingerprints.add(fp)) {
+                //noinspection UnnecessaryContinue
+                continue;   // debug breakpoint
+            }
+        }
+
+        return loadMatchingKeyFingerprints(session, fingerprints);
+    }
+
+    List<PublicKey> loadMatchingKeyFingerprints(
+        SessionContext session, Collection<String> fingerprints)
+            throws IOException, GeneralSecurityException, PGPException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcher.java b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcher.java
new file mode 100644
index 0000000..8d33af9
--- /dev/null
+++ b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcher.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys.loader.openpgp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.KeySpec;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.session.SessionContext;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+import org.apache.sshd.common.util.io.resource.IoResource;
+import org.apache.sshd.common.util.io.resource.PathResource;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.bouncycastle.openpgp.PGPException;
+import org.c02e.jpgpj.Key;
+import org.c02e.jpgpj.Ring;
+import org.c02e.jpgpj.Subkey;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class PGPPublicRingWatcher extends ModifiableFileWatcher implements PGPAuthorizedKeyEntriesLoader {
+    /**
+     * @see <A HREF="https://www.gnupg.org/faq/whats-new-in-2.1.html#nosecring">Removal of the secret keyring</A>
+     */
+    public static final String DEFAULT_PUBLIC_RING_FILENAME = "pubring.gpg";
+
+    /** Holds a {@link Map} whose key=the fingerprint (case <U>insensitive</U>), value=the associated {@link PublicKey} */
+    protected final AtomicReference<NavigableMap<String, PublicKey>> ringKeys =
+        new AtomicReference<>(Collections.emptyNavigableMap());
+
+    public PGPPublicRingWatcher() {
+        this(getDefaultPublicRingFilePath());
+    }
+
+    public PGPPublicRingWatcher(Path file) {
+        super(file);
+    }
+
+    @Override
+    public List<PublicKey> loadMatchingKeyFingerprints(
+            SessionContext session, Collection<String> fingerprints)
+                throws IOException, GeneralSecurityException, PGPException {
+        int numEntries = GenericUtils.size(fingerprints);
+        if (numEntries <= 0) {
+            return Collections.emptyList();
+        }
+
+        Map<String, PublicKey> keysMap = resolveRingKeys(session);
+        if (GenericUtils.isEmpty(keysMap)) {
+            return Collections.emptyList();
+        }
+
+        List<PublicKey> matches = Collections.emptyList();
+        for (String fp : fingerprints) {
+            PublicKey key = keysMap.get(fp);
+            if (key == null) {
+                continue;
+            }
+
+            if (GenericUtils.isEmpty(matches)) {
+                matches = new ArrayList<>(numEntries);
+            }
+            matches.add(key);
+        }
+
+        return matches;
+    }
+
+    protected NavigableMap<String, PublicKey> resolveRingKeys(SessionContext session)
+            throws IOException, GeneralSecurityException, PGPException {
+        NavigableMap<String, PublicKey> keysMap = ringKeys.get();
+        if (GenericUtils.isEmpty(keysMap) || checkReloadRequired()) {
+            ringKeys.set(Collections.emptyNavigableMap());  // mark stale
+
+            if (!exists()) {
+                return ringKeys.get();
+            }
+
+            Path file = getPath();
+            keysMap = reloadRingKeys(session, new PathResource(file));
+
+            int numKeys = GenericUtils.size(keysMap);
+            if (log.isDebugEnabled()) {
+                log.debug("resolveRingKeys({}) reloaded {} keys from {}", session, numKeys, file);
+            }
+
+            if (numKeys > 0) {
+                ringKeys.set(keysMap);
+                updateReloadAttributes();
+            }
+        }
+
+        return keysMap;
+    }
+
+    protected NavigableMap<String, PublicKey> reloadRingKeys(
+            SessionContext session, IoResource<?> resourceKey)
+                throws IOException, GeneralSecurityException, PGPException {
+        Ring ring;
+        try (InputStream stream = resourceKey.openInputStream()) {
+            ring = new Ring(stream);
+        }
+
+        return reloadRingKeys(session, resourceKey, ring);
+    }
+
+    protected NavigableMap<String, PublicKey> reloadRingKeys(
+            SessionContext session, NamedResource resourceKey, Ring ring)
+                throws IOException, GeneralSecurityException, PGPException {
+        return reloadRingKeys(session, resourceKey, ring.getKeys());
+    }
+
+    protected NavigableMap<String, PublicKey> reloadRingKeys(
+            SessionContext session, NamedResource resourceKey, Collection<Key> keys)
+                throws IOException, GeneralSecurityException, PGPException {
+        if (GenericUtils.isEmpty(keys)) {
+            return Collections.emptyNavigableMap();
+        }
+
+        NavigableMap<String, PublicKey> keysMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        boolean debugEnabled = log.isDebugEnabled();
+        for (Key k : keys) {
+            Map<String, Subkey> subKeys = PGPUtils.mapSubKeysByFingerprint(k);
+            for (Map.Entry<String, Subkey> se : subKeys.entrySet()) {
+                String fp = se.getKey();
+                Subkey sk = se.getValue();
+                PublicKey pubKey;
+                try {
+                    pubKey = extractPublicKey(resourceKey, sk);
+                } catch (IOException | GeneralSecurityException | RuntimeException e) {
+                    pubKey = handlePublicKeyExtractionError(session, resourceKey, fp, sk, e);
+                }
+
+                if (debugEnabled) {
+                    log.debug("reloadRingKeys({}) loaded {} key ({}) for fingerprint={} from {}",
+                        session, KeyUtils.getKeyType(pubKey), KeyUtils.getFingerPrint(pubKey), fp, resourceKey.getName());
+                }
+                if (pubKey == null) {
+                    continue;
+                }
+
+                PublicKey prev = keysMap.put(fp, pubKey);
+                if (prev != null) {
+                    PublicKey effective = handleDuplicateKeyFingerprint(session, resourceKey, fp, sk, prev, pubKey);
+                    if (effective == null) {
+                        keysMap.remove(fp);
+                    } else if (!GenericUtils.isSameReference(effective, pubKey)) {
+                        keysMap.put(fp, effective);
+                    }
+                }
+            }
+        }
+
+        return keysMap;
+    }
+
+    /**
+     * Invoked if failed to extract a {@link PublicKey} from a given {@link Subkey}
+     *
+     * @param session The {@link SessionContext} of the invocation - may be {@code null} if
+     * no session context available (e.g., offline tool invocation)
+     * @param resourceKey A key representing the resource from which the key data was read
+     * @param fingerprint The fingerprint value
+     * @param subKey The {@link Subkey} that contains the failed public key
+     * @param reason The reason for the failure
+     * @return The effective key to use - if {@code null} (default behavior) then sub-key is skipped
+     * @throws IOException If failed to process some internal data stream
+     * @throws GeneralSecurityException If failed to generate a surrogate key
+     * @throws PGPException If failed to convert PGP key to Java one
+     */
+    protected PublicKey handlePublicKeyExtractionError(
+            SessionContext session, NamedResource resourceKey, String fingerprint, Subkey subKey, Throwable reason)
+                throws IOException, GeneralSecurityException, PGPException {
+        log.warn("handlePublicKeyExtractionError({}) failed ({}) to extract value for fingerprint={} from {}: {}",
+            session, reason.getClass().getSimpleName(), fingerprint, resourceKey.getName(), reason.getMessage());
+        return null;
+    }
+
+    /**
+    /**
+     * Invoked if duplicate public keys found for the same fingerprint
+     *
+     * @param session The {@link SessionContext} of the invocation - may be {@code null} if
+     * no session context available (e.g., offline tool invocation)
+     * @param resourceKey A key representing the resource from which the key data was read
+     * @param fingerprint The duplicate fingerprint
+     * @param subKey The {@link Subkey} from which the duplicate originated
+     * @param k1 The original {@link PublicKey} associated with this fingerprint
+     * @param k2 The replacing {@link PublicKey} associated for same fingerprint
+     * @return The effective key to use (default=the replacing one) - if {@code null}
+     * then associated for the specified fingerprint is nullified
+     * @throws IOException If failed to process some internal data stream
+     * @throws GeneralSecurityException If failed to generate a surrogate key
+     * @throws PGPException If failed to convert PGP key to Java one
+     */
+    protected PublicKey handleDuplicateKeyFingerprint(
+            SessionContext session, NamedResource resourceKey, String fingerprint, Subkey subKey, PublicKey k1, PublicKey k2)
+                    throws IOException, GeneralSecurityException, PGPException {
+        log.warn("handleDuplicateKeyFingerprint({}) duplicate keys found for fingerprint={} ({}[{}] / {}[{}]) in {}",
+            session, fingerprint, KeyUtils.getKeyType(k1), KeyUtils.getFingerPrint(k1),
+            KeyUtils.getKeyType(k2), KeyUtils.getFingerPrint(k2), resourceKey.getName());
+        return k2;
+    }
+
+    @Override
+    public <K extends PublicKey> K generatePublicKey(String algorithm, Class<K> keyType, KeySpec keySpec)
+            throws GeneralSecurityException {
+        KeyFactory factory = getKeyFactory(algorithm);
+        PublicKey pubKey = factory.generatePublic(keySpec);
+        return keyType.cast(pubKey);
+    }
+
+    protected KeyFactory getKeyFactory(String algorithm) throws GeneralSecurityException {
+        return SecurityUtils.getKeyFactory(algorithm);
+    }
+
+    private static final class LazyDefaultPublicRingPathHolder {
+        private static final Path PATH = PGPUtils.getDefaultPgpFolderPath().resolve(DEFAULT_PUBLIC_RING_FILENAME);
+
+        private LazyDefaultPublicRingPathHolder() {
+            throw new UnsupportedOperationException("No instance");
+        }
+    }
+
+    @SuppressWarnings("synthetic-access")
+    public static Path getDefaultPublicRingFilePath() {
+        return LazyDefaultPublicRingPathHolder.PATH;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPUtils.java
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPUtils.java b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPUtils.java
index 9894301..9bd84f5 100644
--- a/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPUtils.java
+++ b/sshd-openpgp/src/main/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPUtils.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.common.config.keys.loader.openpgp;
 
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -26,7 +27,9 @@ import java.util.NavigableMap;
 import java.util.Set;
 import java.util.TreeMap;
 
+import org.apache.sshd.common.config.keys.IdentityUtils;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.c02e.jpgpj.CompressionAlgorithm;
 import org.c02e.jpgpj.EncryptionAlgorithm;
@@ -41,6 +44,9 @@ import org.c02e.jpgpj.Subkey;
 public final class PGPUtils {
     public static final String DEFAULT_PGP_FILE_SUFFIX = ".gpg";
 
+    public static final String STD_LINUX_PGP_FOLDER_NAME = ".gnupg";
+    public static final String STD_WINDOWS_PGP_FOLDER_NAME = "gnupg";
+
     /** Default MIME type for PGP encrypted files */
     public static final String PGP_ENCRYPTED_FILE = "application/pgp-encrypted";
 
@@ -151,4 +157,24 @@ public final class PGPUtils {
             .findFirst()
             .orElse(null);
     }
+
+    private static final class LazyDefaultPgpKeysFolderHolder {
+        private static final Path PATH =
+            IdentityUtils.getUserHomeFolder()
+                .resolve(OsUtils.isUNIX() ? STD_LINUX_PGP_FOLDER_NAME : STD_WINDOWS_PGP_FOLDER_NAME);
+
+        private LazyDefaultPgpKeysFolderHolder() {
+            throw new UnsupportedOperationException("No instance allowed");
+        }
+    }
+
+    /**
+     * @return The default <A HREF="https://www.gnupg.org/">Gnu Privacy Guard</A> folder used
+     * to hold key files.
+     */
+    @SuppressWarnings("synthetic-access")
+    public static Path getDefaultPgpFolderPath() {
+        return LazyDefaultPgpKeysFolderHolder.PATH;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/test/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcherTest.java
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/test/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcherTest.java b/sshd-openpgp/src/test/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcherTest.java
new file mode 100644
index 0000000..c4aef51
--- /dev/null
+++ b/sshd-openpgp/src/test/java/org/apache/sshd/common/config/keys/loader/openpgp/PGPPublicRingWatcherTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.config.keys.loader.openpgp;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.Map;
+import java.util.NavigableMap;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.resource.PathResource;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.junit.Assume;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class PGPPublicRingWatcherTest extends JUnitTestSupport {
+    public PGPPublicRingWatcherTest() {
+        super();
+    }
+
+    @Test
+    public void testDefaultRingPath() {
+        Path path = PGPPublicRingWatcher.getDefaultPublicRingFilePath();
+        Assume.assumeTrue("File does not exist: " + path, Files.exists(path));
+
+        try {
+            testPublicRingWatcher(path);
+        } catch (Exception e) {
+            outputDebugMessage("Failed (%s) to load keys from ring=%s: %s",
+                e.getClass().getSimpleName(), path, e.getMessage());
+        }
+    }
+
+    @Test
+    public void testResourcesKeyPath() throws Exception {
+        Path dir = CommonTestSupportUtils.resolve(
+            detectSourcesFolder(), TEST_SUBFOLDER, RESOURCES_SUBFOLDER, "keyring");
+        Path file = dir.resolve(PGPPublicRingWatcher.DEFAULT_PUBLIC_RING_FILENAME);
+        Map<String, PublicKey> keys = testPublicRingWatcher(file);
+        assertFalse("No keys extracted", GenericUtils.isEmpty(keys));
+    }
+
+    private NavigableMap<String, PublicKey> testPublicRingWatcher(Path file) throws Exception {
+        PGPPublicRingWatcher watcher = new PGPPublicRingWatcher(file);
+        NavigableMap<String, PublicKey> keys = watcher.reloadRingKeys(null, new PathResource(file));
+        int numKeys = GenericUtils.size(keys);
+        outputDebugMessage("%s: Loaded %d keys from %s", getCurrentTestName(), numKeys, file);
+
+        if (numKeys > 0) {
+            for (Map.Entry<String, PublicKey> ke : keys.entrySet()) {
+                String fp = ke.getKey();
+                PublicKey k = ke.getValue();
+                outputDebugMessage("%s: %s %s %s",
+                    getCurrentTestName(), fp, KeyUtils.getKeyType(k), KeyUtils.getFingerPrint(k));
+            }
+        }
+
+        return keys;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/test/resources/keyring/pubring.gpg
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/test/resources/keyring/pubring.gpg b/sshd-openpgp/src/test/resources/keyring/pubring.gpg
new file mode 100644
index 0000000..1535b1a
Binary files /dev/null and b/sshd-openpgp/src/test/resources/keyring/pubring.gpg differ

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/test/resources/keyring/random_seed
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/test/resources/keyring/random_seed b/sshd-openpgp/src/test/resources/keyring/random_seed
new file mode 100644
index 0000000..f432fad
Binary files /dev/null and b/sshd-openpgp/src/test/resources/keyring/random_seed differ

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/test/resources/keyring/secring.gpg
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/test/resources/keyring/secring.gpg b/sshd-openpgp/src/test/resources/keyring/secring.gpg
new file mode 100644
index 0000000..3e56d5c
Binary files /dev/null and b/sshd-openpgp/src/test/resources/keyring/secring.gpg differ

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/b494bbe0/sshd-openpgp/src/test/resources/keyring/trustdb.gpg
----------------------------------------------------------------------
diff --git a/sshd-openpgp/src/test/resources/keyring/trustdb.gpg b/sshd-openpgp/src/test/resources/keyring/trustdb.gpg
new file mode 100644
index 0000000..fff2d42
Binary files /dev/null and b/sshd-openpgp/src/test/resources/keyring/trustdb.gpg differ