Line data Source code
1 : // Copyright (C) 2015 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.gpg; 16 : 17 : import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD; 18 : import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.OK; 19 : import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED; 20 : import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; 21 : import static com.google.gerrit.gpg.PublicKeyStore.keyToString; 22 : 23 : import com.google.common.base.Joiner; 24 : import com.google.common.flogger.FluentLogger; 25 : import com.google.gerrit.common.Nullable; 26 : import com.google.gerrit.extensions.common.GpgKeyInfo.Status; 27 : import java.io.ByteArrayInputStream; 28 : import java.io.IOException; 29 : import java.time.Instant; 30 : import java.util.ArrayList; 31 : import java.util.List; 32 : import org.bouncycastle.bcpg.ArmoredInputStream; 33 : import org.bouncycastle.openpgp.PGPException; 34 : import org.bouncycastle.openpgp.PGPObjectFactory; 35 : import org.bouncycastle.openpgp.PGPPublicKey; 36 : import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; 37 : import org.bouncycastle.openpgp.PGPSignature; 38 : import org.bouncycastle.openpgp.PGPSignatureList; 39 : import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; 40 : import org.eclipse.jgit.lib.Constants; 41 : import org.eclipse.jgit.lib.Repository; 42 : import org.eclipse.jgit.transport.PushCertificate; 43 : import org.eclipse.jgit.transport.PushCertificate.NonceStatus; 44 : 45 : /** Checker for push certificates. */ 46 : public abstract class PushCertificateChecker { 47 1 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 48 : 49 : public static class Result { 50 : private final PGPPublicKey key; 51 : private final CheckResult checkResult; 52 : 53 1 : private Result(PGPPublicKey key, CheckResult checkResult) { 54 1 : this.key = key; 55 1 : this.checkResult = checkResult; 56 1 : } 57 : 58 : public PGPPublicKey getPublicKey() { 59 1 : return key; 60 : } 61 : 62 : public CheckResult getCheckResult() { 63 1 : return checkResult; 64 : } 65 : } 66 : 67 : private final PublicKeyChecker publicKeyChecker; 68 : 69 : private boolean checkNonce; 70 : 71 1 : protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) { 72 1 : this.publicKeyChecker = publicKeyChecker; 73 1 : checkNonce = true; 74 1 : } 75 : 76 : /** Set whether to check the status of the nonce; defaults to true. */ 77 : public PushCertificateChecker setCheckNonce(boolean checkNonce) { 78 1 : this.checkNonce = checkNonce; 79 1 : return this; 80 : } 81 : 82 : /** 83 : * Check a push certificate. 84 : * 85 : * @return result of the check. 86 : */ 87 : public final Result check(PushCertificate cert) { 88 1 : if (checkNonce && cert.getNonceStatus() != NonceStatus.OK) { 89 1 : return new Result(null, CheckResult.bad("Invalid nonce")); 90 : } 91 1 : List<CheckResult> results = new ArrayList<>(2); 92 1 : Result sigResult = null; 93 : try { 94 1 : PGPSignature sig = readSignature(cert); 95 1 : if (sig != null) { 96 : @SuppressWarnings("resource") 97 1 : Repository repo = getRepository(); 98 1 : try (PublicKeyStore store = new PublicKeyStore(repo)) { 99 1 : sigResult = checkSignature(sig, cert, store); 100 1 : results.add(checkCustom(repo)); 101 : } finally { 102 1 : if (shouldClose(repo)) { 103 0 : repo.close(); 104 : } 105 : } 106 1 : } else { 107 0 : results.add(CheckResult.bad("Invalid signature format")); 108 : } 109 0 : } catch (PGPException | IOException e) { 110 0 : String msg = "Internal error checking push certificate"; 111 0 : logger.atSevere().withCause(e).log("%s", msg); 112 0 : results.add(CheckResult.bad(msg)); 113 1 : } 114 : 115 1 : return combine(sigResult, results); 116 : } 117 : 118 : private static Result combine(Result sigResult, List<CheckResult> results) { 119 : // Combine results: 120 : // - If any input result is BAD, the final result is bad. 121 : // - If sigResult is TRUSTED and no other result is BAD, the final result 122 : // is TRUSTED. 123 : // - Otherwise, the result is OK. 124 1 : List<String> problems = new ArrayList<>(); 125 1 : boolean bad = false; 126 1 : for (CheckResult result : results) { 127 1 : problems.addAll(result.getProblems()); 128 1 : bad |= result.getStatus() == BAD; 129 1 : } 130 1 : Status status = bad ? BAD : OK; 131 : 132 : PGPPublicKey key; 133 1 : if (sigResult != null) { 134 1 : key = sigResult.getPublicKey(); 135 1 : CheckResult cr = sigResult.getCheckResult(); 136 1 : problems.addAll(cr.getProblems()); 137 1 : if (cr.getStatus() == BAD) { 138 1 : status = BAD; 139 1 : } else if (!bad && cr.getStatus() == TRUSTED) { 140 1 : status = TRUSTED; 141 : } 142 1 : } else { 143 0 : key = null; 144 : } 145 1 : return new Result(key, CheckResult.create(status, problems)); 146 : } 147 : 148 : /** 149 : * Get the repository that this checker should operate on. 150 : * 151 : * <p>This method is called once per call to {@link #check(PushCertificate)}. 152 : * 153 : * @return the repository. 154 : * @throws IOException if an error occurred reading the repository. 155 : */ 156 : protected abstract Repository getRepository() throws IOException; 157 : 158 : /** 159 : * Specifies whether this repository should be closed before returning froms {@link 160 : * #check(PushCertificate)} 161 : * 162 : * @param repo a repository previously returned by {@link #getRepository()}. 163 : * @return true if this repository should be closed before returning from {@link 164 : * #check(PushCertificate)}. 165 : */ 166 : protected abstract boolean shouldClose(Repository repo); 167 : 168 : /** 169 : * Perform custom checks. 170 : * 171 : * <p>Default implementation reports no problems, but may be overridden by subclasses. 172 : * 173 : * @param repo a repository previously returned by {@link #getRepository()}. 174 : * @return the result of the custom check. 175 : */ 176 : protected CheckResult checkCustom(Repository repo) { 177 1 : return CheckResult.ok(); 178 : } 179 : 180 : @Nullable 181 : private PGPSignature readSignature(PushCertificate cert) throws IOException { 182 1 : ArmoredInputStream in = 183 1 : new ArmoredInputStream(new ByteArrayInputStream(Constants.encode(cert.getSignature()))); 184 1 : PGPObjectFactory factory = new BcPGPObjectFactory(in); 185 : Object obj; 186 1 : while ((obj = factory.nextObject()) != null) { 187 1 : if (obj instanceof PGPSignatureList) { 188 1 : PGPSignatureList sigs = (PGPSignatureList) obj; 189 1 : if (!sigs.isEmpty()) { 190 1 : return sigs.get(0); 191 : } 192 0 : } 193 : } 194 0 : return null; 195 : } 196 : 197 : private Result checkSignature(PGPSignature sig, PushCertificate cert, PublicKeyStore store) 198 : throws PGPException, IOException { 199 1 : PGPPublicKeyRingCollection keys = store.get(sig.getKeyID()); 200 1 : if (!keys.getKeyRings().hasNext()) { 201 1 : return new Result( 202 : null, 203 1 : CheckResult.bad("No public keys found for key ID " + keyIdToString(sig.getKeyID()))); 204 : } 205 1 : PGPPublicKey signer = PublicKeyStore.getSigner(keys, sig, Constants.encode(cert.toText())); 206 1 : if (signer == null) { 207 0 : return new Result( 208 0 : null, CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID()) + " is not valid")); 209 : } 210 1 : CheckResult result = 211 1 : publicKeyChecker.setStore(store).setEffectiveTime(getCreationTime(sig)).check(signer); 212 1 : if (!result.getProblems().isEmpty()) { 213 1 : StringBuilder err = 214 : new StringBuilder("Invalid public key ") 215 1 : .append(keyToString(signer)) 216 1 : .append(":\n ") 217 1 : .append(Joiner.on("\n ").join(result.getProblems())); 218 1 : return new Result(signer, CheckResult.create(result.getStatus(), err.toString())); 219 : } 220 1 : return new Result(signer, result); 221 : } 222 : 223 : @SuppressWarnings("JdkObsolete") 224 : public static Instant getCreationTime(PGPSignature signature) { 225 1 : return signature.getCreationTime().toInstant(); 226 : } 227 : }