diff options
Diffstat (limited to 'java/com/google/gerrit/gpg/PushCertificateChecker.java')
-rw-r--r-- | java/com/google/gerrit/gpg/PushCertificateChecker.java | 216 |
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); + } +} |