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