View Javadoc
1   /*
2    * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.internal.transport.sshd;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static java.text.MessageFormat.format;
47  
48  import java.io.BufferedReader;
49  import java.io.BufferedWriter;
50  import java.io.File;
51  import java.io.FileNotFoundException;
52  import java.io.IOException;
53  import java.io.OutputStreamWriter;
54  import java.net.InetSocketAddress;
55  import java.net.SocketAddress;
56  import java.nio.file.Files;
57  import java.nio.file.InvalidPathException;
58  import java.nio.file.Path;
59  import java.nio.file.Paths;
60  import java.security.GeneralSecurityException;
61  import java.security.PublicKey;
62  import java.util.ArrayList;
63  import java.util.Collection;
64  import java.util.Collections;
65  import java.util.LinkedList;
66  import java.util.List;
67  import java.util.Locale;
68  import java.util.Map;
69  import java.util.concurrent.ConcurrentHashMap;
70  import java.util.function.Supplier;
71  
72  import org.apache.sshd.client.config.hosts.HostConfigEntry;
73  import org.apache.sshd.client.config.hosts.KnownHostEntry;
74  import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
75  import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
76  import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
77  import org.apache.sshd.client.session.ClientSession;
78  import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
79  import org.apache.sshd.common.config.keys.KeyUtils;
80  import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
81  import org.apache.sshd.common.digest.BuiltinDigests;
82  import org.apache.sshd.common.util.io.ModifiableFileWatcher;
83  import org.apache.sshd.common.util.net.SshdSocketAddress;
84  import org.eclipse.jgit.internal.storage.file.LockFile;
85  import org.eclipse.jgit.transport.CredentialItem;
86  import org.eclipse.jgit.transport.CredentialsProvider;
87  import org.eclipse.jgit.transport.SshConstants;
88  import org.eclipse.jgit.transport.URIish;
89  import org.slf4j.Logger;
90  import org.slf4j.LoggerFactory;
91  
92  /**
93   * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
94   * {@code UserKnownHostsFile} values from the ssh configuration.
95   * <p>
96   * The verifier can be given default known_hosts files in the constructor, which
97   * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
98   * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
99   * uses the given files in the order given. Non-existing or unreadable files are
100  * ignored.
101  * <p>
102  * {@code StrictHostKeyChecking} accepts the following values:
103  * </p>
104  * <dl>
105  * <dt>ask</dt>
106  * <dd>Ask the user whether new or changed keys shall be accepted and be added
107  * to the known_hosts file.</dd>
108  * <dt>yes/true</dt>
109  * <dd>Accept only keys listed in the known_hosts file.</dd>
110  * <dt>no/false</dt>
111  * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
112  * file.</dd>
113  * <dt>accept-new</dt>
114  * <dd>Silently accept keys for new hosts and add them to the known_hosts
115  * file.</dd>
116  * </dl>
117  * <p>
118  * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
119  * default value <b>ask</b> is active.
120  * </p>
121  * <p>
122  * This implementation relies on the {@link ClientSession} being a
123  * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
124  * config file host entry to the session, so it would be unknown here which
125  * entry it was and what setting of {@code StrictHostKeyChecking} should be
126  * used. If used with some other session type, the implementation assumes
127  * "<b>ask</b>".
128  * <p>
129  * <p>
130  * Asking the user is done via a {@link CredentialsProvider} obtained from the
131  * session. If none is set, the implementation falls back to strict host key
132  * checking ("<b>yes</b>").
133  * </p>
134  * <p>
135  * Note that adding a key to the known hosts file may create the file. You can
136  * specify in the constructor whether the user shall be asked about that, too.
137  * If the user declines updating the file, but the key was otherwise
138  * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
139  * active), the key is accepted for this session only.
140  * </p>
141  * <p>
142  * If several known hosts files are specified, a new key is always added to the
143  * first file (even if it doesn't exist yet; see the note about file creation
144  * above).
145  * </p>
146  *
147  * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
148  *      ssh-config</a>
149  */
150 public class OpenSshServerKeyVerifier
151 		implements ServerKeyVerifier, ServerKeyLookup {
152 
153 	// TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
154 	// files may be large!
155 
156 	private static final Logger LOG = LoggerFactory
157 			.getLogger(OpenSshServerKeyVerifier.class);
158 
159 	/** Can be used to mark revoked known host lines. */
160 	private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
161 
162 	private final boolean askAboutNewFile;
163 
164 	private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
165 
166 	private final List<HostKeyFile> defaultFiles = new ArrayList<>();
167 
168 	private enum ModifiedKeyHandling {
169 		DENY, ALLOW, ALLOW_AND_STORE
170 	}
171 
172 	/**
173 	 * Creates a new {@link OpenSshServerKeyVerifier}.
174 	 *
175 	 * @param askAboutNewFile
176 	 *            whether to ask the user, if possible, about creating a new
177 	 *            non-existing known_hosts file
178 	 * @param defaultFiles
179 	 *            typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
180 	 *            empty or {@code null}, in which case no default files are
181 	 *            installed. The files need not exist.
182 	 */
183 	public OpenSshServerKeyVerifier(boolean askAboutNewFile,
184 			List<Path> defaultFiles) {
185 		if (defaultFiles != null) {
186 			for (Path file : defaultFiles) {
187 				HostKeyFile newFile = new HostKeyFile(file);
188 				knownHostsFiles.put(file, newFile);
189 				this.defaultFiles.add(newFile);
190 			}
191 		}
192 		this.askAboutNewFile = askAboutNewFile;
193 	}
194 
195 	private List<HostKeyFile> getFilesToUse(ClientSession session) {
196 		List<HostKeyFile> filesToUse = defaultFiles;
197 		if (session instanceof JGitClientSession) {
198 			HostConfigEntry entry = ((JGitClientSession) session)
199 					.getHostConfigEntry();
200 			if (entry instanceof JGitHostConfigEntry) {
201 				// Always true!
202 				List<HostKeyFile> userFiles = addUserHostKeyFiles(
203 						((JGitHostConfigEntry) entry).getMultiValuedOptions()
204 								.get(SshConstants.USER_KNOWN_HOSTS_FILE));
205 				if (!userFiles.isEmpty()) {
206 					filesToUse = userFiles;
207 				}
208 			}
209 		}
210 		return filesToUse;
211 	}
212 
213 	@Override
214 	public List<HostEntryPair> lookup(ClientSession session,
215 			SocketAddress remote) {
216 		List<HostKeyFile> filesToUse = getFilesToUse(session);
217 		HostKeyHelper helper = new HostKeyHelper();
218 		List<HostEntryPair> result = new ArrayList<>();
219 		Collection<SshdSocketAddress> candidates = helper
220 				.resolveHostNetworkIdentities(session, remote);
221 		for (HostKeyFile file : filesToUse) {
222 			for (HostEntryPair current : file.get()) {
223 				KnownHostEntry entry = current.getHostEntry();
224 				for (SshdSocketAddress host : candidates) {
225 					if (entry.isHostMatch(host.getHostName(), host.getPort())) {
226 						result.add(current);
227 						break;
228 					}
229 				}
230 			}
231 		}
232 		return result;
233 	}
234 
235 	@Override
236 	public boolean verifyServerKey(ClientSession clientSession,
237 			SocketAddress remoteAddress, PublicKey serverKey) {
238 		List<HostKeyFile> filesToUse = getFilesToUse(clientSession);
239 		AskUser ask = new AskUser();
240 		HostEntryPair[] modified = { null };
241 		Path path = null;
242 		HostKeyHelper helper = new HostKeyHelper();
243 		for (HostKeyFile file : filesToUse) {
244 			try {
245 				if (find(clientSession, remoteAddress, serverKey, file.get(),
246 						modified, helper)) {
247 					return true;
248 				}
249 			} catch (RevokedKeyException e) {
250 				ask.revokedKey(clientSession, remoteAddress, serverKey,
251 						file.getPath());
252 				return false;
253 			}
254 			if (path == null && modified[0] != null) {
255 				// Remember the file in which we might need to update the
256 				// entry
257 				path = file.getPath();
258 			}
259 		}
260 		if (modified[0] != null) {
261 			// We found an entry, but with a different key
262 			ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
263 					clientSession, remoteAddress, modified[0].getServerKey(),
264 					serverKey, path);
265 			if (toDo == ModifiedKeyHandling.ALLOW_AND_STORE) {
266 				try {
267 					updateModifiedServerKey(clientSession, remoteAddress,
268 							serverKey, modified[0], path, helper);
269 					knownHostsFiles.get(path).resetReloadAttributes();
270 				} catch (IOException e) {
271 					LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
272 							path));
273 				}
274 			}
275 			if (toDo == ModifiedKeyHandling.DENY) {
276 				return false;
277 			}
278 			// TODO: OpenSsh disables password and keyboard-interactive
279 			// authentication in this case. Also agent and local port forwarding
280 			// are switched off. (Plus a few other things such as X11 forwarding
281 			// that are of no interest to a git client.)
282 			return true;
283 		} else if (ask.acceptUnknownKey(clientSession, remoteAddress,
284 				serverKey)) {
285 			if (!filesToUse.isEmpty()) {
286 				HostKeyFile toUpdate = filesToUse.get(0);
287 				path = toUpdate.getPath();
288 				try {
289 					updateKnownHostsFile(clientSession, remoteAddress,
290 							serverKey, path, helper);
291 					toUpdate.resetReloadAttributes();
292 				} catch (IOException e) {
293 					LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
294 							path));
295 				}
296 			}
297 			return true;
298 		}
299 		return false;
300 	}
301 
302 	private static class RevokedKeyException extends Exception {
303 		private static final long serialVersionUID = 1L;
304 	}
305 
306 	private boolean find(ClientSession clientSession,
307 			SocketAddress remoteAddress, PublicKey serverKey,
308 			List<HostEntryPair> entries, HostEntryPair[] modified,
309 			HostKeyHelper helper) throws RevokedKeyException {
310 		Collection<SshdSocketAddress> candidates = helper
311 				.resolveHostNetworkIdentities(clientSession, remoteAddress);
312 		for (HostEntryPair current : entries) {
313 			KnownHostEntry entry = current.getHostEntry();
314 			for (SshdSocketAddress host : candidates) {
315 				if (entry.isHostMatch(host.getHostName(), host.getPort())) {
316 					boolean isRevoked = MARKER_REVOKED
317 							.equals(entry.getMarker());
318 					if (KeyUtils.compareKeys(serverKey,
319 							current.getServerKey())) {
320 						// Exact match
321 						if (isRevoked) {
322 							throw new RevokedKeyException();
323 						}
324 						modified[0] = null;
325 						return true;
326 					} else if (!isRevoked) {
327 						// Server sent a different key
328 						modified[0] = current;
329 						// Keep going -- maybe there's another entry for this
330 						// host
331 					}
332 				}
333 			}
334 		}
335 		return false;
336 	}
337 
338 	private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
339 		if (fileNames == null || fileNames.isEmpty()) {
340 			return Collections.emptyList();
341 		}
342 		List<HostKeyFile> userFiles = new ArrayList<>();
343 		for (String name : fileNames) {
344 			try {
345 				Path path = Paths.get(name);
346 				HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
347 						p -> new HostKeyFile(path));
348 				userFiles.add(file);
349 			} catch (InvalidPathException e) {
350 				LOG.warn(format(SshdText.get().knownHostsInvalidPath,
351 						name));
352 			}
353 		}
354 		return userFiles;
355 	}
356 
357 	private void updateKnownHostsFile(ClientSession clientSession,
358 			SocketAddress remoteAddress, PublicKey serverKey, Path path,
359 			HostKeyHelper updater)
360 			throws IOException {
361 		KnownHostEntry entry = updater.prepareKnownHostEntry(clientSession,
362 				remoteAddress, serverKey);
363 		if (entry == null) {
364 			return;
365 		}
366 		if (!Files.exists(path)) {
367 			if (askAboutNewFile) {
368 				CredentialsProvider provider = getCredentialsProvider(
369 						clientSession);
370 				if (provider == null) {
371 					// We can't ask, so don't create the file
372 					return;
373 				}
374 				URIish uri = new URIish().setPath(path.toString());
375 				if (!askUser(provider, uri, //
376 						format(SshdText.get().knownHostsUserAskCreationPrompt,
377 								path), //
378 						format(SshdText.get().knownHostsUserAskCreationMsg,
379 								path))) {
380 					return;
381 				}
382 			}
383 		}
384 		LockFile lock = new LockFile(path.toFile());
385 		if (lock.lockForAppend()) {
386 			try {
387 				try (BufferedWriter writer = new BufferedWriter(
388 						new OutputStreamWriter(lock.getOutputStream(),
389 								UTF_8))) {
390 					writer.newLine();
391 					writer.write(entry.getConfigLine());
392 					writer.newLine();
393 				}
394 				lock.commit();
395 			} catch (IOException e) {
396 				lock.unlock();
397 				throw e;
398 			}
399 		} else {
400 			LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
401 					path));
402 		}
403 	}
404 
405 	private void updateModifiedServerKey(ClientSession clientSession,
406 			SocketAddress remoteAddress, PublicKey serverKey,
407 			HostEntryPair entry, Path path, HostKeyHelper helper)
408 			throws IOException {
409 		KnownHostEntry hostEntry = entry.getHostEntry();
410 		String oldLine = hostEntry.getConfigLine();
411 		String newLine = helper.prepareModifiedServerKeyLine(clientSession,
412 				remoteAddress, hostEntry, oldLine, entry.getServerKey(),
413 				serverKey);
414 		if (newLine == null || newLine.isEmpty()) {
415 			return;
416 		}
417 		if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
418 			// Shouldn't happen.
419 			return;
420 		}
421 		LockFile lock = new LockFile(path.toFile());
422 		if (lock.lock()) {
423 			try {
424 				try (BufferedWriter writer = new BufferedWriter(
425 						new OutputStreamWriter(lock.getOutputStream(), UTF_8));
426 						BufferedReader reader = Files.newBufferedReader(path,
427 								UTF_8)) {
428 					boolean done = false;
429 					String line;
430 					while ((line = reader.readLine()) != null) {
431 						String toWrite = line;
432 						if (!done) {
433 							int pos = line.indexOf('#');
434 							String toTest = pos < 0 ? line
435 									: line.substring(0, pos);
436 							if (toTest.trim().equals(oldLine)) {
437 								toWrite = newLine;
438 								done = true;
439 							}
440 						}
441 						writer.write(toWrite);
442 						writer.newLine();
443 					}
444 				}
445 				lock.commit();
446 			} catch (IOException e) {
447 				lock.unlock();
448 				throw e;
449 			}
450 		} else {
451 			LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
452 					path));
453 		}
454 	}
455 
456 	private static CredentialsProvider getCredentialsProvider(
457 			ClientSession session) {
458 		if (session instanceof JGitClientSession) {
459 			return ((JGitClientSession) session).getCredentialsProvider();
460 		}
461 		return null;
462 	}
463 
464 	private static boolean askUser(CredentialsProvider provider, URIish uri,
465 			String prompt, String... messages) {
466 		List<CredentialItem> items = new ArrayList<>(messages.length + 1);
467 		for (String message : messages) {
468 			items.add(new CredentialItem.InformationalMessage(message));
469 		}
470 		if (prompt != null) {
471 			CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
472 					prompt);
473 			items.add(answer);
474 			return provider.get(uri, items) && answer.getValue();
475 		} else {
476 			return provider.get(uri, items);
477 		}
478 	}
479 
480 	private static class AskUser {
481 
482 		private enum Check {
483 			ASK, DENY, ALLOW;
484 		}
485 
486 		@SuppressWarnings("nls")
487 		private Check checkMode(ClientSession session,
488 				SocketAddress remoteAddress, boolean changed) {
489 			if (!(remoteAddress instanceof InetSocketAddress)) {
490 				return Check.DENY;
491 			}
492 			if (session instanceof JGitClientSession) {
493 				HostConfigEntry entry = ((JGitClientSession) session)
494 						.getHostConfigEntry();
495 				String value = entry.getProperty(
496 						SshConstants.STRICT_HOST_KEY_CHECKING, "ask");
497 				switch (value.toLowerCase(Locale.ROOT)) {
498 				case SshConstants.YES:
499 				case SshConstants.ON:
500 					return Check.DENY;
501 				case SshConstants.NO:
502 				case SshConstants.OFF:
503 					return Check.ALLOW;
504 				case "accept-new":
505 					return changed ? Check.DENY : Check.ALLOW;
506 				default:
507 					break;
508 				}
509 			}
510 			if (getCredentialsProvider(session) == null) {
511 				// This is called only for new, unknown hosts. If we have no way
512 				// to interact with the user, the fallback mode is to deny the
513 				// key.
514 				return Check.DENY;
515 			}
516 			return Check.ASK;
517 		}
518 
519 		public void revokedKey(ClientSession clientSession,
520 				SocketAddress remoteAddress, PublicKey serverKey, Path path) {
521 			CredentialsProvider provider = getCredentialsProvider(
522 					clientSession);
523 			if (provider == null) {
524 				return;
525 			}
526 			InetSocketAddress remote = (InetSocketAddress) remoteAddress;
527 			URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
528 					remote);
529 			String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
530 					serverKey);
531 			String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
532 			String keyAlgorithm = serverKey.getAlgorithm();
533 			askUser(provider, uri, null, //
534 					format(SshdText.get().knownHostsRevokedKeyMsg,
535 							remote.getHostString(), path),
536 					format(SshdText.get().knownHostsKeyFingerprints,
537 							keyAlgorithm),
538 					md5, sha256);
539 		}
540 
541 		public boolean acceptUnknownKey(ClientSession clientSession,
542 				SocketAddress remoteAddress, PublicKey serverKey) {
543 			Check check = checkMode(clientSession, remoteAddress, false);
544 			if (check != Check.ASK) {
545 				return check == Check.ALLOW;
546 			}
547 			CredentialsProvider provider = getCredentialsProvider(
548 					clientSession);
549 			InetSocketAddress remote = (InetSocketAddress) remoteAddress;
550 			// Ask the user
551 			String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
552 					serverKey);
553 			String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
554 			String keyAlgorithm = serverKey.getAlgorithm();
555 			String remoteHost = remote.getHostString();
556 			URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
557 					remote);
558 			String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
559 			return askUser(provider, uri, prompt, //
560 					format(SshdText.get().knownHostsUnknownKeyMsg,
561 							remoteHost),
562 					format(SshdText.get().knownHostsKeyFingerprints,
563 							keyAlgorithm),
564 					md5, sha256);
565 		}
566 
567 		public ModifiedKeyHandling acceptModifiedServerKey(
568 				ClientSession clientSession,
569 				SocketAddress remoteAddress, PublicKey expected,
570 				PublicKey actual, Path path) {
571 			Check check = checkMode(clientSession, remoteAddress, true);
572 			if (check == Check.ALLOW) {
573 				// Never auto-store on CHECK.ALLOW
574 				return ModifiedKeyHandling.ALLOW;
575 			}
576 			InetSocketAddress remote = (InetSocketAddress) remoteAddress;
577 			String keyAlgorithm = actual.getAlgorithm();
578 			String remoteHost = remote.getHostString();
579 			URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
580 					remote);
581 			List<String> messages = new ArrayList<>();
582 			String warning = format(
583 					SshdText.get().knownHostsModifiedKeyWarning,
584 					keyAlgorithm, expected.getAlgorithm(), remoteHost,
585 					KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
586 					KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
587 					KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
588 					KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
589 			for (String line : warning.split("\n")) { //$NON-NLS-1$
590 				messages.add(line);
591 			}
592 
593 			CredentialsProvider provider = getCredentialsProvider(
594 					clientSession);
595 			if (check == Check.DENY) {
596 				if (provider != null) {
597 					messages.add(format(
598 							SshdText.get().knownHostsModifiedKeyDenyMsg, path));
599 					askUser(provider, uri, null,
600 							messages.toArray(new String[0]));
601 				}
602 				return ModifiedKeyHandling.DENY;
603 			}
604 			// ASK -- two questions: procceed? and store?
605 			List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
606 			for (String message : messages) {
607 				items.add(new CredentialItem.InformationalMessage(message));
608 			}
609 			CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
610 					SshdText.get().knownHostsModifiedKeyAcceptPrompt);
611 			CredentialItem.YesNoType store = new CredentialItem.YesNoType(
612 					SshdText.get().knownHostsModifiedKeyStorePrompt);
613 			items.add(proceed);
614 			items.add(store);
615 			if (provider.get(uri, items) && proceed.getValue()) {
616 				return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
617 						: ModifiedKeyHandling.ALLOW;
618 			}
619 			return ModifiedKeyHandling.DENY;
620 		}
621 
622 	}
623 
624 	private static class HostKeyFile extends ModifiableFileWatcher
625 			implements Supplier<List<HostEntryPair>> {
626 
627 		private List<HostEntryPair> entries = Collections.emptyList();
628 
629 		public HostKeyFile(Path path) {
630 			super(path);
631 		}
632 
633 		@Override
634 		public List<HostEntryPair> get() {
635 			Path path = getPath();
636 			try {
637 				if (checkReloadRequired()) {
638 					if (!Files.exists(path)) {
639 						// Has disappeared.
640 						resetReloadAttributes();
641 						return Collections.emptyList();
642 					}
643 					LockFile lock = new LockFile(path.toFile());
644 					if (lock.lock()) {
645 						try {
646 							entries = reload(getPath());
647 						} finally {
648 							lock.unlock();
649 						}
650 					} else {
651 						LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
652 								path));
653 					}
654 				}
655 			} catch (IOException e) {
656 				LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
657 			}
658 			return Collections.unmodifiableList(entries);
659 		}
660 
661 		private List<HostEntryPair> reload(Path path) throws IOException {
662 			try {
663 				List<KnownHostEntry> rawEntries = KnownHostEntryReader
664 						.readFromFile(path);
665 				updateReloadAttributes();
666 				if (rawEntries == null || rawEntries.isEmpty()) {
667 					return Collections.emptyList();
668 				}
669 				List<HostEntryPair> newEntries = new LinkedList<>();
670 				for (KnownHostEntry entry : rawEntries) {
671 					AuthorizedKeyEntry keyPart = entry.getKeyEntry();
672 					if (keyPart == null) {
673 						continue;
674 					}
675 					try {
676 						PublicKey serverKey = keyPart.resolvePublicKey(
677 								PublicKeyEntryResolver.IGNORING);
678 						if (serverKey == null) {
679 							LOG.warn(format(
680 									SshdText.get().knownHostsUnknownKeyType,
681 									path, entry.getConfigLine()));
682 						} else {
683 							newEntries.add(new HostEntryPair(entry, serverKey));
684 						}
685 					} catch (GeneralSecurityException e) {
686 						LOG.warn(format(SshdText.get().knownHostsInvalidLine,
687 								path, entry.getConfigLine()));
688 					}
689 				}
690 				return newEntries;
691 			} catch (FileNotFoundException e) {
692 				resetReloadAttributes();
693 				return Collections.emptyList();
694 			}
695 		}
696 	}
697 
698 	// The stuff below is just a hack to avoid having to copy a lot of code from
699 	// KnownHostsServerKeyVerifier
700 
701 	private static class HostKeyHelper extends KnownHostsServerKeyVerifier {
702 
703 		public HostKeyHelper() {
704 			// These two arguments will never be used in any way.
705 			super((c, r, s) -> false, new File(".").toPath()); //$NON-NLS-1$
706 		}
707 
708 		@Override
709 		protected KnownHostEntry prepareKnownHostEntry(
710 				ClientSession clientSession, SocketAddress remoteAddress,
711 				PublicKey serverKey) throws IOException {
712 			// Make this method accessible
713 			try {
714 				return super.prepareKnownHostEntry(clientSession, remoteAddress,
715 						serverKey);
716 			} catch (Exception e) {
717 				throw new IOException(e.getMessage(), e);
718 			}
719 		}
720 
721 		@Override
722 		protected String prepareModifiedServerKeyLine(
723 				ClientSession clientSession, SocketAddress remoteAddress,
724 				KnownHostEntry entry, String curLine, PublicKey expected,
725 				PublicKey actual) throws IOException {
726 			// Make this method accessible
727 			try {
728 				return super.prepareModifiedServerKeyLine(clientSession,
729 						remoteAddress, entry, curLine, expected, actual);
730 			} catch (Exception e) {
731 				throw new IOException(e.getMessage(), e);
732 			}
733 		}
734 
735 		@Override
736 		protected Collection<SshdSocketAddress> resolveHostNetworkIdentities(
737 				ClientSession clientSession, SocketAddress remoteAddress) {
738 			// Make this method accessible
739 			return super.resolveHostNetworkIdentities(clientSession,
740 					remoteAddress);
741 		}
742 	}
743 
744 }