1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150 public class OpenSshServerKeyVerifier
151 implements ServerKeyVerifier, ServerKeyLookup {
152
153
154
155
156 private static final Logger LOG = LoggerFactory
157 .getLogger(OpenSshServerKeyVerifier.class);
158
159
160 private static final String MARKER_REVOKED = "revoked";
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
174
175
176
177
178
179
180
181
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
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
256
257 path = file.getPath();
258 }
259 }
260 if (modified[0] != null) {
261
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
279
280
281
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
321 if (isRevoked) {
322 throw new RevokedKeyException();
323 }
324 modified[0] = null;
325 return true;
326 } else if (!isRevoked) {
327
328 modified[0] = current;
329
330
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
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
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
512
513
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
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
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")) {
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
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
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
699
700
701 private static class HostKeyHelper extends KnownHostsServerKeyVerifier {
702
703 public HostKeyHelper() {
704
705 super((c, r, s) -> false, new File(".").toPath());
706 }
707
708 @Override
709 protected KnownHostEntry prepareKnownHostEntry(
710 ClientSession clientSession, SocketAddress remoteAddress,
711 PublicKey serverKey) throws IOException {
712
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
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
739 return super.resolveHostNetworkIdentities(clientSession,
740 remoteAddress);
741 }
742 }
743
744 }