| // Copyright (C) 2008 The Android Open Source Project | |
| // | |
| // Licensed 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 com.google.gerrit.sshd; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import static java.nio.charset.StandardCharsets.ISO_8859_1; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.base.Splitter; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.common.io.BaseEncoding; | |
| import com.google.gerrit.common.FileUtil; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.PeerDaemonUser; | |
| import com.google.gerrit.server.account.AccountSshKey; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.config.SitePaths; | |
| import com.google.inject.Inject; | |
| import java.io.BufferedReader; | |
| import java.io.IOException; | |
| import java.nio.file.Files; | |
| import java.nio.file.NoSuchFileException; | |
| import java.nio.file.Path; | |
| import java.security.GeneralSecurityException; | |
| import java.security.KeyPair; | |
| import java.security.PublicKey; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.HashSet; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.Set; | |
| import org.apache.sshd.common.SshException; | |
| import org.apache.sshd.common.keyprovider.KeyPairProvider; | |
| import org.apache.sshd.common.util.buffer.ByteArrayBuffer; | |
| import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; | |
| import org.apache.sshd.server.session.ServerSession; | |
| import org.eclipse.jgit.lib.Config; | |
| /** Authenticates by public key through {@link AccountSshKey} entities. */ | |
| class DatabasePubKeyAuth implements PublickeyAuthenticator { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private final SshKeyCacheImpl sshKeyCache; | |
| private final SshLog sshLog; | |
| private final IdentifiedUser.GenericFactory userFactory; | |
| private final PeerDaemonUser.Factory peerFactory; | |
| private final Config config; | |
| private final SshScope sshScope; | |
| private final Set<PublicKey> myHostKeys; | |
| private volatile PeerKeyCache peerKeyCache; | |
| @Inject | |
| DatabasePubKeyAuth( | |
| SshKeyCacheImpl skc, | |
| SshLog l, | |
| IdentifiedUser.GenericFactory uf, | |
| PeerDaemonUser.Factory pf, | |
| SitePaths site, | |
| KeyPairProvider hostKeyProvider, | |
| @GerritServerConfig Config cfg, | |
| SshScope s) { | |
| sshKeyCache = skc; | |
| sshLog = l; | |
| userFactory = uf; | |
| peerFactory = pf; | |
| config = cfg; | |
| sshScope = s; | |
| myHostKeys = myHostKeys(hostKeyProvider); | |
| peerKeyCache = new PeerKeyCache(site.peer_keys); | |
| } | |
| private static Set<PublicKey> myHostKeys(KeyPairProvider p) { | |
| Set<PublicKey> keys = new HashSet<>(6); | |
| try { | |
| addPublicKey(keys, p, KeyPairProvider.SSH_ED25519); | |
| addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256); | |
| addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384); | |
| addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521); | |
| addPublicKey(keys, p, KeyPairProvider.SSH_RSA); | |
| addPublicKey(keys, p, KeyPairProvider.SSH_DSS); | |
| } catch (IOException | GeneralSecurityException e) { | |
| throw new IllegalStateException("Cannot load SSHD host key", e); | |
| } | |
| return keys; | |
| } | |
| private static void addPublicKey(Collection<PublicKey> out, KeyPairProvider p, String type) | |
| throws IOException, GeneralSecurityException { | |
| KeyPair pair = p.loadKey(null, type); | |
| if (pair != null && pair.getPublic() != null) { | |
| out.add(pair.getPublic()); | |
| } | |
| } | |
| @Override | |
| public boolean authenticate(String username, PublicKey suppliedKey, ServerSession session) { | |
| SshSession sd = session.getAttribute(SshSession.KEY); | |
| checkState(sd.getUser() == null); | |
| if (PeerDaemonUser.USER_NAME.equals(username)) { | |
| if (myHostKeys.contains(suppliedKey) || getPeerKeys().contains(suppliedKey)) { | |
| PeerDaemonUser user = peerFactory.create(sd.getRemoteAddress()); | |
| return SshUtil.success(username, session, sshScope, sshLog, sd, user); | |
| } | |
| sd.authenticationError(username, "no-matching-key"); | |
| return false; | |
| } | |
| if (config.getBoolean("auth", "userNameToLowerCase", false)) { | |
| username = username.toLowerCase(Locale.US); | |
| } | |
| Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username); | |
| SshKeyCacheEntry key = find(keyList, suppliedKey); | |
| if (key == null) { | |
| String err; | |
| if (keyList == SshKeyCacheImpl.NO_SUCH_USER) { | |
| err = "user-not-found"; | |
| } else if (keyList == SshKeyCacheImpl.NO_KEYS) { | |
| err = "key-list-empty"; | |
| } else { | |
| err = "no-matching-key"; | |
| } | |
| sd.authenticationError(username, err); | |
| return false; | |
| } | |
| // Double check that all of the keys are for the same user account. | |
| // This should have been true when the cache factory method loaded | |
| // the list into memory, but we want to be extra paranoid about our | |
| // security check to ensure there aren't two users sharing the same | |
| // user name on the server. | |
| // | |
| for (SshKeyCacheEntry otherKey : keyList) { | |
| if (!key.getAccount().equals(otherKey.getAccount())) { | |
| sd.authenticationError(username, "keys-cross-accounts"); | |
| return false; | |
| } | |
| } | |
| IdentifiedUser cu = SshUtil.createUser(sd, userFactory, key.getAccount()); | |
| if (!cu.getAccount().isActive()) { | |
| sd.authenticationError(username, "inactive-account"); | |
| return false; | |
| } | |
| return SshUtil.success(username, session, sshScope, sshLog, sd, cu); | |
| } | |
| private Set<PublicKey> getPeerKeys() { | |
| PeerKeyCache p = peerKeyCache; | |
| if (!p.isCurrent()) { | |
| p = p.reload(); | |
| peerKeyCache = p; | |
| } | |
| return p.keys; | |
| } | |
| @Nullable | |
| private SshKeyCacheEntry find(Iterable<SshKeyCacheEntry> keyList, PublicKey suppliedKey) { | |
| for (SshKeyCacheEntry k : keyList) { | |
| if (k.match(suppliedKey)) { | |
| return k; | |
| } | |
| } | |
| return null; | |
| } | |
| private static class PeerKeyCache { | |
| private final Path path; | |
| private final long modified; | |
| final Set<PublicKey> keys; | |
| PeerKeyCache(Path path) { | |
| this.path = path; | |
| this.modified = FileUtil.lastModified(path); | |
| this.keys = read(path); | |
| } | |
| private static Set<PublicKey> read(Path path) { | |
| try (BufferedReader br = Files.newBufferedReader(path, UTF_8)) { | |
| final Set<PublicKey> keys = new HashSet<>(); | |
| String line; | |
| while ((line = br.readLine()) != null) { | |
| line = line.trim(); | |
| if (line.startsWith("#") || line.isEmpty()) { | |
| continue; | |
| } | |
| List<String> parts = Splitter.on(' ').splitToList(line); | |
| if (parts.size() > 2) { | |
| throw new IllegalArgumentException( | |
| "Invalid peer key file format, only <key [comment]> lines supported"); | |
| } | |
| try { | |
| byte[] bin = | |
| BaseEncoding.base64() | |
| .decode(new String(parts.get(0).getBytes(ISO_8859_1), ISO_8859_1)); | |
| keys.add(new ByteArrayBuffer(bin).getRawPublicKey()); | |
| } catch (RuntimeException | SshException e) { | |
| logBadKey(path, line, e); | |
| } | |
| } | |
| return Collections.unmodifiableSet(keys); | |
| } catch (NoSuchFileException noFile) { | |
| return Collections.emptySet(); | |
| } catch (IOException err) { | |
| logger.atSevere().withCause(err).log("Cannot read %s", path); | |
| return Collections.emptySet(); | |
| } | |
| } | |
| private static void logBadKey(Path path, String line, Exception e) { | |
| logger.atWarning().withCause(e).log("Invalid key in %s:\n %s", path, line); | |
| } | |
| boolean isCurrent() { | |
| return modified == FileUtil.lastModified(path); | |
| } | |
| PeerKeyCache reload() { | |
| return new PeerKeyCache(path); | |
| } | |
| } | |
| } |