Line data Source code
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 :
15 : package com.google.gerrit.gpg;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static java.nio.charset.StandardCharsets.UTF_8;
19 : import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
20 : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
21 :
22 : import com.google.common.base.Preconditions;
23 : import com.google.common.io.ByteStreams;
24 : import com.google.gerrit.common.Nullable;
25 : import com.google.gerrit.git.LockFailureException;
26 : import com.google.gerrit.git.ObjectIds;
27 : import java.io.ByteArrayInputStream;
28 : import java.io.ByteArrayOutputStream;
29 : import java.io.IOException;
30 : import java.io.InputStream;
31 : import java.util.ArrayList;
32 : import java.util.Arrays;
33 : import java.util.Collections;
34 : import java.util.HashMap;
35 : import java.util.HashSet;
36 : import java.util.Iterator;
37 : import java.util.List;
38 : import java.util.Map;
39 : import java.util.Set;
40 : import org.bouncycastle.bcpg.ArmoredInputStream;
41 : import org.bouncycastle.bcpg.ArmoredOutputStream;
42 : import org.bouncycastle.openpgp.PGPException;
43 : import org.bouncycastle.openpgp.PGPPublicKey;
44 : import org.bouncycastle.openpgp.PGPPublicKeyRing;
45 : import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
46 : import org.bouncycastle.openpgp.PGPSignature;
47 : import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
48 : import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
49 : import org.eclipse.jgit.errors.IncorrectObjectTypeException;
50 : import org.eclipse.jgit.errors.MissingObjectException;
51 : import org.eclipse.jgit.lib.CommitBuilder;
52 : import org.eclipse.jgit.lib.ObjectId;
53 : import org.eclipse.jgit.lib.ObjectInserter;
54 : import org.eclipse.jgit.lib.ObjectReader;
55 : import org.eclipse.jgit.lib.Ref;
56 : import org.eclipse.jgit.lib.RefUpdate;
57 : import org.eclipse.jgit.lib.Repository;
58 : import org.eclipse.jgit.notes.Note;
59 : import org.eclipse.jgit.notes.NoteMap;
60 : import org.eclipse.jgit.revwalk.RevCommit;
61 : import org.eclipse.jgit.revwalk.RevWalk;
62 : import org.eclipse.jgit.util.NB;
63 :
64 : /**
65 : * Store of GPG public keys in git notes.
66 : *
67 : * <p>Keys are stored in filenames based on their hex key ID, padded out to 40 characters to match
68 : * the length of a SHA-1. (This is to easily reuse existing fanout code in {@link NoteMap}, and may
69 : * be changed later after an appropriate transition.)
70 : *
71 : * <p>The contents of each file is an ASCII armored stream containing one or more public key rings
72 : * matching the ID. Multiple keys are supported because forging a key ID is possible, but such a key
73 : * cannot be used to verify signatures produced with the correct key.
74 : *
75 : * <p>Subkeys are mapped to the master GPG key in the same NoteMap.
76 : *
77 : * <p>No additional checks are performed on the key after reading; callers should only trust keys
78 : * after checking with a {@link PublicKeyChecker}.
79 : */
80 : public class PublicKeyStore implements AutoCloseable {
81 : /** Ref where GPG public keys are stored. */
82 : public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
83 :
84 : /**
85 : * Choose the public key that produced a signature.
86 : *
87 : * <p>
88 : *
89 : * @param keyRings candidate keys.
90 : * @param sig signature object.
91 : * @param data signed payload.
92 : * @return the key chosen from {@code keyRings} that was able to verify the signature, or {@code
93 : * null} if none was found.
94 : * @throws PGPException if an error occurred verifying the signature.
95 : */
96 : @Nullable
97 : public static PGPPublicKey getSigner(
98 : Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, byte[] data) throws PGPException {
99 1 : for (PGPPublicKeyRing kr : keyRings) {
100 : // Possibly return a signing subkey in case it differs from the master public key
101 1 : PGPPublicKey k = kr.getPublicKey(sig.getKeyID());
102 1 : if (k == null) {
103 0 : throw new IllegalStateException(
104 0 : "No public key found for ID: " + keyIdToString(sig.getKeyID()));
105 : }
106 1 : sig.init(new BcPGPContentVerifierBuilderProvider(), k);
107 1 : sig.update(data);
108 1 : if (sig.verify()) {
109 : // If the signature was made using a subkey, return the main public key.
110 : // This enables further validity checks, like user ID checks, that can only
111 : // be performed using the master public key.
112 1 : return kr.getPublicKey();
113 : }
114 0 : }
115 0 : return null;
116 : }
117 :
118 : /**
119 : * Choose the public key that produced a certification.
120 : *
121 : * <p>
122 : *
123 : * @param keyRings candidate keys.
124 : * @param sig signature object.
125 : * @param userId user ID being certified.
126 : * @param key key being certified.
127 : * @return the key chosen from {@code keyRings} that was able to verify the certification, or
128 : * {@code null} if none was found.
129 : * @throws PGPException if an error occurred verifying the certification.
130 : */
131 : @Nullable
132 : public static PGPPublicKey getSigner(
133 : Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, String userId, PGPPublicKey key)
134 : throws PGPException {
135 1 : for (PGPPublicKeyRing kr : keyRings) {
136 1 : PGPPublicKey k = kr.getPublicKey();
137 1 : sig.init(new BcPGPContentVerifierBuilderProvider(), k);
138 1 : if (sig.verifyCertification(userId, key)) {
139 1 : return k;
140 : }
141 0 : }
142 0 : return null;
143 : }
144 :
145 : private final Repository repo;
146 : private ObjectReader reader;
147 : private RevCommit tip;
148 : private NoteMap notes;
149 : private Map<Fingerprint, PGPPublicKeyRing> toAdd;
150 : private Set<Fingerprint> toRemove;
151 :
152 : /** @param repo repository to read keys from. */
153 3 : public PublicKeyStore(Repository repo) {
154 3 : this.repo = repo;
155 3 : toAdd = new HashMap<>();
156 3 : toRemove = new HashSet<>();
157 3 : }
158 :
159 : @Override
160 : public void close() {
161 3 : reset();
162 3 : }
163 :
164 : private void reset() {
165 3 : if (reader != null) {
166 3 : reader.close();
167 3 : reader = null;
168 3 : notes = null;
169 : }
170 3 : }
171 :
172 : private void load() throws IOException {
173 3 : reset();
174 3 : reader = repo.newObjectReader();
175 :
176 3 : Ref ref = repo.getRefDatabase().exactRef(REFS_GPG_KEYS);
177 3 : if (ref == null) {
178 3 : return;
179 : }
180 3 : try (RevWalk rw = new RevWalk(reader)) {
181 3 : tip = rw.parseCommit(ref.getObjectId());
182 3 : notes = NoteMap.read(reader, tip);
183 : }
184 3 : }
185 :
186 : /**
187 : * Read public keys with the given key ID.
188 : *
189 : * <p>Keys should not be trusted unless checked with {@link PublicKeyChecker}.
190 : *
191 : * <p>Multiple calls to this method use the same state of the key ref; to reread the ref, call
192 : * {@link #close()} first.
193 : *
194 : * @param keyId key ID.
195 : * @return any keys found that could be successfully parsed.
196 : * @throws PGPException if an error occurred parsing the key data.
197 : * @throws IOException if an error occurred reading the repository data.
198 : */
199 : public PGPPublicKeyRingCollection get(long keyId) throws PGPException, IOException {
200 3 : return new PGPPublicKeyRingCollection(get(keyId, null));
201 : }
202 :
203 : /**
204 : * Read public key with the given fingerprint.
205 : *
206 : * <p>Keys should not be trusted unless checked with {@link PublicKeyChecker}.
207 : *
208 : * <p>Multiple calls to this method use the same state of the key ref; to reread the ref, call
209 : * {@link #close()} first.
210 : *
211 : * @param fingerprint key fingerprint.
212 : * @return the key if found, or {@code null}.
213 : * @throws PGPException if an error occurred parsing the key data.
214 : * @throws IOException if an error occurred reading the repository data.
215 : */
216 : @Nullable
217 : public PGPPublicKeyRing get(byte[] fingerprint) throws PGPException, IOException {
218 3 : List<PGPPublicKeyRing> keyRings = get(Fingerprint.getId(fingerprint), fingerprint);
219 3 : return !keyRings.isEmpty() ? keyRings.get(0) : null;
220 : }
221 :
222 : private List<PGPPublicKeyRing> get(long keyId, byte[] fp) throws IOException {
223 3 : if (reader == null) {
224 3 : load();
225 : }
226 3 : if (notes == null) {
227 0 : return Collections.emptyList();
228 : }
229 :
230 3 : return get(keyObjectId(keyId), fp);
231 : }
232 :
233 : private List<PGPPublicKeyRing> get(ObjectId keyObjectId, byte[] fp) throws IOException {
234 3 : Note note = notes.getNote(keyObjectId);
235 3 : if (note == null) {
236 3 : return Collections.emptyList();
237 : }
238 :
239 3 : return readKeysFromNote(note, fp);
240 : }
241 :
242 : private List<PGPPublicKeyRing> readKeysFromNote(Note note, byte[] fp)
243 : throws IOException, MissingObjectException, IncorrectObjectTypeException {
244 3 : boolean foundAtLeastOneKey = false;
245 3 : List<PGPPublicKeyRing> keys = new ArrayList<>();
246 3 : ObjectId data = note.getData();
247 3 : try (InputStream stream = reader.open(data, OBJ_BLOB).openStream()) {
248 3 : byte[] bytes = ByteStreams.toByteArray(stream);
249 3 : InputStream in = new ByteArrayInputStream(bytes);
250 : while (true) {
251 : @SuppressWarnings("unchecked")
252 3 : Iterator<Object> it = new BcPGPObjectFactory(new ArmoredInputStream(in)).iterator();
253 3 : if (!it.hasNext()) {
254 3 : break;
255 : }
256 3 : foundAtLeastOneKey = true;
257 3 : Object obj = it.next();
258 3 : if (obj instanceof PGPPublicKeyRing) {
259 3 : PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
260 3 : if (fp == null || Arrays.equals(fp, kr.getPublicKey().getFingerprint())) {
261 3 : keys.add(kr);
262 : }
263 : }
264 3 : checkState(!it.hasNext(), "expected one PGP object per ArmoredInputStream");
265 3 : }
266 :
267 3 : if (foundAtLeastOneKey) {
268 3 : return keys;
269 : }
270 :
271 : // Subkey handling
272 0 : String id = new String(bytes, UTF_8);
273 0 : Preconditions.checkArgument(ObjectId.isId(id), "Not valid SHA1: " + id);
274 0 : return get(ObjectId.fromString(id), fp);
275 3 : }
276 : }
277 :
278 : public void rebuildSubkeyMasterKeyMap()
279 : throws MissingObjectException, IncorrectObjectTypeException, IOException, PGPException {
280 0 : if (reader == null) {
281 0 : load();
282 : }
283 0 : if (notes != null) {
284 0 : try (ObjectInserter ins = repo.newObjectInserter()) {
285 0 : for (Note note : notes) {
286 : for (PGPPublicKeyRing keyRing :
287 0 : new PGPPublicKeyRingCollection(readKeysFromNote(note, null))) {
288 0 : long masterKeyId = keyRing.getPublicKey().getKeyID();
289 0 : ObjectId masterKeyObjectId = keyObjectId(masterKeyId);
290 0 : saveSubkeyMapping(ins, keyRing, masterKeyId, masterKeyObjectId);
291 0 : }
292 0 : }
293 : }
294 : }
295 0 : }
296 :
297 : /**
298 : * Add a public key to the store.
299 : *
300 : * <p>Multiple calls may be made to buffer keys in memory, and they are not saved until {@link
301 : * #save(CommitBuilder)} is called.
302 : *
303 : * @param keyRing a key ring containing exactly one public master key.
304 : */
305 : public void add(PGPPublicKeyRing keyRing) {
306 3 : int numMaster = 0;
307 3 : for (PGPPublicKey key : keyRing) {
308 3 : if (key.isMasterKey()) {
309 3 : numMaster++;
310 : }
311 3 : }
312 : // We could have an additional sanity check to ensure all subkeys belong to
313 : // this master key, but that requires doing actual signature verification
314 : // here. The alternative is insane but harmless.
315 3 : if (numMaster != 1) {
316 0 : throw new IllegalArgumentException("Exactly 1 master key is required, found " + numMaster);
317 : }
318 3 : Fingerprint fp = new Fingerprint(keyRing.getPublicKey().getFingerprint());
319 3 : toAdd.put(fp, keyRing);
320 3 : toRemove.remove(fp);
321 3 : }
322 :
323 : /**
324 : * Remove a public key from the store.
325 : *
326 : * <p>Multiple calls may be made to buffer deletes in memory, and they are not saved until {@link
327 : * #save(CommitBuilder)} is called.
328 : *
329 : * @param fingerprint the fingerprint of the key to remove.
330 : */
331 : public void remove(byte[] fingerprint) {
332 3 : Fingerprint fp = new Fingerprint(fingerprint);
333 3 : toAdd.remove(fp);
334 3 : toRemove.add(fp);
335 3 : }
336 :
337 : /**
338 : * Save pending keys to the store.
339 : *
340 : * <p>One commit is created and the ref updated. The pending list is cleared if and only if the
341 : * ref update succeeds, which allows for easy retries in case of lock failure.
342 : *
343 : * @param cb commit builder with at least author and identity populated; tree and parent are
344 : * ignored.
345 : * @return result of the ref update.
346 : */
347 : public RefUpdate.Result save(CommitBuilder cb) throws PGPException, IOException {
348 3 : if (toAdd.isEmpty() && toRemove.isEmpty()) {
349 0 : return RefUpdate.Result.NO_CHANGE;
350 : }
351 3 : if (reader == null) {
352 3 : load();
353 : }
354 3 : if (notes == null) {
355 3 : notes = NoteMap.newEmptyMap();
356 : }
357 : ObjectId newTip;
358 3 : try (ObjectInserter ins = repo.newObjectInserter()) {
359 3 : for (PGPPublicKeyRing keyRing : toAdd.values()) {
360 3 : saveToNotes(ins, keyRing);
361 3 : }
362 3 : for (Fingerprint fp : toRemove) {
363 3 : deleteFromNotes(ins, fp);
364 3 : }
365 3 : cb.setTreeId(notes.writeTree(ins));
366 3 : if (cb.getTreeId().equals(tip != null ? tip.getTree() : EMPTY_TREE_ID)) {
367 1 : return RefUpdate.Result.NO_CHANGE;
368 : }
369 :
370 3 : if (tip != null) {
371 3 : cb.setParentId(tip);
372 : }
373 3 : if (cb.getMessage() == null) {
374 3 : int n = toAdd.size() + toRemove.size();
375 3 : cb.setMessage(String.format("Update %d public key%s", n, n != 1 ? "s" : ""));
376 : }
377 3 : newTip = ins.insert(cb);
378 3 : ins.flush();
379 1 : }
380 :
381 3 : RefUpdate ru = repo.updateRef(PublicKeyStore.REFS_GPG_KEYS);
382 3 : ru.setExpectedOldObjectId(tip);
383 3 : ru.setNewObjectId(newTip);
384 3 : ru.setRefLogIdent(cb.getCommitter());
385 3 : ru.setRefLogMessage("Store public keys", true);
386 3 : RefUpdate.Result result = ru.update();
387 3 : reset();
388 3 : switch (result) {
389 : case FAST_FORWARD:
390 : case NEW:
391 : case NO_CHANGE:
392 3 : toAdd.clear();
393 3 : toRemove.clear();
394 3 : break;
395 : case LOCK_FAILURE:
396 0 : throw new LockFailureException("Failed to store public keys", ru);
397 : case FORCED:
398 : case IO_FAILURE:
399 : case NOT_ATTEMPTED:
400 : case REJECTED:
401 : case REJECTED_CURRENT_BRANCH:
402 : case RENAMED:
403 : case REJECTED_MISSING_OBJECT:
404 : case REJECTED_OTHER_REASON:
405 : default:
406 : break;
407 : }
408 3 : return result;
409 : }
410 :
411 : private void saveToNotes(ObjectInserter ins, PGPPublicKeyRing keyRing)
412 : throws PGPException, IOException {
413 3 : long masterKeyId = keyRing.getPublicKey().getKeyID();
414 3 : PGPPublicKeyRingCollection existing = get(masterKeyId);
415 3 : List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size() + 1);
416 3 : boolean replaced = false;
417 3 : for (PGPPublicKeyRing kr : existing) {
418 2 : if (sameKey(keyRing, kr)) {
419 2 : toWrite.add(keyRing);
420 2 : replaced = true;
421 : } else {
422 1 : toWrite.add(kr);
423 : }
424 2 : }
425 3 : if (!replaced) {
426 3 : toWrite.add(keyRing);
427 : }
428 :
429 3 : ObjectId masterKeyObjectId = keyObjectId(masterKeyId);
430 3 : notes.set(masterKeyObjectId, ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
431 :
432 3 : saveSubkeyMapping(ins, keyRing, masterKeyId, masterKeyObjectId);
433 3 : }
434 :
435 : private void saveSubkeyMapping(
436 : ObjectInserter ins, PGPPublicKeyRing keyRing, long masterKeyId, ObjectId masterKeyObjectId)
437 : throws IOException {
438 : // Subkey handling
439 3 : byte[] masterKeyBytes = masterKeyObjectId.name().getBytes(UTF_8);
440 3 : ObjectId masterKeyObject = null;
441 3 : for (PGPPublicKey key : keyRing) {
442 3 : long subKeyId = key.getKeyID();
443 : // Skip master public key
444 3 : if (masterKeyId == subKeyId) {
445 3 : continue;
446 : }
447 :
448 : // Insert master key object only once for all subkeys
449 3 : if (masterKeyObject == null) {
450 3 : masterKeyObject = ins.insert(OBJ_BLOB, masterKeyBytes);
451 : }
452 :
453 3 : ObjectId subkeyObjectId = keyObjectId(subKeyId);
454 3 : Preconditions.checkArgument(
455 3 : notes.get(subkeyObjectId) == null || notes.get(subkeyObjectId).equals(masterKeyObject),
456 3 : "Master key differs for subkey: " + subkeyObjectId.name());
457 3 : notes.set(subkeyObjectId, masterKeyObject);
458 3 : }
459 3 : }
460 :
461 : private void deleteFromNotes(ObjectInserter ins, Fingerprint fp)
462 : throws PGPException, IOException {
463 3 : long keyId = fp.getId();
464 3 : PGPPublicKeyRingCollection existing = get(keyId);
465 3 : List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size());
466 3 : for (PGPPublicKeyRing kr : existing) {
467 3 : if (!fp.equalsBytes(kr.getPublicKey().getFingerprint())) {
468 0 : toWrite.add(kr);
469 : }
470 3 : }
471 3 : if (toWrite.size() == existing.size()) {
472 1 : return;
473 : }
474 :
475 3 : ObjectId keyObjectId = keyObjectId(keyId);
476 3 : if (!toWrite.isEmpty()) {
477 0 : notes.set(keyObjectId, ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
478 : } else {
479 3 : PGPPublicKeyRing keyRing = get(fp.get());
480 :
481 3 : for (PGPPublicKey key : keyRing) {
482 3 : long subKeyId = key.getKeyID();
483 : // Skip master public key
484 3 : if (keyId == subKeyId) {
485 3 : continue;
486 : }
487 3 : notes.remove(keyObjectId(subKeyId));
488 3 : }
489 :
490 3 : notes.remove(keyObjectId);
491 : }
492 3 : }
493 :
494 : private static boolean sameKey(PGPPublicKeyRing kr1, PGPPublicKeyRing kr2) {
495 2 : return Arrays.equals(kr1.getPublicKey().getFingerprint(), kr2.getPublicKey().getFingerprint());
496 : }
497 :
498 : private static byte[] keysToArmored(List<PGPPublicKeyRing> keys) throws IOException {
499 3 : ByteArrayOutputStream out = new ByteArrayOutputStream(4096 * keys.size());
500 3 : for (PGPPublicKeyRing kr : keys) {
501 3 : try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
502 3 : kr.encode(aout);
503 : }
504 3 : }
505 3 : return out.toByteArray();
506 : }
507 :
508 : public static String keyToString(PGPPublicKey key) {
509 3 : Iterator<String> it = key.getUserIDs();
510 3 : return String.format(
511 : "%s %s(%s)",
512 3 : keyIdToString(key.getKeyID()),
513 3 : it.hasNext() ? it.next() + " " : "",
514 3 : Fingerprint.toString(key.getFingerprint()));
515 : }
516 :
517 : public static String keyIdToString(long keyId) {
518 : // Match key ID format from gpg --list-keys.
519 3 : return String.format("%08X", (int) keyId);
520 : }
521 :
522 : static ObjectId keyObjectId(long keyId) {
523 3 : byte[] buf = new byte[ObjectIds.LEN];
524 3 : NB.encodeInt64(buf, 0, keyId);
525 3 : return ObjectId.fromRaw(buf);
526 : }
527 : }
|