| // 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.server.project.ProjectCache.illegalState; | |
| import com.google.common.base.Strings; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.entities.BooleanProjectConfig; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.extensions.registration.DynamicSet; | |
| import com.google.gerrit.server.EnableSignedPush; | |
| import com.google.gerrit.server.config.AllUsersName; | |
| import com.google.gerrit.server.config.GerritServerConfig; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.git.ReceivePackInitializer; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.project.ProjectState; | |
| import com.google.inject.AbstractModule; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| import com.google.inject.ProvisionException; | |
| import com.google.inject.Singleton; | |
| import com.google.inject.multibindings.OptionalBinder; | |
| import java.io.IOException; | |
| import java.security.NoSuchAlgorithmException; | |
| import java.security.SecureRandom; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Random; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.transport.PreReceiveHook; | |
| import org.eclipse.jgit.transport.PreReceiveHookChain; | |
| import org.eclipse.jgit.transport.ReceivePack; | |
| import org.eclipse.jgit.transport.SignedPushConfig; | |
| class SignedPushModule extends AbstractModule { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| @Override | |
| protected void configure() { | |
| if (!BouncyCastleUtil.havePGP()) { | |
| throw new ProvisionException("Bouncy Castle PGP not installed"); | |
| } | |
| // This binding is optional as some modules might bind | |
| // {@code UnimplementedPublicKeyStoreProvider} as default binding. | |
| OptionalBinder.newOptionalBinder(binder(), PublicKeyStore.class) | |
| .setBinding() | |
| .toProvider(StoreProvider.class); | |
| DynamicSet.bind(binder(), ReceivePackInitializer.class).to(Initializer.class); | |
| } | |
| @Singleton | |
| private static class Initializer implements ReceivePackInitializer { | |
| private final SignedPushConfig signedPushConfig; | |
| private final SignedPushPreReceiveHook hook; | |
| private final ProjectCache projectCache; | |
| @Inject | |
| Initializer( | |
| @GerritServerConfig Config cfg, | |
| @EnableSignedPush boolean enableSignedPush, | |
| SignedPushPreReceiveHook hook, | |
| ProjectCache projectCache) { | |
| this.hook = hook; | |
| this.projectCache = projectCache; | |
| if (enableSignedPush) { | |
| String seed = cfg.getString("receive", null, "certNonceSeed"); | |
| if (Strings.isNullOrEmpty(seed)) { | |
| seed = randomString(64); | |
| } | |
| signedPushConfig = new SignedPushConfig(); | |
| signedPushConfig.setCertNonceSeed(seed); | |
| signedPushConfig.setCertNonceSlopLimit( | |
| cfg.getInt("receive", null, "certNonceSlop", 5 * 60)); | |
| } else { | |
| signedPushConfig = null; | |
| } | |
| } | |
| @Override | |
| public void init(Project.NameKey project, ReceivePack rp) { | |
| ProjectState ps = projectCache.get(project).orElseThrow(illegalState(project)); | |
| if (!ps.is(BooleanProjectConfig.ENABLE_SIGNED_PUSH)) { | |
| rp.setSignedPushConfig(null); | |
| return; | |
| } else if (signedPushConfig == null) { | |
| logger.atSevere().log( | |
| "receive.enableSignedPush is true for project %s but" | |
| + " false in gerrit.config, so signed push verification is" | |
| + " disabled", | |
| project.get()); | |
| rp.setSignedPushConfig(null); | |
| return; | |
| } | |
| rp.setSignedPushConfig(signedPushConfig); | |
| List<PreReceiveHook> hooks = new ArrayList<>(3); | |
| if (ps.is(BooleanProjectConfig.REQUIRE_SIGNED_PUSH)) { | |
| hooks.add(SignedPushPreReceiveHook.Required.INSTANCE); | |
| } | |
| hooks.add(hook); | |
| hooks.add(rp.getPreReceiveHook()); | |
| rp.setPreReceiveHook(PreReceiveHookChain.newChain(hooks)); | |
| } | |
| } | |
| @Singleton | |
| private static class StoreProvider implements Provider<PublicKeyStore> { | |
| private final GitRepositoryManager repoManager; | |
| private final AllUsersName allUsers; | |
| @Inject | |
| StoreProvider(GitRepositoryManager repoManager, AllUsersName allUsers) { | |
| this.repoManager = repoManager; | |
| this.allUsers = allUsers; | |
| } | |
| @Override | |
| public PublicKeyStore get() { | |
| final Repository repo; | |
| try { | |
| repo = repoManager.openRepository(allUsers); | |
| } catch (IOException e) { | |
| throw new ProvisionException("Cannot open " + allUsers, e); | |
| } | |
| return new PublicKeyStore(repo) { | |
| @Override | |
| public void close() { | |
| try { | |
| super.close(); | |
| } finally { | |
| repo.close(); | |
| } | |
| } | |
| }; | |
| } | |
| } | |
| private static String randomString(int len) { | |
| Random random; | |
| try { | |
| random = SecureRandom.getInstance("SHA1PRNG"); | |
| } catch (NoSuchAlgorithmException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| StringBuilder sb = new StringBuilder(len); | |
| for (int i = 0; i < len; i++) { | |
| sb.append((char) random.nextInt()); | |
| } | |
| return sb.toString(); | |
| } | |
| } |