PublicKeyChecker: Fix revocation check (mostly)
Bouncy Castle's isRevoked() method is so dumb as to be harmful: it
only checks whether there is a revocation packet in the key, without
doing any sort of validity checks on the packet.
Improve the check by verifying the signature against the key that
provided it, and for non-self-signatures, verifying that the signer is
an authorized revocation key.
This requires always passing in a store to any PublicKeyChecker.
However, we still have the same initialization order problem as
before, where a store is not always available at PublicKeyChecker
creation, in particular in the PushCertificateChecker codepath.
Argument precondition checks are the best we can do. Also add a
convenience method to GerritPublicKeyChecker.Factory for the very
common case (particularly in tests) where we do have both an expected
user and a store.
Change-Id: Id15714f0395a200fcb33fb199c57355b860187a3
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
index 54c12c6..fa78f01 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
@@ -36,6 +36,10 @@
NB.decodeUInt16(fp, 16), NB.decodeUInt16(fp, 18));
}
+ public static long getId(byte[] fp) {
+ return NB.decodeInt64(fp, 12);
+ }
+
public static Map<Long, Fingerprint> byId(Iterable<Fingerprint> fps) {
Map<Long, Fingerprint> result = new HashMap<>();
for (Fingerprint fp : fps) {
@@ -65,6 +69,23 @@
this.fp = checkLength(fp);
}
+ /**
+ * Wrap a portion of a fingerprint byte array.
+ * <p>
+ * Unlike {@link #Fingerprint(byte[])}, creates a new copy of the byte array.
+ *
+ * @param buf byte array to wrap; must have at least {@code off + 20} bytes.
+ * @param off offset in buf.
+ */
+ public Fingerprint(byte[] buf, int off) {
+ int expected = 20 + off;
+ checkArgument(buf.length >= expected,
+ "fingerprint buffer must have at least %s bytes, got %s",
+ expected, buf.length);
+ this.fp = new byte[20];
+ System.arraycopy(buf, off, fp, 0, 20);
+ }
+
public byte[] get() {
return fp;
}
@@ -90,6 +111,6 @@
}
public long getId() {
- return NB.decodeInt64(fp, 12);
+ return getId(fp);
}
}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 88ebc8d..c3c886f 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -96,6 +96,14 @@
public GerritPublicKeyChecker create() {
return new GerritPublicKeyChecker(this);
}
+
+ public GerritPublicKeyChecker create(IdentifiedUser expectedUser,
+ PublicKeyStore store) {
+ GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
+ checker.setExpectedUser(expectedUser);
+ checker.setStore(store);
+ return checker;
+ }
}
private final Provider<ReviewDb> db;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
index d19264a..3bbad9b 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -19,19 +19,34 @@
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 static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_COMPROMISED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_RETIRED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_SUPERSEDED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.NO_REASON;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import org.bouncycastle.bcpg.SignatureSubpacket;
import org.bouncycastle.bcpg.SignatureSubpacketTags;
+import org.bouncycastle.bcpg.sig.RevocationKey;
+import org.bouncycastle.bcpg.sig.RevocationReason;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -40,6 +55,9 @@
/** Checker for GPG public keys for use in a push certificate. */
public class PublicKeyChecker {
+ private static final Logger log =
+ LoggerFactory.getLogger(PublicKeyChecker.class);
+
// https://tools.ietf.org/html/rfc4880#section-5.2.3.13
private static final int COMPLETE_TRUST = 120;
@@ -78,16 +96,11 @@
/** Disable web-of-trust checks. */
public PublicKeyChecker disableTrust() {
- store = null;
trusted = null;
return this;
}
- /**
- * Set the public key store for web-of-trust checks.
- * <p>
- * If set, {@link #enableTrust(int, Map)} must also be called.
- */
+ /** Set the public key store for reading keys referenced in signatures. */
public PublicKeyChecker setStore(PublicKeyStore store) {
if (store == null) {
throw new IllegalArgumentException("PublicKeyStore is required");
@@ -103,7 +116,7 @@
* @return the result of the check.
*/
public final CheckResult check(PGPPublicKey key) {
- if (store == null && trusted != null) {
+ if (store == null) {
throw new IllegalStateException("PublicKeyStore is required");
}
return check(key, 0, true,
@@ -165,11 +178,7 @@
private CheckResult checkBasic(PGPPublicKey key) {
List<String> problems = new ArrayList<>(2);
- if (key.isRevoked()) {
- // TODO(dborowitz): isRevoked is overeager:
- // http://www.bouncycastle.org/jira/browse/BJB-45
- problems.add("Key is revoked");
- }
+ gatherRevocationProblems(key, problems);
long validSecs = key.getValidSeconds();
if (validSecs != 0) {
@@ -182,6 +191,143 @@
return CheckResult.create(problems);
}
+ private void gatherRevocationProblems(PGPPublicKey key, List<String> problems) {
+ try {
+ List<PGPSignature> revocations = new ArrayList<>();
+ Map<Long, RevocationKey> revokers = new HashMap<>();
+ PGPSignature selfRevocation = scanRevocations(key, revocations, revokers);
+ if (selfRevocation != null) {
+ problems.add(reasonToString(getRevocationReason(selfRevocation)));
+ } else {
+ checkRevocations(key, revocations, revokers, problems);
+ }
+ } catch (PGPException | IOException e) {
+ problems.add("Error checking key revocation");
+ }
+ }
+
+ private PGPSignature scanRevocations(PGPPublicKey key,
+ List<PGPSignature> revocations, Map<Long, RevocationKey> revokers)
+ throws PGPException {
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> allSigs = key.getSignatures();
+ while (allSigs.hasNext()) {
+ PGPSignature sig = allSigs.next();
+ switch (sig.getSignatureType()) {
+ case KEY_REVOCATION:
+ if (sig.getKeyID() == key.getKeyID()) {
+ sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+ if (sig.verifyCertification(key)) {
+ return sig;
+ }
+ } else {
+ revocations.add(sig);
+ }
+ break;
+ case DIRECT_KEY:
+ RevocationKey r = getRevocationKey(key, sig);
+ if (r != null) {
+ revokers.put(Fingerprint.getId(r.getFingerprint()), r);
+ }
+ break;
+ }
+ }
+ return null;
+ }
+
+ private RevocationKey getRevocationKey(PGPPublicKey key, PGPSignature sig)
+ throws PGPException {
+ if (sig.getKeyID() != key.getKeyID()) {
+ return null;
+ }
+ SignatureSubpacket sub =
+ sig.getHashedSubPackets().getSubpacket(REVOCATION_KEY);
+ if (sub == null) {
+ return null;
+ }
+ sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+ if (!sig.verifyCertification(key)) {
+ return null;
+ }
+
+ return new RevocationKey(sub.isCritical(), sub.getData());
+ }
+
+ private void checkRevocations(PGPPublicKey key,
+ List<PGPSignature> revocations, Map<Long, RevocationKey> revokers,
+ List<String> problems)
+ throws PGPException, IOException {
+ for (PGPSignature revocation : revocations) {
+ RevocationKey revoker = revokers.get(revocation.getKeyID());
+ if (revoker == null) {
+ continue; // Not a designated revoker.
+ }
+ byte[] rfp = revoker.getFingerprint();
+ PGPPublicKeyRing rkr = store.get(rfp);
+ if (rkr == null
+ || rkr.getPublicKey().getAlgorithm() != revoker.getAlgorithm()) {
+ // Revoker is authorized and there is a revocation signature by this
+ // revoker, but the key is not in the store so we can't verify the
+ // signature.
+ log.info("Key " + Fingerprint.toString(key.getFingerprint())
+ + " is revoked by " + Fingerprint.toString(rfp)
+ + ", which is not in the store. Assuming revocation is valid.");
+ problems.add(reasonToString(getRevocationReason(revocation)));
+ continue;
+ }
+ revocation.init(
+ new BcPGPContentVerifierBuilderProvider(), rkr.getPublicKey());
+ if (revocation.verifyCertification(key)) {
+ problems.add(reasonToString(getRevocationReason(revocation)));
+ }
+ }
+ }
+
+ private static RevocationReason getRevocationReason(PGPSignature sig) {
+ if (sig.getSignatureType() != KEY_REVOCATION) {
+ throw new IllegalArgumentException(
+ "Expected KEY_REVOCATION signature, got " + sig.getSignatureType());
+ }
+ SignatureSubpacket sub =
+ sig.getHashedSubPackets().getSubpacket(REVOCATION_REASON);
+ if (sub == null) {
+ return null;
+ }
+ return new RevocationReason(sub.isCritical(), sub.getData());
+ }
+
+ private static String reasonToString(RevocationReason reason) {
+ StringBuilder r = new StringBuilder("Key is revoked (");
+ if (reason == null) {
+ return r.append("no reason provided)").toString();
+ }
+ switch (reason.getRevocationReason()) {
+ case NO_REASON:
+ r.append("no reason code specified");
+ break;
+ case KEY_SUPERSEDED:
+ r.append("superseded");
+ break;
+ case KEY_COMPROMISED:
+ r.append("key material has been compromised");
+ break;
+ case KEY_RETIRED:
+ r.append("retired and no longer valid");
+ break;
+ default:
+ r.append("reason code ")
+ .append(Integer.toString(reason.getRevocationReason()))
+ .append(')');
+ break;
+ }
+ r.append(')');
+ String desc = reason.getRevocationDescription();
+ if (!desc.isEmpty()) {
+ r.append(": ").append(desc);
+ }
+ return r.toString();
+ }
+
private CheckResult checkWebOfTrust(PGPPublicKey key, PublicKeyStore store,
int depth, Set<Fingerprint> seen) {
if (trusted == null) {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
index b2798f5..8f614d7 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -178,15 +178,39 @@
*/
public PGPPublicKeyRingCollection get(long keyId)
throws PGPException, IOException {
+ return new PGPPublicKeyRingCollection(get(keyId, null));
+ }
+
+ /**
+ * Read public key with the given fingerprint.
+ * <p>
+ * Keys should not be trusted unless checked with {@link PublicKeyChecker}.
+ * <p>
+ * Multiple calls to this method use the same state of the key ref; to reread
+ * the ref, call {@link #close()} first.
+ *
+ * @param fingerprint key fingerprint.
+ * @return the key if found, or {@code null}.
+ * @throws PGPException if an error occurred parsing the key data.
+ * @throws IOException if an error occurred reading the repository data.
+ */
+ public PGPPublicKeyRing get(byte[] fingerprint)
+ throws PGPException, IOException {
+ List<PGPPublicKeyRing> keyRings =
+ get(Fingerprint.getId(fingerprint), fingerprint);
+ return !keyRings.isEmpty() ? keyRings.get(0) : null;
+ }
+
+ private List<PGPPublicKeyRing> get(long keyId, byte[] fp) throws IOException {
if (reader == null) {
load();
}
if (notes == null) {
- return empty();
+ return Collections.emptyList();
}
Note note = notes.getNote(keyObjectId(keyId));
if (note == null) {
- return empty();
+ return Collections.emptyList();
}
List<PGPPublicKeyRing> keys = new ArrayList<>();
@@ -200,12 +224,16 @@
}
Object obj = it.next();
if (obj instanceof PGPPublicKeyRing) {
- keys.add((PGPPublicKeyRing) obj);
+ PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
+ if (fp == null
+ || Arrays.equals(fp, kr.getPublicKey().getFingerprint())) {
+ keys.add(kr);
+ }
}
checkState(!it.hasNext(),
"expected one PGP object per ArmoredInputStream");
}
- return new PGPPublicKeyRingCollection(keys);
+ return keys;
}
}
@@ -375,12 +403,6 @@
return out.toByteArray();
}
- private static PGPPublicKeyRingCollection empty()
- throws PGPException, IOException {
- return new PGPPublicKeyRingCollection(
- Collections.<PGPPublicKeyRing> emptyList());
- }
-
public static String keyToString(PGPPublicKey key) {
@SuppressWarnings("unchecked")
Iterator<String> it = key.getUserIDs();
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 87d778a..e87a0ee 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -208,7 +208,9 @@
CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID())
+ " is not valid"));
}
- CheckResult result = publicKeyChecker.check(signer);
+ CheckResult result = publicKeyChecker
+ .setStore(store)
+ .check(signer);
if (!result.getProblems().isEmpty()) {
StringBuilder err = new StringBuilder("Invalid public key ")
.append(keyToString(signer))
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
index 63d4fe8..a136007 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -163,7 +163,7 @@
found = true;
GpgKeyInfo info = toJson(
keyRing.getPublicKey(),
- checkerFactory.create().setExpectedUser(rsrc.getUser()),
+ checkerFactory.create(rsrc.getUser(), store),
store);
keys.put(info.id, info);
info.id = null;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 0ed1f7b..91c4494 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -194,8 +194,7 @@
for (PGPPublicKeyRing keyRing : keyRings) {
PGPPublicKey key = keyRing.getPublicKey();
// Don't check web of trust; admins can fill in certifications later.
- CheckResult result = checkerFactory.create()
- .setExpectedUser(rsrc.getUser())
+ CheckResult result = checkerFactory.create(rsrc.getUser(), store)
.disableTrust()
.check(key);
if (!result.isOk()) {
@@ -249,9 +248,7 @@
throws IOException {
// Unlike when storing keys, include web-of-trust checks when producing
// result JSON, so the user at least knows of any issues.
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
- .setStore(store);
+ PublicKeyChecker checker = checkerFactory.create(user, store);
Map<String, GpgKeyInfo> infos =
Maps.newHashMapWithExpectedSize(keys.size() + deleted.size());
for (PGPPublicKeyRing keyRing : keys) {
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index edc0559..db0bb88 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -172,8 +172,7 @@
@Test
public void defaultGpgCertificationMatchesEmail() throws Exception {
TestKey key = validKeyWithSecondUserId();
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
+ PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD,
@@ -183,8 +182,7 @@
+ " username:user");
addExternalId("test", "test", "test5@example.com");
- checker = checkerFactory.create()
- .setExpectedUser(user)
+ checker = checkerFactory.create(user, store)
.disableTrust();
assertNoProblems(checker.check(key.getPublicKey()));
}
@@ -192,8 +190,7 @@
@Test
public void defaultGpgCertificationDoesNotMatchEmail() throws Exception {
addExternalId("test", "test", "nobody@example.com");
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
+ PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
@@ -208,8 +205,7 @@
@Test
public void manualCertificationMatchesExternalId() throws Exception {
addExternalId("foo", "myId", null);
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
+ PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertNoProblems(checker.check(validKeyWithSecondUserId().getPublicKey()));
}
@@ -217,8 +213,7 @@
@Test
public void manualCertificationDoesNotMatchExternalId() throws Exception {
addExternalId("foo", "otherId", null);
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
+ PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
@@ -236,8 +231,7 @@
reloadUser();
TestKey key = validKeyWithSecondUserId();
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
+ PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD,
@@ -245,6 +239,7 @@
+ " http://test/#/settings/web-identities");
checker = checkerFactory.create()
+ .setStore(store)
.disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD,
@@ -277,16 +272,12 @@
add(keyE(), addUser("userE"));
// Checker for A, checking A.
- PublicKeyChecker checkerA = checkerFactory.create()
- .setExpectedUser(user)
- .setStore(store);
+ PublicKeyChecker checkerA = checkerFactory.create(user, store);
assertNoProblems(checkerA.check(keyA.getPublicKey()));
// Checker for B, checking B. Trust chain and IDs are correct, so the only
// problem is with the key itself.
- PublicKeyChecker checkerB = checkerFactory.create()
- .setExpectedUser(userB)
- .setStore(store);
+ PublicKeyChecker checkerB = checkerFactory.create(userB, store);
assertProblems(
checkerB.check(keyB.getPublicKey()), Status.BAD,
"Key is expired");
@@ -311,9 +302,7 @@
add(keyE(), addUser("userE"));
// Checker for A, checking B.
- PublicKeyChecker checkerA = checkerFactory.create()
- .setExpectedUser(user)
- .setStore(store);
+ PublicKeyChecker checkerA = checkerFactory.create(user, store);
assertProblems(
checkerA.check(keyB.getPublicKey()), Status.BAD,
"Key is expired",
@@ -325,9 +314,7 @@
+ " username:user");
// Checker for B, checking A.
- PublicKeyChecker checkerB = checkerFactory.create()
- .setExpectedUser(userB)
- .setStore(store);
+ PublicKeyChecker checkerB = checkerFactory.create(userB, store);
assertProblems(
checkerB.check(keyA.getPublicKey()), Status.BAD,
"Key must contain a valid certification for one of the following"
@@ -346,9 +333,7 @@
TestKey keyA = add(keyA(), user);
TestKey keyB = add(keyB(), addUser("userB"));
- PublicKeyChecker checker = checkerFactory.create()
- .setExpectedUser(user)
- .setStore(store);
+ PublicKeyChecker checker = checkerFactory.create(user, store);
assertProblems(
checker.check(keyA.getPublicKey()), Status.OK,
"No path to a trusted key",
@@ -406,9 +391,7 @@
keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB);
add(keyRingB, addUser("userB"));
- PublicKeyChecker checkerA = checkerFactory.create()
- .setExpectedUser(user)
- .setStore(store);
+ PublicKeyChecker checkerA = checkerFactory.create(user, store);
assertProblems(checkerA.check(keyA.getPublicKey()), Status.OK,
"No path to a trusted key",
"Certification by " + keyToString(keyB)
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index 6f69b38..a611ec9 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -16,6 +16,8 @@
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedCompromisedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedNoLongerUsedKey;
import static com.google.gerrit.gpg.testutil.TestKeys.selfRevokedKey;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
@@ -29,10 +31,15 @@
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyH;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyI;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyJ;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
import static org.junit.Assert.assertEquals;
import com.google.gerrit.gpg.testutil.TestKey;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSignature;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -46,6 +53,7 @@
import java.util.Arrays;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
public class PublicKeyCheckerTest {
@@ -90,7 +98,8 @@
@Test
public void selfRevokedKeyIsRevoked() throws Exception {
- assertProblems(selfRevokedKey(), "Key is revoked");
+ assertProblems(selfRevokedKey(),
+ "Key is revoked (key material has been compromised)");
}
// Test keys specific to this test are at the bottom of this class. Each test
@@ -174,6 +183,57 @@
"No path to a trusted key", notTrusted(ki));
}
+ @Test
+ public void revokedKeyDueToCompromise() throws Exception {
+ TestKey k = add(revokedCompromisedKey());
+ add(validKeyWithoutExpiration());
+ save();
+
+ assertProblems(k,
+ "Key is revoked (key material has been compromised):"
+ + " test6 compromised");
+
+ PGPPublicKeyRing kr = removeRevokers(k.getPublicKeyRing());
+ store.add(kr);
+ save();
+
+ // Key no longer specified as revoker.
+ assertProblems(kr.getPublicKey());
+ }
+
+ @Test
+ public void revokedByKeyNotPresentInStore() throws Exception {
+ TestKey k = add(revokedCompromisedKey());
+ save();
+
+ assertProblems(k,
+ "Key is revoked (key material has been compromised):"
+ + " test6 compromised");
+ }
+
+ @Test
+ public void revokedKeyDueToNoLongerBeingUsed() throws Exception {
+ TestKey k = add(revokedNoLongerUsedKey());
+ add(validKeyWithoutExpiration());
+ save();
+
+ assertProblems(k,
+ "Key is revoked (retired and no longer valid): test7 not used");
+ }
+
+ private PGPPublicKeyRing removeRevokers(PGPPublicKeyRing kr) {
+ PGPPublicKey k = kr.getPublicKey();
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> sigs = k.getSignaturesOfType(DIRECT_KEY);
+ while (sigs.hasNext()) {
+ PGPSignature sig = sigs.next();
+ if (sig.getHashedSubPackets().hasSubpacket(REVOCATION_KEY)) {
+ k = PGPPublicKey.removeCertification(k, sig);
+ }
+ }
+ return PGPPublicKeyRing.insertPublicKey(kr, k);
+ }
+
private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
Map<Long, Fingerprint> fps = new HashMap<>();
for (TestKey k : trusted) {
@@ -208,12 +268,19 @@
private void assertProblems(PublicKeyChecker checker, TestKey k,
String... expected) {
- CheckResult result = checker.check(k.getPublicKey());
+ CheckResult result = checker.setStore(store)
+ .check(k.getPublicKey());
assertEquals(Arrays.asList(expected), result.getProblems());
}
private void assertProblems(TestKey tk, String... expected) throws Exception {
- CheckResult result = new PublicKeyChecker().check(tk.getPublicKey());
+ assertProblems(tk.getPublicKey(), expected);
+ }
+
+ private void assertProblems(PGPPublicKey k, String... expected) throws Exception {
+ CheckResult result = new PublicKeyChecker()
+ .setStore(store)
+ .check(k);
assertEquals(Arrays.asList(expected), result.getProblems());
}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index dc1b557..9b1c058 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -14,7 +14,6 @@
package com.google.gerrit.gpg;
-import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
@@ -33,7 +32,9 @@
import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.PushCertificateIdent;
@@ -49,7 +50,8 @@
import java.util.Arrays;
public class PushCertificateCheckerTest {
- private TestRepository<?> tr;
+ private InMemoryRepository repo;
+ private PublicKeyStore store;
private SignedPushConfig signedPushConfig;
private PushCertificateChecker checker;
@@ -57,14 +59,17 @@
public void setUp() throws Exception {
TestKey key1 = validKeyWithoutExpiration();
TestKey key3 = expiredKey();
- tr = new TestRepository<>(new InMemoryRepository(
- new DfsRepositoryDescription("repo")));
- tr.branch(REFS_GPG_KEYS).commit()
- .add(PublicKeyStore.keyObjectId(key1.getPublicKey().getKeyID()).name(),
- key1.getPublicKeyArmored())
- .add(PublicKeyStore.keyObjectId(key3.getPublicKey().getKeyID()).name(),
- key3.getPublicKeyArmored())
- .create();
+ repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+ store = new PublicKeyStore(repo);
+ store.add(key1.getPublicKeyRing());
+ store.add(key3.getPublicKeyRing());
+
+ PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+ CommitBuilder cb = new CommitBuilder();
+ cb.setAuthor(ident);
+ cb.setCommitter(ident);
+ assertEquals(RefUpdate.Result.NEW, store.save(cb));
+
signedPushConfig = new SignedPushConfig();
signedPushConfig.setCertNonceSeed("sekret");
signedPushConfig.setCertNonceSlopLimit(60 * 24);
@@ -72,10 +77,11 @@
}
private PushCertificateChecker newChecker(boolean checkNonce) {
- return new PushCertificateChecker(new PublicKeyChecker()) {
+ PublicKeyChecker keyChecker = new PublicKeyChecker().setStore(store);
+ return new PushCertificateChecker(keyChecker) {
@Override
protected Repository getRepository() {
- return tr.getRepository();
+ return repo;
}
@Override
@@ -126,7 +132,7 @@
private String validNonce() {
return signedPushConfig.getNonceGenerator()
- .createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
+ .createNonce(repo, System.currentTimeMillis() / 1000);
}
private PushCertificate newSignedCert(String nonce, TestKey signingKey)
@@ -158,7 +164,7 @@
Reader reader =
new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
PushCertificateParser parser =
- new PushCertificateParser(tr.getRepository(), signedPushConfig);
+ new PushCertificateParser(repo, signedPushConfig);
return parser.parse(reader);
}
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
index 3b3ab88..97a94b9 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
@@ -542,6 +542,246 @@
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
}
- // TODO(dborowitz): Figure out how to get gpg to revoke a key for someone
- // else.
+ /**
+ * A key revoked by a valid key, due to key compromise.
+ * <p>
+ * Revoked by {@link #validKeyWithoutExpiration()}.
+ *
+ * <pre>
+ * pub 2048R/3434B39F 2015-10-20 [revoked: 2015-10-20]
+ * Key fingerprint = 931F 047D 7D01 DDEF 367A 8D90 8C4F D28E 3434 B39F
+ * uid Testuser Six <test6@example.com>
+ * </pre>
+ */
+ public static TestKey revokedCompromisedKey() throws Exception {
+ return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ + "Version: GnuPG v1\n"
+ + "\n"
+ + "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\n"
+ + "tULvIEIjNNP8Qizfc4u2O8h7HP2b3yYSrp9MMQ==\n"
+ + "=Dxr7\n"
+ + "-----END PGP PUBLIC KEY BLOCK-----\n",
+ "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+ + "Version: GnuPG v1\n"
+ + "\n"
+ + "lQOYBFYmpXkBCACqaLz51DcWQmfOJnat9iHSySfSHwbKfVvoN43Ba2cf/D/PadRc\n"
+ + "HLgc+91k2yk1kV1LnMdvUGj5zZ84ZqrQx3f1WeItnzZpqxtmQS/GSxCp9EY/s7w6\n"
+ + "5i86R/k9Tzgvk0B7dKZJXbM/OWxxDkkxHWE3Un9wreX7bDU5b9D2knHRiNFqH9ZJ\n"
+ + "KqDIFZqH9WTUxNZcHz20sTCRIMfvsAwf2vRU5N5xTu4Mbk6JFc7BAj7h1f/mYEPo\n"
+ + "CTyB1jV/DSDVdn1FjJVocSg6W/CvsYF9hKFYjJHl4VXdePTpnOjHhJLL0QWk0TMe\n"
+ + "xYeUi/xDr5DeMxTmi7F7BFaQEF+KmUM46e+9ABEBAAEAB/wOspbuA1A3AsY6QRYG\n"
+ + "Xg6/w+rD1Do9N7+4ESaQUqej2hlU1d9jjHSSx2RqgP6WaLG/xkdrQeez9/iuICjG\n"
+ + "dhXSGw0He05xobjswl2RAENxLSjr8KAhAl57a97C23TQoaYzn7WB6Wt+3gCM5bsJ\n"
+ + "WevbHinwuYb2/ve+OvcudSYM+Nhtpv0DoTaizhi9wzc3g/XLbturlpdCffbw4y+h\n"
+ + "gBPd/t3cc/0Ams8Wi2RlmDOoe73ls23nBHcNomgydyIYBn7U5Z3v3YkPNp9VBiXx\n"
+ + "rC4mDtB1ugucMhqjRNAYqinaLP35CiBTU/IB0WLu7ZyytnjY5frly1ShAG8wFL0B\n"
+ + "MOMxBADJjGy1NwGSd/7eMeYyYThyhXDxo5so91/O1+RLnSUVv/Nz6VOPp2TtuVN5\n"
+ + "uTJkpSXtUFyWbf8mkQiFz4++vHW5E/Q6+KomXRalK7JeBzeFMtax64ykQHID9cSu\n"
+ + "TaSHBhOEEeZZuf6BlulYEJEBHYK6EFlPJn+cpZtTFaqDoKh22QQA2HKjfyeppNre\n"
+ + "WRFJ9h1x1hBlSRR+XIPYmDmZUjL37jQUlw8iF+txPclfyNBw2I2Om+Jhcf25peOx\n"
+ + "ow4yvjt8r3qDjNhI2zLE9u4zrQ9xU8CUingT0t4k3NO2vigpKlmp1/w2IHSMctry\n"
+ + "v1v3+BAS8qGIYDY1lgI7QBvle5hxGYUD/00zMyHOIgYg/cM5sR0qafesoj9kRff5\n"
+ + "UMnSy1dw+pGMv6GqKGbcZDoC060hUO9GhQRPZXF8PlYzD30lOLS2Uw4mPXjOmQVv\n"
+ + "lDiyl/vLkfkVfP/alYH0FW6mErDrjtHhrZewqDm3iPLGMVGfGCJsL+N37VBSe+jr\n"
+ + "4rZCnjk/Jo5JRoKJATcEHwECACEFAlYmphQXDIABBK6n7S+CETPlsSjR7QYl3EYy\n"
+ + "iowCBwAACgkQjE/SjjQ0s59h3gf/cal/F8n6czMEKGfUmoWso6qpiI47ixTC5tfO\n"
+ + "lE7fdXaNStRXvk9NYb/o2WANs98oVxTSpm53ozYRDFJL38jbSL8ylEHuomdYwz/o\n"
+ + "fF0wMnSj+k3giYgaPfxBMhbHQNL2fdvCVyhuXtmUzyDY3WS/mNA8S66t994lFLif\n"
+ + "LikS8IKbYhJIY64HQNW1gcYOsktDSTiYOQQm5Tx8/m6fyY3wjx4Ag2WxMWmxOzAx\n"
+ + "cyKm1lOblIjlBmUbebky+9ySeBh2HKe8JZzokca3UQdeUjgDlB62v/gVs0Ti5hwQ\n"
+ + "SHLMHLhZB+JAI+IjPObc7hwyev037kfNipGG7Y695ibzobZHGrQgVGVzdHVzZXIg\n"
+ + "U2l4IDx0ZXN0NkBleGFtcGxlLmNvbT6JATgEEwECACIFAlYmpXkCGwMGCwkIBwMC\n"
+ + "BhUIAgkKCwQWAgMBAh4BAheAAAoJEIxP0o40NLOfZe0H/jCXhy1zdxb0XCnXtyS0\n"
+ + "WLqZ3x+NGJHAT+u5UogqYAfN4wsGhmfgtlW6fI0JEYtl8/4xSzgmEuSnyKhPNQHv\n"
+ + "E+X2T3gVd6Vs5uf0XRPuG2MUuIUA4x7KSd7KWTaRqVO3ogEq1o4+f5XqicHA75gd\n"
+ + "vC37EsyEBaNn9YDRMgLVEi6R3NYSuF372UbwH2S6kYLls+j8CAI+hqM+FLWouu2H\n"
+ + "6UKw0ldP9G2TonHmibKnExc/ODM21QswU42zNpGceesIdCD/IlkMLDd99blKF/J9\n"
+ + "Z0UzrmOAQqicwovPL1dX4EzoglCWAcIEA/xYcpJ/iowbj+ERYuvAD9tNDvDQNJXz\n"
+ + "yaydA5gEVialeQEIALMgwUMLqRxXny68XdfA6tLcOYo08M3M7/gDL+I7Flub700/\n"
+ + "xBYQSb4DMK1kx9h1nwLcOO7PGSFxpwj8TlqBkgJ2zpuzT5VLtUoR41in9StMX2mR\n"
+ + "k1ex4Ksk+CtTfV6gZVzlsTlzRMYuECNswq1HktkUykBeCkDuFclJK4BcazyCOQmR\n"
+ + "hIlicnAg7J3E6LmOacT9TlC4yMEbRvDEVfGRoNrmZx5sn/oLyaCQrIBNtCQJ7tgk\n"
+ + "aXUksri9jbzJtIyEwO1mLCC8rN34dxCv7FRC+aBIwCklozEo8dEDUis/9JNdQQCN\n"
+ + "wZ+T8cLP/SPuYkbZy8LF44wCan+lPOFKheKpQ2UAEQEAAQAH/A1Os+Tb9yiGnuoN\n"
+ + "LuiSKa/YEgNBOxmC7dnuPK6xJpBQNZc200WzWJMf8AwVpl4foNxIyYb+Rjbsl1Ts\n"
+ + "z5JcOWFq+57oE5O7D+EMkqf5tFZO4nC4kqprac41HSW02mW/A0DDRKcIt/WEIwlK\n"
+ + "sWzHmjJ736moAtl/holRYQS0ePgB8bUPDQcFovH6X3SUxlPGTYD1DEX+WNvYRk3r\n"
+ + "pa9YXH65qbG9CEJIFTmwZIRDl+CBtBlN/fKadyMJr9fXtv7Fu9hNsK1K1pUtLqCa\n"
+ + "nc22Zak+o+LCPlZ8vmw/UmOGtp2iZlEragmh2rOywp0dHF7gsdlgoafQf8Q4NIag\n"
+ + "TFyHf1kEAMSOKUUwLBEmPnDVfoEOt5spQLVtlF8sh/Okk9zVazWmw0n/b1Ef72z6\n"
+ + "EZqCW9/XhH5pXfKJeV+08hroHI6a5UESa7/xOIx50TaQdRqjwGciMnH2LJcpIU/L\n"
+ + "f0cGXcnTLKt4Z2GeSPKFTj4VzwmwH5F/RYdc5eiVb7VNoy9DC5RZBADpTVH5pklS\n"
+ + "44VDJIcwSNy1LBEU3oj+Nu+sufCimJ5B7HLokoJtm6q8VQRga5hN1TZkdQcLy+b2\n"
+ + "wzxHYoIsIsYFfG/mqLZ3LJNDFqze1/Kj987DYSUGeNYexMN2Fkzbo35Jf0cpOiao\n"
+ + "390JFOS7qecUak5/yJ/V4xy8/nds37617QP9GWlFBykDoESBC2AIz8wXcpUBVNeH\n"
+ + "BNSthmC+PJPhsS6jTQuipqtXUZBgZBrMHp/bA8gTOkI4rPXycH3+ACbuQMAjbFny\n"
+ + "Kt69lPHD8VWw/82E4EY2J9LmHli+2BcATz89ouC4kqC5zF90qJseviSZPihpnFxA\n"
+ + "1UqMU2ZjsPb4CM9C/YkBHwQYAQIACQUCVialeQIbDAAKCRCMT9KONDSzn+RUB/9C\n"
+ + "8c6G6gFKVLE9yC+e8KXxXfNs1HU/2WCNgbAyy8uHAJPta+aaRQq+1P3Xe4RH3c3z\n"
+ + "EkPay8aEppumI+P396jSnenrh2JyKOw/VRB+mZYo9r9X2y/N6nTxykOX95zNKIuq\n"
+ + "BxNcKhl57BZehAaqqhefBriXBC/hm+PtNMI3bBHOgbBtFyYbaGZzbu47AD6FdsX5\n"
+ + "st7oc5lH/tT3ePxrbaDZf06BJP9AKwuDKJRLDdhhoRqf22fC4SAARj+tyE6+B5d7\n"
+ + "lKXTBMUscfo5LgEEaBzUtEqdIIzZ2Bmq+E47E6+66J4FZjq1Qu8gQiM00/xCLN9z\n"
+ + "i7Y7yHsc/ZvfJhKun0wx\n"
+ + "=M/kw\n"
+ + "-----END PGP PRIVATE KEY BLOCK-----\n");
+ }
+
+ /**
+ * A key revoked by a valid key, due to no longer being used.
+ * <p>
+ * Revoked by {@link #validKeyWithoutExpiration()}.
+ *
+ * <pre>
+ * pub 2048R/3D6C52D0 2015-10-20 [revoked: 2015-10-20]
+ * Key fingerprint = 32DB 6C31 2ED7 A98D 11B2 43EA FAD2 ABE2 3D6C 52D0
+ * uid Testuser Seven <test7@example.com>
+ * </pre>
+ */
+ public static TestKey revokedNoLongerUsedKey() throws Exception {
+ return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ + "Version: GnuPG v1\n"
+ + "\n"
+ + "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\n"
+ + "+B6paw/s8P7XCWznTBRdlFWWgrhcpzQ8fefC\n"
+ + "=CHer\n"
+ + "-----END PGP PUBLIC KEY BLOCK-----\n",
+ "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+ + "Version: GnuPG v1\n"
+ + "\n"
+ + "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\n"
+ + "bOdMFF2UVZaCuFynNDx958I=\n"
+ + "=aoJv\n"
+ + "-----END PGP PRIVATE KEY BLOCK-----\n");
+ }
+
}