blob: 80b87f71753c55af55c7569c831b0496608f9fe2 [file] [log] [blame]
// 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.gpg.PublicKeyStore.keyToString;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
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;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/** Checker for push certificates. */
public abstract class PushCertificateChecker {
private final PublicKeyChecker publicKeyChecker;
protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
this.publicKeyChecker = publicKeyChecker;
}
/**
* Check a push certificate.
*
* @return result of the check.
* @throws PGPException if an error occurred during GPG checks.
* @throws IOException if an error occurred reading from the repository.
*/
public final CheckResult check(PushCertificate cert) throws PGPException, IOException {
if (cert.getNonceStatus() != NonceStatus.OK) {
return new CheckResult("Invalid nonce");
}
PGPSignature sig = readSignature(cert);
if (sig == null) {
return new CheckResult("Invalid signature format");
}
Repository repo = getRepository();
List<String> problems = new ArrayList<>();
try (PublicKeyStore store = new PublicKeyStore(repo)) {
checkSignature(sig, cert, store.get(sig.getKeyID()), problems);
checkCustom(repo, problems);
return new CheckResult(problems);
} finally {
if (shouldClose(repo)) {
repo.close();
}
}
}
/**
* 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 does nothing, but may be overridden by subclasses.
*
* @param repo a repository previously returned by {@link #getRepository()}.
* @param problems list to which any problems should be added.
*/
protected void checkCustom(Repository repo, List<String> problems) {
// Default implementation does nothing.
}
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 void checkSignature(PGPSignature sig,
PushCertificate cert, PGPPublicKeyRingCollection keys,
List<String> problems) {
List<String> deferredProblems = new ArrayList<>();
boolean anyKeys = false;
for (PGPPublicKeyRing kr : keys) {
PGPPublicKey k = kr.getPublicKey();
anyKeys = true;
try {
sig.init(new BcPGPContentVerifierBuilderProvider(), k);
sig.update(Constants.encode(cert.toText()));
if (!sig.verify()) {
// TODO(dborowitz): Privacy issues with exposing fingerprint/user ID
// of keys having the same ID as the pusher's key?
deferredProblems.add(
"Signature not valid with public key: " + keyToString(k));
continue;
}
CheckResult result = publicKeyChecker.check(k, sig.getKeyID());
if (result.isOk()) {
return;
}
StringBuilder err = new StringBuilder("Invalid public key (")
.append(keyToString(k))
.append("):");
for (int i = 0; i < result.getProblems().size(); i++) {
err.append('\n').append(" ").append(result.getProblems().get(i));
}
problems.add(err.toString());
return;
} catch (PGPException e) {
deferredProblems.add(
"Error checking signature with public key (" + keyToString(k)
+ ": " + e.getMessage());
}
}
if (!anyKeys) {
problems.add(
"No public keys found for Key ID " + keyIdToString(sig.getKeyID()));
} else {
problems.addAll(deferredProblems);
}
}
}