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