summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/gpg/PushCertificateChecker.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/gpg/PushCertificateChecker.java')
-rw-r--r--java/com/google/gerrit/gpg/PushCertificateChecker.java216
1 files changed, 216 insertions, 0 deletions
diff --git a/java/com/google/gerrit/gpg/PushCertificateChecker.java b/java/com/google/gerrit/gpg/PushCertificateChecker.java
new file mode 100644
index 0000000000..82b3892364
--- /dev/null
+++ b/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -0,0 +1,216 @@
+// 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.extensions.common.GpgKeyInfo.Status.BAD;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK;
+import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
+import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
+import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+
+import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+
+/** Checker for push certificates. */
+public abstract class PushCertificateChecker {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public static class Result {
+ private final PGPPublicKey key;
+ private final CheckResult checkResult;
+
+ private Result(PGPPublicKey key, CheckResult checkResult) {
+ this.key = key;
+ this.checkResult = checkResult;
+ }
+
+ public PGPPublicKey getPublicKey() {
+ return key;
+ }
+
+ public CheckResult getCheckResult() {
+ return checkResult;
+ }
+ }
+
+ private final PublicKeyChecker publicKeyChecker;
+
+ private boolean checkNonce;
+
+ protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
+ this.publicKeyChecker = publicKeyChecker;
+ checkNonce = true;
+ }
+
+ /** Set whether to check the status of the nonce; defaults to true. */
+ public PushCertificateChecker setCheckNonce(boolean checkNonce) {
+ this.checkNonce = checkNonce;
+ return this;
+ }
+
+ /**
+ * Check a push certificate.
+ *
+ * @return result of the check.
+ */
+ public final Result check(PushCertificate cert) {
+ if (checkNonce && cert.getNonceStatus() != NonceStatus.OK) {
+ return new Result(null, CheckResult.bad("Invalid nonce"));
+ }
+ List<CheckResult> results = new ArrayList<>(2);
+ Result sigResult = null;
+ try {
+ PGPSignature sig = readSignature(cert);
+ if (sig != null) {
+ @SuppressWarnings("resource")
+ Repository repo = getRepository();
+ try (PublicKeyStore store = new PublicKeyStore(repo)) {
+ sigResult = checkSignature(sig, cert, store);
+ results.add(checkCustom(repo));
+ } finally {
+ if (shouldClose(repo)) {
+ repo.close();
+ }
+ }
+ } else {
+ results.add(CheckResult.bad("Invalid signature format"));
+ }
+ } catch (PGPException | IOException e) {
+ String msg = "Internal error checking push certificate";
+ logger.atSevere().withCause(e).log(msg);
+ results.add(CheckResult.bad(msg));
+ }
+
+ return combine(sigResult, results);
+ }
+
+ private static Result combine(Result sigResult, List<CheckResult> results) {
+ // Combine results:
+ // - If any input result is BAD, the final result is bad.
+ // - If sigResult is TRUSTED and no other result is BAD, the final result
+ // is TRUSTED.
+ // - Otherwise, the result is OK.
+ List<String> problems = new ArrayList<>();
+ boolean bad = false;
+ for (CheckResult result : results) {
+ problems.addAll(result.getProblems());
+ bad |= result.getStatus() == BAD;
+ }
+ Status status = bad ? BAD : OK;
+
+ PGPPublicKey key;
+ if (sigResult != null) {
+ key = sigResult.getPublicKey();
+ CheckResult cr = sigResult.getCheckResult();
+ problems.addAll(cr.getProblems());
+ if (cr.getStatus() == BAD) {
+ status = BAD;
+ } else if (!bad && cr.getStatus() == TRUSTED) {
+ status = TRUSTED;
+ }
+ } else {
+ key = null;
+ }
+ return new Result(key, CheckResult.create(status, problems));
+ }
+
+ /**
+ * Get the repository that this checker should operate on.
+ *
+ * <p>This method is called once per call to {@link #check(PushCertificate)}.
+ *
+ * @return the repository.
+ * @throws IOException if an error occurred reading the repository.
+ */
+ protected abstract Repository getRepository() throws IOException;
+
+ /**
+ * @param repo a repository previously returned by {@link #getRepository()}.
+ * @return whether this repository should be closed before returning from {@link
+ * #check(PushCertificate)}.
+ */
+ protected abstract boolean shouldClose(Repository repo);
+
+ /**
+ * Perform custom checks.
+ *
+ * <p>Default implementation reports no problems, but may be overridden by subclasses.
+ *
+ * @param repo a repository previously returned by {@link #getRepository()}.
+ * @return the result of the custom check.
+ */
+ protected CheckResult checkCustom(Repository repo) {
+ return CheckResult.ok();
+ }
+
+ private PGPSignature readSignature(PushCertificate cert) throws IOException {
+ ArmoredInputStream in =
+ new ArmoredInputStream(new ByteArrayInputStream(Constants.encode(cert.getSignature())));
+ PGPObjectFactory factory = new BcPGPObjectFactory(in);
+ Object obj;
+ while ((obj = factory.nextObject()) != null) {
+ if (obj instanceof PGPSignatureList) {
+ PGPSignatureList sigs = (PGPSignatureList) obj;
+ if (!sigs.isEmpty()) {
+ return sigs.get(0);
+ }
+ }
+ }
+ return null;
+ }
+
+ private Result checkSignature(PGPSignature sig, PushCertificate cert, PublicKeyStore store)
+ throws PGPException, IOException {
+ PGPPublicKeyRingCollection keys = store.get(sig.getKeyID());
+ if (!keys.getKeyRings().hasNext()) {
+ return new Result(
+ null,
+ CheckResult.bad("No public keys found for key ID " + keyIdToString(sig.getKeyID())));
+ }
+ PGPPublicKey signer = PublicKeyStore.getSigner(keys, sig, Constants.encode(cert.toText()));
+ if (signer == null) {
+ return new Result(
+ null, CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID()) + " is not valid"));
+ }
+ CheckResult result =
+ publicKeyChecker.setStore(store).setEffectiveTime(sig.getCreationTime()).check(signer);
+ if (!result.getProblems().isEmpty()) {
+ StringBuilder err =
+ new StringBuilder("Invalid public key ")
+ .append(keyToString(signer))
+ .append(":\n ")
+ .append(Joiner.on("\n ").join(result.getProblems()));
+ return new Result(signer, CheckResult.create(result.getStatus(), err.toString()));
+ }
+ return new Result(signer, result);
+ }
+}