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