LCOV - code coverage report
Current view: top level - gpg - PublicKeyStore.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 179 206 86.9 %
Date: 2022-11-19 15:00:39 Functions: 22 23 95.7 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750