LCOV - code coverage report
Current view: top level - gpg - GerritPublicKeyChecker.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 91 98 92.9 %
Date: 2022-11-19 15:00:39 Functions: 16 16 100.0 %

          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.gerrit.gpg.PublicKeyStore.keyIdToString;
      18             : import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
      19             : 
      20             : import com.google.common.base.CharMatcher;
      21             : import com.google.common.collect.ImmutableMap;
      22             : import com.google.common.collect.Maps;
      23             : import com.google.common.flogger.FluentLogger;
      24             : import com.google.common.io.BaseEncoding;
      25             : import com.google.gerrit.extensions.registration.DynamicItem;
      26             : import com.google.gerrit.server.IdentifiedUser;
      27             : import com.google.gerrit.server.account.AccountState;
      28             : import com.google.gerrit.server.account.externalids.ExternalId;
      29             : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
      30             : import com.google.gerrit.server.config.GerritServerConfig;
      31             : import com.google.gerrit.server.config.UrlFormatter;
      32             : import com.google.gerrit.server.query.account.InternalAccountQuery;
      33             : import com.google.inject.Inject;
      34             : import com.google.inject.Provider;
      35             : import com.google.inject.Singleton;
      36             : import java.util.Collections;
      37             : import java.util.HashSet;
      38             : import java.util.Iterator;
      39             : import java.util.List;
      40             : import java.util.Map;
      41             : import java.util.Optional;
      42             : import java.util.Set;
      43             : import org.bouncycastle.openpgp.PGPException;
      44             : import org.bouncycastle.openpgp.PGPPublicKey;
      45             : import org.bouncycastle.openpgp.PGPSignature;
      46             : import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
      47             : import org.eclipse.jgit.lib.Config;
      48             : import org.eclipse.jgit.transport.PushCertificateIdent;
      49             : 
      50             : /**
      51             :  * Checker for GPG public keys including Gerrit-specific checks.
      52             :  *
      53             :  * <p>For Gerrit, keys must contain a self-signed user ID certification matching a trusted external
      54             :  * ID in the database, or an email address thereof.
      55             :  */
      56             : public class GerritPublicKeyChecker extends PublicKeyChecker {
      57           3 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      58             : 
      59             :   @Singleton
      60             :   public static class Factory {
      61             :     private final Provider<InternalAccountQuery> accountQueryProvider;
      62             :     private final DynamicItem<UrlFormatter> urlFormatter;
      63             :     private final IdentifiedUser.GenericFactory userFactory;
      64             :     private final int maxTrustDepth;
      65             :     private final ImmutableMap<Long, Fingerprint> trusted;
      66             :     private final ExternalIdKeyFactory externalIdKeyFactory;
      67             : 
      68             :     @Inject
      69             :     Factory(
      70             :         @GerritServerConfig Config cfg,
      71             :         Provider<InternalAccountQuery> accountQueryProvider,
      72             :         IdentifiedUser.GenericFactory userFactory,
      73             :         DynamicItem<UrlFormatter> urlFormatter,
      74           8 :         ExternalIdKeyFactory externalIdKeyFactory) {
      75           8 :       this.accountQueryProvider = accountQueryProvider;
      76           8 :       this.urlFormatter = urlFormatter;
      77           8 :       this.userFactory = userFactory;
      78           8 :       this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0);
      79           8 :       this.externalIdKeyFactory = externalIdKeyFactory;
      80             : 
      81           8 :       String[] strs = cfg.getStringList("receive", null, "trustedKey");
      82           8 :       if (strs.length != 0) {
      83           1 :         Map<Long, Fingerprint> fps = Maps.newHashMapWithExpectedSize(strs.length);
      84           1 :         for (String str : strs) {
      85           1 :           str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
      86           1 :           Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
      87           1 :           fps.put(fp.getId(), fp);
      88             :         }
      89           1 :         trusted = ImmutableMap.copyOf(fps);
      90           1 :       } else {
      91           7 :         trusted = null;
      92             :       }
      93           8 :     }
      94             : 
      95             :     public GerritPublicKeyChecker create() {
      96           3 :       return new GerritPublicKeyChecker(this);
      97             :     }
      98             : 
      99             :     public GerritPublicKeyChecker create(IdentifiedUser expectedUser, PublicKeyStore store) {
     100           3 :       GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
     101           3 :       checker.setExpectedUser(expectedUser);
     102           3 :       checker.setStore(store);
     103           3 :       return checker;
     104             :     }
     105             :   }
     106             : 
     107             :   private final Provider<InternalAccountQuery> accountQueryProvider;
     108             :   private final DynamicItem<UrlFormatter> urlFormatter;
     109             :   private final IdentifiedUser.GenericFactory userFactory;
     110             :   private final ExternalIdKeyFactory externalIdKeyFactory;
     111             : 
     112             :   private IdentifiedUser expectedUser;
     113             : 
     114           3 :   private GerritPublicKeyChecker(Factory factory) {
     115           3 :     this.accountQueryProvider = factory.accountQueryProvider;
     116           3 :     this.urlFormatter = factory.urlFormatter;
     117           3 :     this.userFactory = factory.userFactory;
     118           3 :     if (factory.trusted != null) {
     119           1 :       enableTrust(factory.maxTrustDepth, factory.trusted);
     120             :     }
     121           3 :     this.externalIdKeyFactory = factory.externalIdKeyFactory;
     122           3 :   }
     123             : 
     124             :   /**
     125             :    * Set the expected user for this checker.
     126             :    *
     127             :    * <p>If set, the top-level key passed to {@link #check(PGPPublicKey)} must belong to the given
     128             :    * user. (Other keys checked in the course of verifying the web of trust are checked against the
     129             :    * set of identities in the database belonging to the same user as the key.)
     130             :    */
     131             :   public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) {
     132           3 :     this.expectedUser = expectedUser;
     133           3 :     return this;
     134             :   }
     135             : 
     136             :   @Override
     137             :   public CheckResult checkCustom(PGPPublicKey key, int depth) {
     138             :     try {
     139           3 :       if (depth == 0 && expectedUser != null) {
     140           3 :         return checkIdsForExpectedUser(key);
     141             :       }
     142           1 :       return checkIdsForArbitraryUser(key);
     143           0 :     } catch (PGPException | RuntimeException e) {
     144           0 :       String msg = "Error checking user IDs for key";
     145           0 :       logger.atWarning().withCause(e).log("%s %s", msg, keyIdToString(key.getKeyID()));
     146           0 :       return CheckResult.bad(msg);
     147             :     }
     148             :   }
     149             : 
     150             :   private CheckResult checkIdsForExpectedUser(PGPPublicKey key) throws PGPException {
     151           3 :     Set<String> allowedUserIds = getAllowedUserIds(expectedUser);
     152           3 :     if (allowedUserIds.isEmpty()) {
     153           1 :       Optional<String> settings = urlFormatter.get().getSettingsUrl("Identities");
     154           1 :       return CheckResult.bad(
     155             :           "No identities found for user"
     156           1 :               + (settings.isPresent() ? "; check " + settings.get() : ""));
     157             :     }
     158           3 :     if (hasAllowedUserId(key, allowedUserIds)) {
     159           3 :       return CheckResult.trusted();
     160             :     }
     161           1 :     return CheckResult.bad(missingUserIds(allowedUserIds));
     162             :   }
     163             : 
     164             :   private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException {
     165           1 :     List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
     166           1 :     if (accountStates.isEmpty()) {
     167           1 :       return CheckResult.bad("Key is not associated with any users");
     168             :     }
     169           1 :     if (accountStates.size() > 1) {
     170           0 :       return CheckResult.bad("Key is associated with multiple users");
     171             :     }
     172           1 :     IdentifiedUser user = userFactory.create(accountStates.get(0));
     173             : 
     174           1 :     Set<String> allowedUserIds = getAllowedUserIds(user);
     175           1 :     if (allowedUserIds.isEmpty()) {
     176           1 :       return CheckResult.bad("No identities found for user");
     177             :     }
     178           1 :     if (hasAllowedUserId(key, allowedUserIds)) {
     179           1 :       return CheckResult.trusted();
     180             :     }
     181           1 :     return CheckResult.bad("Key does not contain any valid certifications for user's identities");
     182             :   }
     183             : 
     184             :   private boolean hasAllowedUserId(PGPPublicKey key, Set<String> allowedUserIds)
     185             :       throws PGPException {
     186           3 :     Iterator<String> userIds = key.getUserIDs();
     187           3 :     while (userIds.hasNext()) {
     188           3 :       String userId = userIds.next();
     189           3 :       if (isAllowed(userId, allowedUserIds)) {
     190           3 :         Iterator<PGPSignature> sigs = getSignaturesForId(key, userId);
     191           3 :         while (sigs.hasNext()) {
     192           3 :           if (isValidCertification(key, sigs.next(), userId)) {
     193           3 :             return true;
     194             :           }
     195             :         }
     196             :       }
     197           1 :     }
     198             : 
     199           1 :     return false;
     200             :   }
     201             : 
     202             :   private Iterator<PGPSignature> getSignaturesForId(PGPPublicKey key, String userId) {
     203           3 :     Iterator<PGPSignature> result = key.getSignaturesForID(userId);
     204           3 :     return result != null ? result : Collections.emptyIterator();
     205             :   }
     206             : 
     207             :   private Set<String> getAllowedUserIds(IdentifiedUser user) {
     208           3 :     Set<String> result = new HashSet<>();
     209           3 :     result.addAll(user.getEmailAddresses());
     210           3 :     for (ExternalId extId : user.state().externalIds()) {
     211           3 :       if (extId.isScheme(SCHEME_GPGKEY)) {
     212           2 :         continue; // Omit GPG keys.
     213             :       }
     214           3 :       result.add(extId.key().get());
     215           3 :     }
     216           3 :     return result;
     217             :   }
     218             : 
     219             :   private static boolean isAllowed(String userId, Set<String> allowedUserIds) {
     220           3 :     return allowedUserIds.contains(userId)
     221           3 :         || allowedUserIds.contains(PushCertificateIdent.parse(userId).getEmailAddress());
     222             :   }
     223             : 
     224             :   private static boolean isValidCertification(PGPPublicKey key, PGPSignature sig, String userId)
     225             :       throws PGPException {
     226           3 :     if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION
     227           3 :         && sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) {
     228           0 :       return false;
     229             :     }
     230           3 :     if (sig.getKeyID() != key.getKeyID()) {
     231           0 :       return false;
     232             :     }
     233             :     // TODO(dborowitz): Handle certification revocations:
     234             :     // - Is there a revocation by either this key or another key trusted by the
     235             :     //   server?
     236             :     // - Does such a revocation postdate all other valid certifications?
     237             : 
     238           3 :     sig.init(new BcPGPContentVerifierBuilderProvider(), key);
     239           3 :     return sig.verifyCertification(userId, key);
     240             :   }
     241             : 
     242             :   private static String missingUserIds(Set<String> allowedUserIds) {
     243           1 :     StringBuilder sb =
     244             :         new StringBuilder(
     245             :             "Key must contain a valid certification for one of the following identities:\n");
     246           1 :     Iterator<String> sorted = allowedUserIds.stream().sorted().iterator();
     247           1 :     while (sorted.hasNext()) {
     248           1 :       sb.append("  ").append(sorted.next());
     249           1 :       if (sorted.hasNext()) {
     250           1 :         sb.append('\n');
     251             :       }
     252             :     }
     253           1 :     return sb.toString();
     254             :   }
     255             : 
     256             :   ExternalId.Key toExtIdKey(PGPPublicKey key) {
     257           1 :     return externalIdKeyFactory.create(
     258           1 :         SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
     259             :   }
     260             : }

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