/javatests/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
Java | 439 lines | 333 code | 51 blank | 55 comment | 6 complexity | 693907024233b9a07d96e047bb56442d MD5 | raw file
1// Copyright (C) 2015 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
15package com.google.gerrit.gpg;
16
17import static com.google.common.truth.Truth.assertThat;
18import static com.google.gerrit.gpg.GerritPublicKeyChecker.toExtIdKey;
19import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
20import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithSecondUserId;
21import static com.google.gerrit.gpg.testing.TestTrustKeys.keyA;
22import static com.google.gerrit.gpg.testing.TestTrustKeys.keyB;
23import static com.google.gerrit.gpg.testing.TestTrustKeys.keyC;
24import static com.google.gerrit.gpg.testing.TestTrustKeys.keyD;
25import static com.google.gerrit.gpg.testing.TestTrustKeys.keyE;
26import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
27import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED;
28import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
29
30import com.google.common.collect.ImmutableList;
31import com.google.common.collect.Iterators;
32import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
33import com.google.gerrit.gpg.testing.TestKey;
34import com.google.gerrit.lifecycle.LifecycleManager;
35import com.google.gerrit.reviewdb.client.Account;
36import com.google.gerrit.reviewdb.server.ReviewDb;
37import com.google.gerrit.server.CurrentUser;
38import com.google.gerrit.server.IdentifiedUser;
39import com.google.gerrit.server.ServerInitiated;
40import com.google.gerrit.server.account.AccountManager;
41import com.google.gerrit.server.account.AccountsUpdate;
42import com.google.gerrit.server.account.AuthRequest;
43import com.google.gerrit.server.account.externalids.ExternalId;
44import com.google.gerrit.server.schema.SchemaCreator;
45import com.google.gerrit.server.util.RequestContext;
46import com.google.gerrit.server.util.ThreadLocalRequestContext;
47import com.google.gerrit.testing.InMemoryDatabase;
48import com.google.gerrit.testing.InMemoryModule;
49import com.google.gerrit.testing.NoteDbMode;
50import com.google.inject.Guice;
51import com.google.inject.Inject;
52import com.google.inject.Injector;
53import com.google.inject.Provider;
54import com.google.inject.util.Providers;
55import java.util.ArrayList;
56import java.util.Arrays;
57import java.util.List;
58import org.bouncycastle.openpgp.PGPPublicKey;
59import org.bouncycastle.openpgp.PGPPublicKeyRing;
60import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
61import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
62import org.eclipse.jgit.lib.CommitBuilder;
63import org.eclipse.jgit.lib.Config;
64import org.eclipse.jgit.lib.PersonIdent;
65import org.eclipse.jgit.lib.Repository;
66import org.eclipse.jgit.transport.PushCertificateIdent;
67import org.junit.After;
68import org.junit.Before;
69import org.junit.Test;
70
71/** Unit tests for {@link GerritPublicKeyChecker}. */
72public class GerritPublicKeyCheckerTest {
73 @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider;
74
75 @Inject private AccountManager accountManager;
76
77 @Inject private GerritPublicKeyChecker.Factory checkerFactory;
78
79 @Inject private IdentifiedUser.GenericFactory userFactory;
80
81 @Inject private InMemoryDatabase schemaFactory;
82
83 @Inject private SchemaCreator schemaCreator;
84
85 @Inject private ThreadLocalRequestContext requestContext;
86
87 private LifecycleManager lifecycle;
88 private ReviewDb db;
89 private Account.Id userId;
90 private IdentifiedUser user;
91 private Repository storeRepo;
92 private PublicKeyStore store;
93
94 @Before
95 public void setUpInjector() throws Exception {
96 Config cfg = InMemoryModule.newDefaultConfig();
97 cfg.setInt("receive", null, "maxTrustDepth", 2);
98 cfg.setStringList(
99 "receive",
100 null,
101 "trustedKey",
102 ImmutableList.of(
103 Fingerprint.toString(keyB().getPublicKey().getFingerprint()),
104 Fingerprint.toString(keyD().getPublicKey().getFingerprint())));
105 Injector injector =
106 Guice.createInjector(new InMemoryModule(cfg, NoteDbMode.newNotesMigrationFromEnv()));
107
108 lifecycle = new LifecycleManager();
109 lifecycle.add(injector);
110 injector.injectMembers(this);
111 lifecycle.start();
112
113 db = schemaFactory.open();
114 schemaCreator.create(db);
115 userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
116 // Note: does not match any key in TestKeys.
117 accountsUpdateProvider
118 .get()
119 .update("Set Preferred Email", userId, u -> u.setPreferredEmail("user@example.com"));
120 user = reloadUser();
121
122 requestContext.setContext(
123 new RequestContext() {
124 @Override
125 public CurrentUser getUser() {
126 return user;
127 }
128
129 @Override
130 public Provider<ReviewDb> getReviewDbProvider() {
131 return Providers.of(db);
132 }
133 });
134
135 storeRepo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
136 store = new PublicKeyStore(storeRepo);
137 }
138
139 @After
140 public void tearDown() throws Exception {
141 store.close();
142 storeRepo.close();
143 }
144
145 private IdentifiedUser addUser(String name) throws Exception {
146 AuthRequest req = AuthRequest.forUser(name);
147 Account.Id id = accountManager.authenticate(req).getAccountId();
148 return userFactory.create(id);
149 }
150
151 private IdentifiedUser reloadUser() {
152 user = userFactory.create(userId);
153 return user;
154 }
155
156 @After
157 public void tearDownInjector() {
158 if (lifecycle != null) {
159 lifecycle.stop();
160 }
161 if (db != null) {
162 db.close();
163 }
164 InMemoryDatabase.drop(schemaFactory);
165 }
166
167 @Test
168 public void defaultGpgCertificationMatchesEmail() throws Exception {
169 TestKey key = validKeyWithSecondUserId();
170 PublicKeyChecker checker = checkerFactory.create(user, store).disableTrust();
171 assertProblems(
172 checker.check(key.getPublicKey()),
173 Status.BAD,
174 "Key must contain a valid certification for one of the following "
175 + "identities:\n"
176 + " gerrit:user\n"
177 + " username:user");
178
179 addExternalId("test", "test", "test5@example.com");
180 checker = checkerFactory.create(user, store).disableTrust();
181 assertNoProblems(checker.check(key.getPublicKey()));
182 }
183
184 @Test
185 public void defaultGpgCertificationDoesNotMatchEmail() throws Exception {
186 addExternalId("test", "test", "nobody@example.com");
187 PublicKeyChecker checker = checkerFactory.create(user, store).disableTrust();
188 assertProblems(
189 checker.check(validKeyWithSecondUserId().getPublicKey()),
190 Status.BAD,
191 "Key must contain a valid certification for one of the following "
192 + "identities:\n"
193 + " gerrit:user\n"
194 + " nobody@example.com\n"
195 + " test:test\n"
196 + " username:user");
197 }
198
199 @Test
200 public void manualCertificationMatchesExternalId() throws Exception {
201 addExternalId("foo", "myId", null);
202 PublicKeyChecker checker = checkerFactory.create(user, store).disableTrust();
203 assertNoProblems(checker.check(validKeyWithSecondUserId().getPublicKey()));
204 }
205
206 @Test
207 public void manualCertificationDoesNotMatchExternalId() throws Exception {
208 addExternalId("foo", "otherId", null);
209 PublicKeyChecker checker = checkerFactory.create(user, store).disableTrust();
210 assertProblems(
211 checker.check(validKeyWithSecondUserId().getPublicKey()),
212 Status.BAD,
213 "Key must contain a valid certification for one of the following "
214 + "identities:\n"
215 + " foo:otherId\n"
216 + " gerrit:user\n"
217 + " username:user");
218 }
219
220 @Test
221 public void noExternalIds() throws Exception {
222 accountsUpdateProvider
223 .get()
224 .update(
225 "Delete External IDs",
226 user.getAccountId(),
227 (a, u) -> u.deleteExternalIds(a.getExternalIds()));
228 reloadUser();
229
230 TestKey key = validKeyWithSecondUserId();
231 PublicKeyChecker checker = checkerFactory.create(user, store).disableTrust();
232 assertProblems(
233 checker.check(key.getPublicKey()),
234 Status.BAD,
235 "No identities found for user; check http://test/#/settings/web-identities");
236
237 checker = checkerFactory.create().setStore(store).disableTrust();
238 assertProblems(
239 checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
240 insertExtId(ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
241 assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user");
242 }
243
244 @Test
245 public void checkValidTrustChainAndCorrectExternalIds() throws Exception {
246 // A---Bx
247 // \
248 // \---C---D
249 // \
250 // \---Ex
251 //
252 // The server ultimately trusts B and D.
253 // D and E trust C to be a valid introducer of depth 2.
254 IdentifiedUser userB = addUser("userB");
255 TestKey keyA = add(keyA(), user);
256 TestKey keyB = add(keyB(), userB);
257 add(keyC(), addUser("userC"));
258 add(keyD(), addUser("userD"));
259 add(keyE(), addUser("userE"));
260
261 // Checker for A, checking A.
262 PublicKeyChecker checkerA = checkerFactory.create(user, store);
263 assertNoProblems(checkerA.check(keyA.getPublicKey()));
264
265 // Checker for B, checking B. Trust chain and IDs are correct, so the only
266 // problem is with the key itself.
267 PublicKeyChecker checkerB = checkerFactory.create(userB, store);
268 assertProblems(checkerB.check(keyB.getPublicKey()), Status.BAD, "Key is expired");
269 }
270
271 @Test
272 public void checkWithValidKeyButWrongExpectedUserInChecker() throws Exception {
273 // A---Bx
274 // \
275 // \---C---D
276 // \
277 // \---Ex
278 //
279 // The server ultimately trusts B and D.
280 // D and E trust C to be a valid introducer of depth 2.
281 IdentifiedUser userB = addUser("userB");
282 TestKey keyA = add(keyA(), user);
283 TestKey keyB = add(keyB(), userB);
284 add(keyC(), addUser("userC"));
285 add(keyD(), addUser("userD"));
286 add(keyE(), addUser("userE"));
287
288 // Checker for A, checking B.
289 PublicKeyChecker checkerA = checkerFactory.create(user, store);
290 assertProblems(
291 checkerA.check(keyB.getPublicKey()),
292 Status.BAD,
293 "Key is expired",
294 "Key must contain a valid certification for one of the following"
295 + " identities:\n"
296 + " gerrit:user\n"
297 + " mailto:testa@example.com\n"
298 + " testa@example.com\n"
299 + " username:user");
300
301 // Checker for B, checking A.
302 PublicKeyChecker checkerB = checkerFactory.create(userB, store);
303 assertProblems(
304 checkerB.check(keyA.getPublicKey()),
305 Status.BAD,
306 "Key must contain a valid certification for one of the following"
307 + " identities:\n"
308 + " gerrit:userB\n"
309 + " mailto:testb@example.com\n"
310 + " testb@example.com\n"
311 + " username:userB");
312 }
313
314 @Test
315 public void checkTrustChainWithExpiredKey() throws Exception {
316 // A---Bx
317 //
318 // The server ultimately trusts B.
319 TestKey keyA = add(keyA(), user);
320 TestKey keyB = add(keyB(), addUser("userB"));
321
322 PublicKeyChecker checker = checkerFactory.create(user, store);
323 assertProblems(
324 checker.check(keyA.getPublicKey()),
325 Status.OK,
326 "No path to a trusted key",
327 "Certification by "
328 + keyToString(keyB.getPublicKey())
329 + " is valid, but key is not trusted",
330 "Key D24FE467 used for certification is not in store");
331 }
332
333 @Test
334 public void checkTrustChainUsingCheckerWithoutExpectedKey() throws Exception {
335 // A---Bx
336 // \
337 // \---C---D
338 // \
339 // \---Ex
340 //
341 // The server ultimately trusts B and D.
342 // D and E trust C to be a valid introducer of depth 2.
343 TestKey keyA = add(keyA(), user);
344 TestKey keyB = add(keyB(), addUser("userB"));
345 TestKey keyC = add(keyC(), addUser("userC"));
346 TestKey keyD = add(keyD(), addUser("userD"));
347 TestKey keyE = add(keyE(), addUser("userE"));
348
349 // This checker can check any key, so the only problems come from issues
350 // with the keys themselves, not having invalid user IDs.
351 PublicKeyChecker checker = checkerFactory.create().setStore(store);
352 assertNoProblems(checker.check(keyA.getPublicKey()));
353 assertProblems(checker.check(keyB.getPublicKey()), Status.BAD, "Key is expired");
354 assertNoProblems(checker.check(keyC.getPublicKey()));
355 assertNoProblems(checker.check(keyD.getPublicKey()));
356 assertProblems(
357 checker.check(keyE.getPublicKey()),
358 Status.BAD,
359 "Key is expired",
360 "No path to a trusted key");
361 }
362
363 @Test
364 public void keyLaterInTrustChainMissingUserId() throws Exception {
365 // A---Bx
366 // \
367 // \---C
368 //
369 // The server ultimately trusts B.
370 // C signed A's key but is not in the store.
371 TestKey keyA = add(keyA(), user);
372
373 PGPPublicKeyRing keyRingB = keyB().getPublicKeyRing();
374 PGPPublicKey keyB = keyRingB.getPublicKey();
375 keyB = PGPPublicKey.removeCertification(keyB, keyB.getUserIDs().next());
376 keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB);
377 add(keyRingB, addUser("userB"));
378
379 PublicKeyChecker checkerA = checkerFactory.create(user, store);
380 assertProblems(
381 checkerA.check(keyA.getPublicKey()),
382 Status.OK,
383 "No path to a trusted key",
384 "Certification by " + keyToString(keyB) + " is valid, but key is not trusted",
385 "Key D24FE467 used for certification is not in store");
386 }
387
388 private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
389 Account.Id id = user.getAccountId();
390 List<ExternalId> newExtIds = new ArrayList<>(2);
391 newExtIds.add(ExternalId.create(toExtIdKey(kr.getPublicKey()), id));
392
393 String userId = Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null);
394 if (userId != null) {
395 String email = PushCertificateIdent.parse(userId).getEmailAddress();
396 assertThat(email).contains("@");
397 newExtIds.add(ExternalId.createEmail(id, email));
398 }
399
400 store.add(kr);
401 PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
402 CommitBuilder cb = new CommitBuilder();
403 cb.setAuthor(ident);
404 cb.setCommitter(ident);
405 assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
406
407 accountsUpdateProvider.get().update("Add External IDs", id, u -> u.addExternalIds(newExtIds));
408 }
409
410 private TestKey add(TestKey k, IdentifiedUser user) throws Exception {
411 add(k.getPublicKeyRing(), user);
412 return k;
413 }
414
415 private void assertProblems(
416 CheckResult result, Status expectedStatus, String first, String... rest) throws Exception {
417 List<String> expectedProblems = new ArrayList<>();
418 expectedProblems.add(first);
419 expectedProblems.addAll(Arrays.asList(rest));
420 assertThat(result.getStatus()).isEqualTo(expectedStatus);
421 assertThat(result.getProblems()).containsExactlyElementsIn(expectedProblems).inOrder();
422 }
423
424 private void assertNoProblems(CheckResult result) {
425 assertThat(result.getStatus()).isEqualTo(Status.TRUSTED);
426 assertThat(result.getProblems()).isEmpty();
427 }
428
429 private void addExternalId(String scheme, String id, String email) throws Exception {
430 insertExtId(ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
431 }
432
433 private void insertExtId(ExternalId extId) throws Exception {
434 accountsUpdateProvider
435 .get()
436 .update("Add External ID", extId.accountId(), u -> u.addExternalId(extId));
437 reloadUser();
438 }
439}