| // 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.flogger.FluentLogger; | |
| import com.google.common.io.BaseEncoding; | |
| import com.google.gerrit.extensions.registration.DynamicItem; | |
| 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.account.externalids.ExternalIdKeyFactory; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.config.UrlFormatter; | |
| import com.google.gerrit.server.query.account.InternalAccountQuery; | |
| 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.Locale; | |
| import java.util.Map; | |
| import java.util.Optional; | |
| 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; | |
| /** | |
| * 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 FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| @Singleton | |
| public static class Factory { | |
| private final Provider<InternalAccountQuery> accountQueryProvider; | |
| private final DynamicItem<UrlFormatter> urlFormatter; | |
| private final IdentifiedUser.GenericFactory userFactory; | |
| private final int maxTrustDepth; | |
| private final ImmutableMap<Long, Fingerprint> trusted; | |
| private final ExternalIdKeyFactory externalIdKeyFactory; | |
| @Inject | |
| Factory( | |
| @GerritServerConfig Config cfg, | |
| Provider<InternalAccountQuery> accountQueryProvider, | |
| IdentifiedUser.GenericFactory userFactory, | |
| DynamicItem<UrlFormatter> urlFormatter, | |
| ExternalIdKeyFactory externalIdKeyFactory) { | |
| this.accountQueryProvider = accountQueryProvider; | |
| this.urlFormatter = urlFormatter; | |
| this.userFactory = userFactory; | |
| this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0); | |
| this.externalIdKeyFactory = externalIdKeyFactory; | |
| 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(Locale.US); | |
| 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 DynamicItem<UrlFormatter> urlFormatter; | |
| private final IdentifiedUser.GenericFactory userFactory; | |
| private final ExternalIdKeyFactory externalIdKeyFactory; | |
| private IdentifiedUser expectedUser; | |
| private GerritPublicKeyChecker(Factory factory) { | |
| this.accountQueryProvider = factory.accountQueryProvider; | |
| this.urlFormatter = factory.urlFormatter; | |
| this.userFactory = factory.userFactory; | |
| if (factory.trusted != null) { | |
| enableTrust(factory.maxTrustDepth, factory.trusted); | |
| } | |
| this.externalIdKeyFactory = factory.externalIdKeyFactory; | |
| } | |
| /** | |
| * 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 | RuntimeException e) { | |
| String msg = "Error checking user IDs for key"; | |
| logger.atWarning().withCause(e).log("%s %s", msg, keyIdToString(key.getKeyID())); | |
| return CheckResult.bad(msg); | |
| } | |
| } | |
| private CheckResult checkIdsForExpectedUser(PGPPublicKey key) throws PGPException { | |
| Set<String> allowedUserIds = getAllowedUserIds(expectedUser); | |
| if (allowedUserIds.isEmpty()) { | |
| Optional<String> settings = urlFormatter.get().getSettingsUrl("Identities"); | |
| return CheckResult.bad( | |
| "No identities found for user" | |
| + (settings.isPresent() ? "; check " + settings.get() : "")); | |
| } | |
| if (hasAllowedUserId(key, allowedUserIds)) { | |
| return CheckResult.trusted(); | |
| } | |
| return CheckResult.bad(missingUserIds(allowedUserIds)); | |
| } | |
| private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException { | |
| 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 { | |
| 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().externalIds()) { | |
| 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(); | |
| } | |
| ExternalId.Key toExtIdKey(PGPPublicKey key) { | |
| return externalIdKeyFactory.create( | |
| SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())); | |
| } | |
| } |