Line data Source code
1 : // Copyright (C) 2008 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.sshd;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static java.nio.charset.StandardCharsets.ISO_8859_1;
19 : import static java.nio.charset.StandardCharsets.UTF_8;
20 :
21 : import com.google.common.base.Splitter;
22 : import com.google.common.flogger.FluentLogger;
23 : import com.google.common.io.BaseEncoding;
24 : import com.google.gerrit.common.FileUtil;
25 : import com.google.gerrit.common.Nullable;
26 : import com.google.gerrit.server.IdentifiedUser;
27 : import com.google.gerrit.server.PeerDaemonUser;
28 : import com.google.gerrit.server.account.AccountSshKey;
29 : import com.google.gerrit.server.config.GerritServerConfig;
30 : import com.google.gerrit.server.config.SitePaths;
31 : import com.google.inject.Inject;
32 : import java.io.BufferedReader;
33 : import java.io.IOException;
34 : import java.nio.file.Files;
35 : import java.nio.file.NoSuchFileException;
36 : import java.nio.file.Path;
37 : import java.security.GeneralSecurityException;
38 : import java.security.KeyPair;
39 : import java.security.PublicKey;
40 : import java.util.Collection;
41 : import java.util.Collections;
42 : import java.util.HashSet;
43 : import java.util.List;
44 : import java.util.Locale;
45 : import java.util.Set;
46 : import org.apache.sshd.common.SshException;
47 : import org.apache.sshd.common.keyprovider.KeyPairProvider;
48 : import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
49 : import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
50 : import org.apache.sshd.server.session.ServerSession;
51 : import org.eclipse.jgit.lib.Config;
52 :
53 : /** Authenticates by public key through {@link AccountSshKey} entities. */
54 : class DatabasePubKeyAuth implements PublickeyAuthenticator {
55 17 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
56 :
57 : private final SshKeyCacheImpl sshKeyCache;
58 : private final SshLog sshLog;
59 : private final IdentifiedUser.GenericFactory userFactory;
60 : private final PeerDaemonUser.Factory peerFactory;
61 : private final Config config;
62 : private final SshScope sshScope;
63 : private final Set<PublicKey> myHostKeys;
64 : private volatile PeerKeyCache peerKeyCache;
65 :
66 : @Inject
67 : DatabasePubKeyAuth(
68 : SshKeyCacheImpl skc,
69 : SshLog l,
70 : IdentifiedUser.GenericFactory uf,
71 : PeerDaemonUser.Factory pf,
72 : SitePaths site,
73 : KeyPairProvider hostKeyProvider,
74 : @GerritServerConfig Config cfg,
75 17 : SshScope s) {
76 17 : sshKeyCache = skc;
77 17 : sshLog = l;
78 17 : userFactory = uf;
79 17 : peerFactory = pf;
80 17 : config = cfg;
81 17 : sshScope = s;
82 17 : myHostKeys = myHostKeys(hostKeyProvider);
83 17 : peerKeyCache = new PeerKeyCache(site.peer_keys);
84 17 : }
85 :
86 : private static Set<PublicKey> myHostKeys(KeyPairProvider p) {
87 17 : Set<PublicKey> keys = new HashSet<>(6);
88 : try {
89 17 : addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
90 17 : addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
91 17 : addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
92 17 : addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
93 17 : addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
94 17 : addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
95 0 : } catch (IOException | GeneralSecurityException e) {
96 0 : throw new IllegalStateException("Cannot load SSHD host key", e);
97 17 : }
98 :
99 17 : return keys;
100 : }
101 :
102 : private static void addPublicKey(Collection<PublicKey> out, KeyPairProvider p, String type)
103 : throws IOException, GeneralSecurityException {
104 17 : KeyPair pair = p.loadKey(null, type);
105 17 : if (pair != null && pair.getPublic() != null) {
106 17 : out.add(pair.getPublic());
107 : }
108 17 : }
109 :
110 : @Override
111 : public boolean authenticate(String username, PublicKey suppliedKey, ServerSession session) {
112 17 : SshSession sd = session.getAttribute(SshSession.KEY);
113 17 : checkState(sd.getUser() == null);
114 17 : if (PeerDaemonUser.USER_NAME.equals(username)) {
115 1 : if (myHostKeys.contains(suppliedKey) || getPeerKeys().contains(suppliedKey)) {
116 1 : PeerDaemonUser user = peerFactory.create(sd.getRemoteAddress());
117 1 : return SshUtil.success(username, session, sshScope, sshLog, sd, user);
118 : }
119 1 : sd.authenticationError(username, "no-matching-key");
120 1 : return false;
121 : }
122 :
123 16 : if (config.getBoolean("auth", "userNameToLowerCase", false)) {
124 0 : username = username.toLowerCase(Locale.US);
125 : }
126 :
127 16 : Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username);
128 16 : SshKeyCacheEntry key = find(keyList, suppliedKey);
129 16 : if (key == null) {
130 : String err;
131 0 : if (keyList == SshKeyCacheImpl.NO_SUCH_USER) {
132 0 : err = "user-not-found";
133 0 : } else if (keyList == SshKeyCacheImpl.NO_KEYS) {
134 0 : err = "key-list-empty";
135 : } else {
136 0 : err = "no-matching-key";
137 : }
138 0 : sd.authenticationError(username, err);
139 0 : return false;
140 : }
141 :
142 : // Double check that all of the keys are for the same user account.
143 : // This should have been true when the cache factory method loaded
144 : // the list into memory, but we want to be extra paranoid about our
145 : // security check to ensure there aren't two users sharing the same
146 : // user name on the server.
147 : //
148 16 : for (SshKeyCacheEntry otherKey : keyList) {
149 16 : if (!key.getAccount().equals(otherKey.getAccount())) {
150 0 : sd.authenticationError(username, "keys-cross-accounts");
151 0 : return false;
152 : }
153 16 : }
154 :
155 16 : IdentifiedUser cu = SshUtil.createUser(sd, userFactory, key.getAccount());
156 16 : if (!cu.getAccount().isActive()) {
157 0 : sd.authenticationError(username, "inactive-account");
158 0 : return false;
159 : }
160 :
161 16 : return SshUtil.success(username, session, sshScope, sshLog, sd, cu);
162 : }
163 :
164 : private Set<PublicKey> getPeerKeys() {
165 1 : PeerKeyCache p = peerKeyCache;
166 1 : if (!p.isCurrent()) {
167 1 : p = p.reload();
168 1 : peerKeyCache = p;
169 : }
170 1 : return p.keys;
171 : }
172 :
173 : @Nullable
174 : private SshKeyCacheEntry find(Iterable<SshKeyCacheEntry> keyList, PublicKey suppliedKey) {
175 16 : for (SshKeyCacheEntry k : keyList) {
176 16 : if (k.match(suppliedKey)) {
177 16 : return k;
178 : }
179 1 : }
180 0 : return null;
181 : }
182 :
183 : private static class PeerKeyCache {
184 : private final Path path;
185 : private final long modified;
186 : final Set<PublicKey> keys;
187 :
188 17 : PeerKeyCache(Path path) {
189 17 : this.path = path;
190 17 : this.modified = FileUtil.lastModified(path);
191 17 : this.keys = read(path);
192 17 : }
193 :
194 : private static Set<PublicKey> read(Path path) {
195 1 : try (BufferedReader br = Files.newBufferedReader(path, UTF_8)) {
196 1 : final Set<PublicKey> keys = new HashSet<>();
197 : String line;
198 1 : while ((line = br.readLine()) != null) {
199 1 : line = line.trim();
200 1 : if (line.startsWith("#") || line.isEmpty()) {
201 0 : continue;
202 : }
203 :
204 1 : List<String> parts = Splitter.on(' ').splitToList(line);
205 1 : if (parts.size() > 2) {
206 0 : throw new IllegalArgumentException(
207 : "Invalid peer key file format, only <key [comment]> lines supported");
208 : }
209 : try {
210 : byte[] bin =
211 1 : BaseEncoding.base64()
212 1 : .decode(new String(parts.get(0).getBytes(ISO_8859_1), ISO_8859_1));
213 1 : keys.add(new ByteArrayBuffer(bin).getRawPublicKey());
214 0 : } catch (RuntimeException | SshException e) {
215 0 : logBadKey(path, line, e);
216 1 : }
217 1 : }
218 1 : return Collections.unmodifiableSet(keys);
219 17 : } catch (NoSuchFileException noFile) {
220 17 : return Collections.emptySet();
221 0 : } catch (IOException err) {
222 0 : logger.atSevere().withCause(err).log("Cannot read %s", path);
223 0 : return Collections.emptySet();
224 : }
225 : }
226 :
227 : private static void logBadKey(Path path, String line, Exception e) {
228 0 : logger.atWarning().withCause(e).log("Invalid key in %s:\n %s", path, line);
229 0 : }
230 :
231 : boolean isCurrent() {
232 1 : return modified == FileUtil.lastModified(path);
233 : }
234 :
235 : PeerKeyCache reload() {
236 1 : return new PeerKeyCache(path);
237 : }
238 : }
239 : }
|