| // 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.common.Nullable; |
| import com.google.gerrit.extensions.common.GpgKeyInfo.Status; |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.time.Instant; |
| 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("%s", 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; |
| |
| /** |
| * Specifies whether this repository should be closed before returning froms {@link |
| * #check(PushCertificate)} |
| * |
| * @param repo a repository previously returned by {@link #getRepository()}. |
| * @return true if 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(); |
| } |
| |
| @Nullable |
| 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(getCreationTime(sig)).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); |
| } |
| |
| @SuppressWarnings("JdkObsolete") |
| public static Instant getCreationTime(PGPSignature signature) { |
| return signature.getCreationTime().toInstant(); |
| } |
| } |