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.FileNotFoundException;
51 import java.io.IOException;
52 import java.io.OutputStreamWriter;
53 import java.net.InetSocketAddress;
54 import java.net.SocketAddress;
55 import java.nio.file.Files;
56 import java.nio.file.InvalidPathException;
57 import java.nio.file.Path;
58 import java.nio.file.Paths;
59 import java.security.GeneralSecurityException;
60 import java.security.PublicKey;
61 import java.security.SecureRandom;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.Collection;
65 import java.util.Collections;
66 import java.util.LinkedList;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.TreeSet;
70 import java.util.concurrent.ConcurrentHashMap;
71 import java.util.function.Supplier;
72
73 import org.apache.sshd.client.config.hosts.HostPatternsHolder;
74 import org.apache.sshd.client.config.hosts.KnownHostDigest;
75 import org.apache.sshd.client.config.hosts.KnownHostEntry;
76 import org.apache.sshd.client.config.hosts.KnownHostHashValue;
77 import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
78 import org.apache.sshd.client.session.ClientSession;
79 import org.apache.sshd.common.NamedFactory;
80 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
81 import org.apache.sshd.common.config.keys.KeyUtils;
82 import org.apache.sshd.common.config.keys.PublicKeyEntry;
83 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
84 import org.apache.sshd.common.digest.BuiltinDigests;
85 import org.apache.sshd.common.mac.Mac;
86 import org.apache.sshd.common.util.io.ModifiableFileWatcher;
87 import org.apache.sshd.common.util.net.SshdSocketAddress;
88 import org.eclipse.jgit.annotations.NonNull;
89 import org.eclipse.jgit.internal.storage.file.LockFile;
90 import org.eclipse.jgit.transport.CredentialItem;
91 import org.eclipse.jgit.transport.CredentialsProvider;
92 import org.eclipse.jgit.transport.URIish;
93 import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
94 import org.slf4j.Logger;
95 import org.slf4j.LoggerFactory;
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
151
152
153
154
155 public class OpenSshServerKeyDatabase
156 implements ServerKeyDatabase {
157
158
159
160
161 private static final Logger LOG = LoggerFactory
162 .getLogger(OpenSshServerKeyDatabase.class);
163
164
165 private static final String MARKER_REVOKED = "revoked";
166
167 private final boolean askAboutNewFile;
168
169 private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
170
171 private final List<HostKeyFile> defaultFiles = new ArrayList<>();
172
173
174
175
176
177
178
179
180
181
182
183
184 public OpenSshServerKeyDatabase(boolean askAboutNewFile,
185 List<Path> defaultFiles) {
186 if (defaultFiles != null) {
187 for (Path file : defaultFiles) {
188 HostKeyFile newFile = new HostKeyFile(file);
189 knownHostsFiles.put(file, newFile);
190 this.defaultFiles.add(newFile);
191 }
192 }
193 this.askAboutNewFile = askAboutNewFile;
194 }
195
196 private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
197 List<HostKeyFile> filesToUse = defaultFiles;
198 List<HostKeyFile> userFiles = addUserHostKeyFiles(
199 config.getUserKnownHostsFiles());
200 if (!userFiles.isEmpty()) {
201 filesToUse = userFiles;
202 }
203 return filesToUse;
204 }
205
206 @Override
207 public List<PublicKey> lookup(@NonNull String connectAddress,
208 @NonNull InetSocketAddress remoteAddress,
209 @NonNull Configuration config) {
210 List<HostKeyFile> filesToUse = getFilesToUse(config);
211 List<PublicKey> result = new ArrayList<>();
212 Collection<SshdSocketAddress> candidates = getCandidates(
213 connectAddress, remoteAddress);
214 for (HostKeyFile file : filesToUse) {
215 for (HostEntryPair current : file.get()) {
216 KnownHostEntry entry = current.getHostEntry();
217 for (SshdSocketAddress host : candidates) {
218 if (entry.isHostMatch(host.getHostName(), host.getPort())) {
219 result.add(current.getServerKey());
220 break;
221 }
222 }
223 }
224 }
225 return result;
226 }
227
228 @Override
229 public boolean accept(@NonNull String connectAddress,
230 @NonNull InetSocketAddress remoteAddress,
231 @NonNull PublicKey serverKey,
232 @NonNull Configuration config, CredentialsProvider provider) {
233 List<HostKeyFile> filesToUse = getFilesToUse(config);
234 AskUser ask = new AskUser(config, provider);
235 HostEntryPair[] modified = { null };
236 Path path = null;
237 Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
238 remoteAddress);
239 for (HostKeyFile file : filesToUse) {
240 try {
241 if (find(candidates, serverKey, file.get(), modified)) {
242 return true;
243 }
244 } catch (RevokedKeyException e) {
245 ask.revokedKey(remoteAddress, serverKey, file.getPath());
246 return false;
247 }
248 if (path == null && modified[0] != null) {
249
250
251 path = file.getPath();
252 }
253 }
254 if (modified[0] != null) {
255
256 AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
257 remoteAddress, modified[0].getServerKey(),
258 serverKey, path);
259 if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
260 try {
261 updateModifiedServerKey(serverKey, modified[0], path);
262 knownHostsFiles.get(path).resetReloadAttributes();
263 } catch (IOException e) {
264 LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
265 path));
266 }
267 }
268 if (toDo == AskUser.ModifiedKeyHandling.DENY) {
269 return false;
270 }
271
272
273
274
275 return true;
276 } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
277 if (!filesToUse.isEmpty()) {
278 HostKeyFile toUpdate = filesToUse.get(0);
279 path = toUpdate.getPath();
280 try {
281 if (Files.exists(path) || !askAboutNewFile
282 || ask.createNewFile(path)) {
283 updateKnownHostsFile(candidates, serverKey, path,
284 config);
285 toUpdate.resetReloadAttributes();
286 }
287 } catch (Exception e) {
288 LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
289 path), e);
290 }
291 }
292 return true;
293 }
294 return false;
295 }
296
297 private static class RevokedKeyException extends Exception {
298 private static final long serialVersionUID = 1L;
299 }
300
301 private boolean find(Collection<SshdSocketAddress> candidates,
302 PublicKey serverKey, List<HostEntryPair> entries,
303 HostEntryPair[] modified) throws RevokedKeyException {
304 for (HostEntryPair current : entries) {
305 KnownHostEntry entry = current.getHostEntry();
306 for (SshdSocketAddress host : candidates) {
307 if (entry.isHostMatch(host.getHostName(), host.getPort())) {
308 boolean isRevoked = MARKER_REVOKED
309 .equals(entry.getMarker());
310 if (KeyUtils.compareKeys(serverKey,
311 current.getServerKey())) {
312
313 if (isRevoked) {
314 throw new RevokedKeyException();
315 }
316 modified[0] = null;
317 return true;
318 } else if (!isRevoked) {
319
320 modified[0] = current;
321
322
323 }
324 }
325 }
326 }
327 return false;
328 }
329
330 private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
331 if (fileNames == null || fileNames.isEmpty()) {
332 return Collections.emptyList();
333 }
334 List<HostKeyFile> userFiles = new ArrayList<>();
335 for (String name : fileNames) {
336 try {
337 Path path = Paths.get(name);
338 HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
339 p -> new HostKeyFile(path));
340 userFiles.add(file);
341 } catch (InvalidPathException e) {
342 LOG.warn(format(SshdText.get().knownHostsInvalidPath,
343 name));
344 }
345 }
346 return userFiles;
347 }
348
349 private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
350 PublicKey serverKey, Path path, Configuration config)
351 throws Exception {
352 String newEntry = createHostKeyLine(candidates, serverKey, config);
353 if (newEntry == null) {
354 return;
355 }
356 LockFile lock = new LockFile(path.toFile());
357 if (lock.lockForAppend()) {
358 try {
359 try (BufferedWriter writer = new BufferedWriter(
360 new OutputStreamWriter(lock.getOutputStream(),
361 UTF_8))) {
362 writer.newLine();
363 writer.write(newEntry);
364 writer.newLine();
365 }
366 lock.commit();
367 } catch (IOException e) {
368 lock.unlock();
369 throw e;
370 }
371 } else {
372 LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
373 path));
374 }
375 }
376
377 private void updateModifiedServerKey(PublicKey serverKey,
378 HostEntryPair entry, Path path)
379 throws IOException {
380 KnownHostEntry hostEntry = entry.getHostEntry();
381 String oldLine = hostEntry.getConfigLine();
382 String newLine = updateHostKeyLine(oldLine, serverKey);
383 if (newLine == null || newLine.isEmpty()) {
384 return;
385 }
386 if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
387
388 return;
389 }
390 LockFile lock = new LockFile(path.toFile());
391 if (lock.lock()) {
392 try {
393 try (BufferedWriter writer = new BufferedWriter(
394 new OutputStreamWriter(lock.getOutputStream(), UTF_8));
395 BufferedReader reader = Files.newBufferedReader(path,
396 UTF_8)) {
397 boolean done = false;
398 String line;
399 while ((line = reader.readLine()) != null) {
400 String toWrite = line;
401 if (!done) {
402 int pos = line.indexOf('#');
403 String toTest = pos < 0 ? line
404 : line.substring(0, pos);
405 if (toTest.trim().equals(oldLine)) {
406 toWrite = newLine;
407 done = true;
408 }
409 }
410 writer.write(toWrite);
411 writer.newLine();
412 }
413 }
414 lock.commit();
415 } catch (IOException e) {
416 lock.unlock();
417 throw e;
418 }
419 } else {
420 LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
421 path));
422 }
423 }
424
425 private static class AskUser {
426
427 public enum ModifiedKeyHandling {
428 DENY, ALLOW, ALLOW_AND_STORE
429 }
430
431 private enum Check {
432 ASK, DENY, ALLOW;
433 }
434
435 private final @NonNull Configuration config;
436
437 private final CredentialsProvider provider;
438
439 public AskUser(@NonNull Configuration config,
440 CredentialsProvider provider) {
441 this.config = config;
442 this.provider = provider;
443 }
444
445 private static boolean askUser(CredentialsProvider provider, URIish uri,
446 String prompt, String... messages) {
447 List<CredentialItem> items = new ArrayList<>(messages.length + 1);
448 for (String message : messages) {
449 items.add(new CredentialItem.InformationalMessage(message));
450 }
451 if (prompt != null) {
452 CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
453 prompt);
454 items.add(answer);
455 return provider.get(uri, items) && answer.getValue();
456 } else {
457 return provider.get(uri, items);
458 }
459 }
460
461 private Check checkMode(SocketAddress remoteAddress, boolean changed) {
462 if (!(remoteAddress instanceof InetSocketAddress)) {
463 return Check.DENY;
464 }
465 switch (config.getStrictHostKeyChecking()) {
466 case REQUIRE_MATCH:
467 return Check.DENY;
468 case ACCEPT_ANY:
469 return Check.ALLOW;
470 case ACCEPT_NEW:
471 return changed ? Check.DENY : Check.ALLOW;
472 default:
473 return provider == null ? Check.DENY : Check.ASK;
474 }
475 }
476
477 public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
478 Path path) {
479 if (provider == null) {
480 return;
481 }
482 InetSocketAddress remote = (InetSocketAddress) remoteAddress;
483 URIish uri = JGitUserInteraction.toURI(config.getUsername(),
484 remote);
485 String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
486 serverKey);
487 String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
488 String keyAlgorithm = serverKey.getAlgorithm();
489 askUser(provider, uri, null,
490 format(SshdText.get().knownHostsRevokedKeyMsg,
491 remote.getHostString(), path),
492 format(SshdText.get().knownHostsKeyFingerprints,
493 keyAlgorithm),
494 md5, sha256);
495 }
496
497 public boolean acceptUnknownKey(SocketAddress remoteAddress,
498 PublicKey serverKey) {
499 Check check = checkMode(remoteAddress, false);
500 if (check != Check.ASK) {
501 return check == Check.ALLOW;
502 }
503 InetSocketAddress remote = (InetSocketAddress) remoteAddress;
504
505 String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
506 serverKey);
507 String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
508 String keyAlgorithm = serverKey.getAlgorithm();
509 String remoteHost = remote.getHostString();
510 URIish uri = JGitUserInteraction.toURI(config.getUsername(),
511 remote);
512 String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
513 return askUser(provider, uri, prompt,
514 format(SshdText.get().knownHostsUnknownKeyMsg,
515 remoteHost),
516 format(SshdText.get().knownHostsKeyFingerprints,
517 keyAlgorithm),
518 md5, sha256);
519 }
520
521 public ModifiedKeyHandling acceptModifiedServerKey(
522 InetSocketAddress remoteAddress, PublicKey expected,
523 PublicKey actual, Path path) {
524 Check check = checkMode(remoteAddress, true);
525 if (check == Check.ALLOW) {
526
527 return ModifiedKeyHandling.ALLOW;
528 }
529 String keyAlgorithm = actual.getAlgorithm();
530 String remoteHost = remoteAddress.getHostString();
531 URIish uri = JGitUserInteraction.toURI(config.getUsername(),
532 remoteAddress);
533 List<String> messages = new ArrayList<>();
534 String warning = format(
535 SshdText.get().knownHostsModifiedKeyWarning,
536 keyAlgorithm, expected.getAlgorithm(), remoteHost,
537 KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
538 KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
539 KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
540 KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
541 messages.addAll(Arrays.asList(warning.split("\n")));
542
543 if (check == Check.DENY) {
544 if (provider != null) {
545 messages.add(format(
546 SshdText.get().knownHostsModifiedKeyDenyMsg, path));
547 askUser(provider, uri, null,
548 messages.toArray(new String[0]));
549 }
550 return ModifiedKeyHandling.DENY;
551 }
552
553 List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
554 for (String message : messages) {
555 items.add(new CredentialItem.InformationalMessage(message));
556 }
557 CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
558 SshdText.get().knownHostsModifiedKeyAcceptPrompt);
559 CredentialItem.YesNoType store = new CredentialItem.YesNoType(
560 SshdText.get().knownHostsModifiedKeyStorePrompt);
561 items.add(proceed);
562 items.add(store);
563 if (provider.get(uri, items) && proceed.getValue()) {
564 return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
565 : ModifiedKeyHandling.ALLOW;
566 }
567 return ModifiedKeyHandling.DENY;
568 }
569
570 public boolean createNewFile(Path path) {
571 if (provider == null) {
572
573 return false;
574 }
575 URIish uri = new URIish().setPath(path.toString());
576 return askUser(provider, uri,
577 format(SshdText.get().knownHostsUserAskCreationPrompt,
578 path),
579 format(SshdText.get().knownHostsUserAskCreationMsg, path));
580 }
581 }
582
583 private static class HostKeyFile extends ModifiableFileWatcher
584 implements Supplier<List<HostEntryPair>> {
585
586 private List<HostEntryPair> entries = Collections.emptyList();
587
588 public HostKeyFile(Path path) {
589 super(path);
590 }
591
592 @Override
593 public List<HostEntryPair> get() {
594 Path path = getPath();
595 try {
596 if (checkReloadRequired()) {
597 if (!Files.exists(path)) {
598
599 resetReloadAttributes();
600 return Collections.emptyList();
601 }
602 LockFile lock = new LockFile(path.toFile());
603 if (lock.lock()) {
604 try {
605 entries = reload(getPath());
606 } finally {
607 lock.unlock();
608 }
609 } else {
610 LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
611 path));
612 }
613 }
614 } catch (IOException e) {
615 LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
616 }
617 return Collections.unmodifiableList(entries);
618 }
619
620 private List<HostEntryPair> reload(Path path) throws IOException {
621 try {
622 List<KnownHostEntry> rawEntries = KnownHostEntryReader
623 .readFromFile(path);
624 updateReloadAttributes();
625 if (rawEntries == null || rawEntries.isEmpty()) {
626 return Collections.emptyList();
627 }
628 List<HostEntryPair> newEntries = new LinkedList<>();
629 for (KnownHostEntry entry : rawEntries) {
630 AuthorizedKeyEntry keyPart = entry.getKeyEntry();
631 if (keyPart == null) {
632 continue;
633 }
634 try {
635 PublicKey serverKey = keyPart.resolvePublicKey(null,
636 PublicKeyEntryResolver.IGNORING);
637 if (serverKey == null) {
638 LOG.warn(format(
639 SshdText.get().knownHostsUnknownKeyType,
640 path, entry.getConfigLine()));
641 } else {
642 newEntries.add(new HostEntryPair(entry, serverKey));
643 }
644 } catch (GeneralSecurityException e) {
645 LOG.warn(format(SshdText.get().knownHostsInvalidLine,
646 path, entry.getConfigLine()));
647 }
648 }
649 return newEntries;
650 } catch (FileNotFoundException e) {
651 resetReloadAttributes();
652 return Collections.emptyList();
653 }
654 }
655 }
656
657 private int parsePort(String s) {
658 try {
659 return Integer.parseInt(s);
660 } catch (NumberFormatException e) {
661 return -1;
662 }
663 }
664
665 private SshdSocketAddress toSshdSocketAddress(@NonNull String address) {
666 String host = null;
667 int port = 0;
668 if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address
669 .charAt(0)) {
670 int end = address.indexOf(
671 HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM);
672 if (end <= 1) {
673 return null;
674 }
675 host = address.substring(1, end);
676 if (end < address.length() - 1
677 && HostPatternsHolder.PORT_VALUE_DELIMITER == address
678 .charAt(end + 1)) {
679 port = parsePort(address.substring(end + 2));
680 }
681 } else {
682 int i = address
683 .lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER);
684 if (i > 0) {
685 port = parsePort(address.substring(i + 1));
686 host = address.substring(0, i);
687 } else {
688 host = address;
689 }
690 }
691 if (port < 0 || port > 65535) {
692 return null;
693 }
694 return new SshdSocketAddress(host, port);
695 }
696
697 private Collection<SshdSocketAddress> getCandidates(
698 @NonNull String connectAddress,
699 @NonNull InetSocketAddress remoteAddress) {
700 Collection<SshdSocketAddress> candidates = new TreeSet<>(
701 SshdSocketAddress.BY_HOST_AND_PORT);
702 candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
703 SshdSocketAddress address = toSshdSocketAddress(connectAddress);
704 if (address != null) {
705 candidates.add(address);
706 }
707 return candidates;
708 }
709
710 private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
711 PublicKey key, Configuration config) throws Exception {
712 StringBuilder result = new StringBuilder();
713 if (config.getHashKnownHosts()) {
714
715
716 NamedFactory<Mac> digester = KnownHostDigest.SHA1;
717 Mac mac = digester.create();
718 SecureRandom prng = new SecureRandom();
719 byte[] salt = new byte[mac.getDefaultBlockSize()];
720 for (SshdSocketAddress address : patterns) {
721 if (result.length() > 0) {
722 result.append(',');
723 }
724 prng.nextBytes(salt);
725 KnownHostHashValue.append(result, digester, salt,
726 KnownHostHashValue.calculateHashValue(
727 address.getHostName(), address.getPort(), mac,
728 salt));
729 }
730 } else {
731 for (SshdSocketAddress address : patterns) {
732 if (result.length() > 0) {
733 result.append(',');
734 }
735 KnownHostHashValue.appendHostPattern(result,
736 address.getHostName(), address.getPort());
737 }
738 }
739 result.append(' ');
740 PublicKeyEntry.appendPublicKeyEntry(result, key);
741 return result.toString();
742 }
743
744 private String updateHostKeyLine(String line, PublicKey newKey)
745 throws IOException {
746
747 int pos = line.indexOf(' ');
748 if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
749
750 pos = line.indexOf(' ', pos + 1);
751 }
752 if (pos < 0) {
753
754 return null;
755 }
756 StringBuilder result = new StringBuilder(line.substring(0, pos + 1));
757 PublicKeyEntry.appendPublicKeyEntry(result, newKey);
758 return result.toString();
759 }
760
761 }