// Copyright (C) 2015 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.gpg; import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.io.BaseEncoding; import com.google.gerrit.common.PageLinks; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.transport.PushCertificateIdent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Checker for GPG public keys including Gerrit-specific checks. * * <p>For Gerrit, keys must contain a self-signed user ID certification matching a trusted external * ID in the database, or an email address thereof. */ public class GerritPublicKeyChecker extends PublicKeyChecker { private static final Logger log = LoggerFactory.getLogger(GerritPublicKeyChecker.class); @Singleton public static class Factory { private final Provider<InternalAccountQuery> accountQueryProvider; private final String webUrl; private final IdentifiedUser.GenericFactory userFactory; private final int maxTrustDepth; private final ImmutableMap<Long, Fingerprint> trusted; @Inject Factory( @GerritServerConfig Config cfg, Provider<InternalAccountQuery> accountQueryProvider, IdentifiedUser.GenericFactory userFactory, @CanonicalWebUrl String webUrl) { this.accountQueryProvider = accountQueryProvider; this.webUrl = webUrl; this.userFactory = userFactory; this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0); String[] strs = cfg.getStringList("receive", null, "trustedKey"); if (strs.length != 0) { Map<Long, Fingerprint> fps = Maps.newHashMapWithExpectedSize(strs.length); for (String str : strs) { str = CharMatcher.whitespace().removeFrom(str).toUpperCase(); Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str)); fps.put(fp.getId(), fp); } trusted = ImmutableMap.copyOf(fps); } else { trusted = null; } } public GerritPublicKeyChecker create() { return new GerritPublicKeyChecker(this); } public GerritPublicKeyChecker create(IdentifiedUser expectedUser, PublicKeyStore store) { GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this); checker.setExpectedUser(expectedUser); checker.setStore(store); return checker; } } private final Provider<InternalAccountQuery> accountQueryProvider; private final String webUrl; private final IdentifiedUser.GenericFactory userFactory; private IdentifiedUser expectedUser; private GerritPublicKeyChecker(Factory factory) { this.accountQueryProvider = factory.accountQueryProvider; this.webUrl = factory.webUrl; this.userFactory = factory.userFactory; if (factory.trusted != null) { enableTrust(factory.maxTrustDepth, factory.trusted); } } /** * Set the expected user for this checker. * * <p>If set, the top-level key passed to {@link #check(PGPPublicKey)} must belong to the given * user. (Other keys checked in the course of verifying the web of trust are checked against the * set of identities in the database belonging to the same user as the key.) */ public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) { this.expectedUser = expectedUser; return this; } @Override public CheckResult checkCustom(PGPPublicKey key, int depth) { try { if (depth == 0 && expectedUser != null) { return checkIdsForExpectedUser(key); } return checkIdsForArbitraryUser(key); } catch (PGPException | OrmException e) { String msg = "Error checking user IDs for key"; log.warn(msg + " " + keyIdToString(key.getKeyID()), e); return CheckResult.bad(msg); } } private CheckResult checkIdsForExpectedUser(PGPPublicKey key) throws PGPException { Set<String> allowedUserIds = getAllowedUserIds(expectedUser); if (allowedUserIds.isEmpty()) { return CheckResult.bad( "No identities found for user; check " + webUrl + "#" + PageLinks.SETTINGS_WEBIDENT); } if (hasAllowedUserId(key, allowedUserIds)) { return CheckResult.trusted(); } return CheckResult.bad(missingUserIds(allowedUserIds)); } private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException { List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key)); if (accountStates.isEmpty()) { return CheckResult.bad("Key is not associated with any users"); } if (accountStates.size() > 1) { return CheckResult.bad("Key is associated with multiple users"); } IdentifiedUser user = userFactory.create(accountStates.get(0)); Set<String> allowedUserIds = getAllowedUserIds(user); if (allowedUserIds.isEmpty()) { return CheckResult.bad("No identities found for user"); } if (hasAllowedUserId(key, allowedUserIds)) { return CheckResult.trusted(); } return CheckResult.bad("Key does not contain any valid certifications for user's identities"); } private boolean hasAllowedUserId(PGPPublicKey key, Set<String> allowedUserIds) throws PGPException { @SuppressWarnings("unchecked") Iterator<String> userIds = key.getUserIDs(); while (userIds.hasNext()) { String userId = userIds.next(); if (isAllowed(userId, allowedUserIds)) { Iterator<PGPSignature> sigs = getSignaturesForId(key, userId); while (sigs.hasNext()) { if (isValidCertification(key, sigs.next(), userId)) { return true; } } } } return false; } private Iterator<PGPSignature> getSignaturesForId(PGPPublicKey key, String userId) { Iterator<PGPSignature> result = key.getSignaturesForID(userId); return result != null ? result : Collections.emptyIterator(); } private Set<String> getAllowedUserIds(IdentifiedUser user) { Set<String> result = new HashSet<>(); result.addAll(user.getEmailAddresses()); for (ExternalId extId : user.state().getExternalIds()) { if (extId.isScheme(SCHEME_GPGKEY)) { continue; // Omit GPG keys. } result.add(extId.key().get()); } return result; } private static boolean isAllowed(String userId, Set<String> allowedUserIds) { return allowedUserIds.contains(userId) || allowedUserIds.contains(PushCertificateIdent.parse(userId).getEmailAddress()); } private static boolean isValidCertification(PGPPublicKey key, PGPSignature sig, String userId) throws PGPException { if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION && sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) { return false; } if (sig.getKeyID() != key.getKeyID()) { return false; } // TODO(dborowitz): Handle certification revocations: // - Is there a revocation by either this key or another key trusted by the // server? // - Does such a revocation postdate all other valid certifications? sig.init(new BcPGPContentVerifierBuilderProvider(), key); return sig.verifyCertification(userId, key); } private static String missingUserIds(Set<String> allowedUserIds) { StringBuilder sb = new StringBuilder( "Key must contain a valid certification for one of the following identities:\n"); Iterator<String> sorted = allowedUserIds.stream().sorted().iterator(); while (sorted.hasNext()) { sb.append(" ").append(sorted.next()); if (sorted.hasNext()) { sb.append('\n'); } } return sb.toString(); } static ExternalId.Key toExtIdKey(PGPPublicKey key) { return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())); } }