Merge changes from topic 'revoked'

* changes:
  Distinguish between assertProblems and assertNoProblems
  PublicKeyStore: Use {@code null} in Javadoc
  PublicKeyChecker: Support checking at a given effective date
  PublicKeyChecker: Fix revocation check (mostly)
  PushCertificateChecker: Convert to a builder pattern
  PublicKeyChecker: Convert to a builder pattern
  TestKeys: Use descriptive names
This commit is contained in:
Dave Borowitz
2015-10-26 18:16:22 +00:00
committed by Gerrit Code Review
18 changed files with 1245 additions and 255 deletions

View File

@@ -19,6 +19,10 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.gpg.testutil.TestKeys.allValidKeys;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Function;
@@ -38,7 +42,6 @@ import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.gpg.testutil.TestKeys;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.config.AllUsersName;
@@ -204,7 +207,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addGpgKey() throws Exception {
TestKey key = TestKeys.key1();
TestKey key = validKeyWithoutExpiration();
String id = key.getKeyIdString();
addExternalIdEmail(admin, "test1@example.com");
@@ -220,7 +223,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void reAddExistingGpgKey() throws Exception {
addExternalIdEmail(admin, "test5@example.com");
TestKey key = TestKeys.key5();
TestKey key = validKeyWithSecondUserId();
String id = key.getKeyIdString();
PGPPublicKey pk = key.getPublicKey();
@@ -243,7 +246,7 @@ public class AccountIT extends AbstractDaemonTest {
db.accountExternalIds().insert(Collections.singleton(extId));
TestKey key = TestKeys.key5();
TestKey key = validKeyWithSecondUserId();
addGpgKey(key.getPublicKeyArmored());
setApiUser(user);
@@ -254,7 +257,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void listGpgKeys() throws Exception {
List<TestKey> keys = TestKeys.allValidKeys();
List<TestKey> keys = allValidKeys();
List<String> toAdd = new ArrayList<>(keys.size());
for (TestKey key : keys) {
addExternalIdEmail(admin,
@@ -267,7 +270,7 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void deleteGpgKey() throws Exception {
TestKey key = TestKeys.key1();
TestKey key = validKeyWithoutExpiration();
String id = key.getKeyIdString();
addExternalIdEmail(admin, "test1@example.com");
addGpgKey(key.getPublicKeyArmored());
@@ -283,13 +286,13 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void addAndRemoveGpgKeys() throws Exception {
for (TestKey key : TestKeys.allValidKeys()) {
for (TestKey key : allValidKeys()) {
addExternalIdEmail(admin,
PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
}
TestKey key1 = TestKeys.key1();
TestKey key2 = TestKeys.key2();
TestKey key5 = TestKeys.key5();
TestKey key1 = validKeyWithoutExpiration();
TestKey key2 = validKeyWithExpiration();
TestKey key5 = validKeyWithSecondUserId();
Map<String, GpgKeyInfo> infos = gApi.accounts().self().putGpgKeys(
ImmutableList.of(

View File

@@ -18,8 +18,8 @@ import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

View File

@@ -19,6 +19,9 @@ import static com.google.common.base.Preconditions.checkArgument;
import org.eclipse.jgit.util.NB;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class Fingerprint {
private final byte[] fp;
@@ -33,6 +36,18 @@ public class Fingerprint {
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) {
result.put(fp.getId(), fp);
}
return Collections.unmodifiableMap(result);
}
private static byte[] checkLength(byte[] fp) {
checkArgument(fp.length == 20,
"fingerprint must be 20 bytes, got %s", fp.length);
@@ -54,6 +69,23 @@ public class Fingerprint {
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;
}
@@ -79,6 +111,6 @@ public class Fingerprint {
}
public long getId() {
return NB.decodeInt64(fp, 12);
return getId(fp);
}
}

View File

@@ -14,14 +14,14 @@
package com.google.gerrit.gpg;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.PageLinks;
@@ -44,11 +44,10 @@ import org.eclipse.jgit.transport.PushCertificateIdent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@@ -61,18 +60,13 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
private static final Logger log =
LoggerFactory.getLogger(GerritPublicKeyChecker.class);
private final Provider<ReviewDb> db;
private final String webUrl;
private final IdentifiedUser.GenericFactory userFactory;
private final IdentifiedUser expectedUser;
@Singleton
public static class Factory {
private final Provider<ReviewDb> db;
private final String webUrl;
private final IdentifiedUser.GenericFactory userFactory;
private final int maxTrustDepth;
private final ImmutableList<Fingerprint> trusted;
private final ImmutableMap<Long, Fingerprint> trusted;
@Inject
Factory(@GerritServerConfig Config cfg,
@@ -86,52 +80,58 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
String[] strs = cfg.getStringList("receive", null, "trustedKey");
if (strs.length != 0) {
List<Fingerprint> fps = new ArrayList<>(strs.length);
Map<Long, Fingerprint> fps =
Maps.newHashMapWithExpectedSize(strs.length);
for (String str : strs) {
str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
fps.add(new Fingerprint(BaseEncoding.base16().decode(str)));
Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
fps.put(fp.getId(), fp);
}
trusted = ImmutableList.copyOf(fps);
trusted = ImmutableMap.copyOf(fps);
} else {
trusted = null;
}
}
/**
* Create a checker that can check arbitrary public keys.
* <p>
* Each key is checked against the set of identities in the database
* belonging to the same user as the key.
*
* @return a new checker.
*/
public GerritPublicKeyChecker create() {
return new GerritPublicKeyChecker(this, null);
return new GerritPublicKeyChecker(this);
}
/**
* Create a checker for checking a single public key against a known user.
* <p>
* The top-level key passed to {@link #check(PGPPublicKey, PublicKeyStore)}
* must belong to the given user. (Other keys checked in the course of
* verifying the web of trust are checked against the set of identities in
* the database belonging to the same user as the key.)
*
* @param expectedUser the user
* @return a new checker.
*/
public GerritPublicKeyChecker create(IdentifiedUser expectedUser) {
checkNotNull(expectedUser);
return new GerritPublicKeyChecker(this, expectedUser);
public GerritPublicKeyChecker create(IdentifiedUser expectedUser,
PublicKeyStore store) {
GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
checker.setExpectedUser(expectedUser);
checker.setStore(store);
return checker;
}
}
private GerritPublicKeyChecker(Factory factory, IdentifiedUser expectedUser) {
super(factory.maxTrustDepth, factory.trusted);
private final Provider<ReviewDb> db;
private final String webUrl;
private final IdentifiedUser.GenericFactory userFactory;
private IdentifiedUser expectedUser;
private GerritPublicKeyChecker(Factory factory) {
this.db = factory.db;
this.webUrl = factory.webUrl;
this.userFactory = factory.userFactory;
if (factory.trusted != null) {
enableTrust(factory.maxTrustDepth, factory.trusted);
}
}
/**
* Set the expected user for this checker.
* <p>
* If set, the top-level key passed to {@link #check(PGPPublicKey)} must
* belong to the given user. (Other keys checked in the course of verifying
* the web of trust are checked against the set of identities in the database
* belonging to the same user as the key.)
*/
public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) {
this.expectedUser = expectedUser;
return this;
}
@Override

View File

@@ -26,8 +26,7 @@ import java.io.IOException;
public class GerritPushCertificateChecker extends PushCertificateChecker {
public interface Factory {
GerritPushCertificateChecker create(IdentifiedUser expectedUser,
boolean checkNonce);
GerritPushCertificateChecker create(IdentifiedUser expectedUser);
}
private final GitRepositoryManager repoManager;
@@ -38,9 +37,8 @@ public class GerritPushCertificateChecker extends PushCertificateChecker {
GerritPublicKeyChecker.Factory keyCheckerFactory,
GitRepositoryManager repoManager,
AllUsersName allUsers,
@Assisted IdentifiedUser expectedUser,
@Assisted boolean checkNonce) {
super(keyCheckerFactory.create(expectedUser), checkNonce);
@Assisted IdentifiedUser expectedUser) {
super(keyCheckerFactory.create().setExpectedUser(expectedUser));
this.repoManager = repoManager;
this.allUsers = allUsers;
}

View File

@@ -19,20 +19,34 @@ 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 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.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -42,69 +56,91 @@ import java.util.Set;
/** 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;
private final Map<Long, Fingerprint> trusted;
private final int maxTrustDepth;
/** Create a new checker that does not check the web of trust. */
public PublicKeyChecker() {
this(0, null);
}
private PublicKeyStore store;
private Map<Long, Fingerprint> trusted;
private int maxTrustDepth;
private Date effectiveTime = new Date();
/**
* Enable web-of-trust checks.
* <p>
* If enabled, a store must be set with {@link #setStore(PublicKeyStore)}.
* (These methods are separate since the store is a closeable resource that
* may not be available when reading trusted keys from a config.)
*
* @param maxTrustDepth maximum depth to search while looking for a trusted
* key.
* @param trusted ultimately trusted key fingerprints; may not be empty. If
* null, disable web-of-trust checks.
* @param trusted ultimately trusted key fingerprints, keyed by fingerprint;
* may not be empty. To construct a map, see {@link
* Fingerprint#byId(Iterable)}.
* @return a reference to this object.
*/
public PublicKeyChecker(int maxTrustDepth, Collection<Fingerprint> trusted) {
if (trusted != null) {
if (maxTrustDepth <= 0) {
public PublicKeyChecker enableTrust(int maxTrustDepth,
Map<Long, Fingerprint> trusted) {
if (maxTrustDepth <= 0) {
throw new IllegalArgumentException(
"maxTrustDepth must be positive, got: " + maxTrustDepth);
}
if (trusted == null || trusted.isEmpty()) {
throw new IllegalArgumentException(
"maxTrustDepth must be positive, got: " + maxTrustDepth);
}
if (trusted.isEmpty()) {
throw new IllegalArgumentException("at least one trusted key required");
}
this.trusted = new HashMap<>();
for (Fingerprint fp : trusted) {
this.trusted.put(fp.getId(), fp);
}
} else {
this.trusted = null;
"at least one trusted key is required");
}
this.maxTrustDepth = maxTrustDepth;
this.trusted = trusted;
return this;
}
/**
* Check a public key, including its web of trust.
*
* @param key the public key.
* @param store a store to read public keys from for trust checks. If this
* store is not configured for web-of-trust checks, this argument is
* ignored.
* @return the result of the check.
*/
public final CheckResult check(PGPPublicKey key, PublicKeyStore store) {
if (trusted == null) {
return check(key);
} else if (store == null) {
throw new IllegalArgumentException(
"PublicKeyStore required for web of trust checks");
/** Disable web-of-trust checks. */
public PublicKeyChecker disableTrust() {
trusted = null;
return this;
}
/** 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");
}
return check(key, store, 0, true, new HashSet<Fingerprint>());
this.store = store;
return this;
}
/**
* Check only a public key, not including its web of trust.
* Set the effective time for checking the key.
* <p>
* If set, check whether the key should be considered valid (e.g. unexpired)
* as of this time.
*
* @param effectiveTime effective time.
* @return a reference to this object.
*/
public PublicKeyChecker setEffectiveTime(Date effectiveTime) {
this.effectiveTime = effectiveTime;
return this;
}
protected Date getEffectiveTime() {
return effectiveTime;
}
/**
* Check a public key.
*
* @param key the public key.
* @return the result of the check.
*/
public final CheckResult check(PGPPublicKey key) {
return check(key, null, 0, false, null);
if (store == null) {
throw new IllegalStateException("PublicKeyStore is required");
}
return check(key, 0, true,
trusted != null ? new HashSet<Fingerprint>() : null);
}
/**
@@ -115,17 +151,17 @@ public class PublicKeyChecker {
*
* @param key the public key.
* @param depth the depth from the initial key passed to {@link #check(
* PGPPublicKey, PublicKeyStore)}: 0 if this was the initial key, up to a
* maximum of {@code maxTrustDepth}.
* PGPPublicKey)}: 0 if this was the initial key, up to a maximum of
* {@code maxTrustDepth}.
* @return the result of the custom check.
*/
public CheckResult checkCustom(PGPPublicKey key, int depth) {
return CheckResult.ok();
}
private CheckResult check(PGPPublicKey key, PublicKeyStore store, int depth,
boolean expand, Set<Fingerprint> seen) {
CheckResult basicResult = checkBasic(key);
private CheckResult check(PGPPublicKey key, int depth, boolean expand,
Set<Fingerprint> seen) {
CheckResult basicResult = checkBasic(key, effectiveTime);
CheckResult customResult = checkCustom(key, depth);
CheckResult trustResult = checkWebOfTrust(key, store, depth, seen);
if (!expand && !trustResult.isTrusted()) {
@@ -160,28 +196,189 @@ public class PublicKeyChecker {
return CheckResult.create(status, problems);
}
private CheckResult checkBasic(PGPPublicKey key) {
private CheckResult checkBasic(PGPPublicKey key, Date now) {
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, now, problems);
long validSecs = key.getValidSeconds();
if (validSecs != 0) {
long createdSecs = key.getCreationTime().getTime() / 1000;
long nowSecs = System.currentTimeMillis() / 1000;
if (nowSecs - createdSecs > validSecs) {
long validMs = key.getValidSeconds() * 1000;
if (validMs != 0) {
long msSinceCreation = now.getTime() - key.getCreationTime().getTime();
if (msSinceCreation > validMs) {
problems.add("Key is expired");
}
}
return CheckResult.create(problems);
}
private void gatherRevocationProblems(PGPPublicKey key, Date now,
List<String> problems) {
try {
List<PGPSignature> revocations = new ArrayList<>();
Map<Long, RevocationKey> revokers = new HashMap<>();
PGPSignature selfRevocation =
scanRevocations(key, now, revocations, revokers);
if (selfRevocation != null) {
RevocationReason reason = getRevocationReason(selfRevocation);
if (isRevocationValid(selfRevocation, reason, now)) {
problems.add(reasonToString(reason));
}
} else {
checkRevocations(key, revocations, revokers, problems);
}
} catch (PGPException | IOException e) {
problems.add("Error checking key revocation");
}
}
private static boolean isRevocationValid(PGPSignature revocation,
RevocationReason reason, Date now) {
// RFC4880 states:
// "If a key has been revoked because of a compromise, all signatures
// created by that key are suspect. However, if it was merely superseded or
// retired, old signatures are still valid."
//
// Note that GnuPG does not implement this correctly, as it does not
// consider the revocation reason and timestamp when checking whether a
// signature (data or certification) is valid.
return reason.getRevocationReason() == KEY_COMPROMISED
|| revocation.getCreationTime().before(now);
}
private PGPSignature scanRevocations(PGPPublicKey key, Date now,
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 {
RevocationReason reason = getRevocationReason(sig);
if (reason != null && isRevocationValid(sig, reason, now)) {
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 revokerKeyRing = store.get(rfp);
if (revokerKeyRing == null) {
// 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;
}
PGPPublicKey rk = revokerKeyRing.getPublicKey();
if (rk.getAlgorithm() != revoker.getAlgorithm()) {
continue;
}
if (!checkBasic(rk, revocation.getCreationTime()).isOk()) {
// Revoker's key was expired or revoked at time of revocation, so the
// revocation is invalid.
continue;
}
revocation.init(new BcPGPContentVerifierBuilderProvider(), rk);
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 || store == null) {
if (trusted == null) {
// Trust checking not configured, server trusts all OK keys.
return CheckResult.trusted();
}
@@ -204,8 +401,13 @@ public class PublicKeyChecker {
Iterator<String> userIds = key.getUserIDs();
while (userIds.hasNext()) {
String userId = userIds.next();
// Don't check the timestamp of these certifications. This allows admins
// to correct untrusted keys by signing them with a trusted key, such that
// older signatures created by those keys retroactively appear valid.
@SuppressWarnings("unchecked")
Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
while (sigs.hasNext()) {
PGPSignature sig = sigs.next();
// TODO(dborowitz): Handle CERTIFICATION_REVOCATION.
@@ -222,8 +424,7 @@ public class PublicKeyChecker {
}
String subpacketProblem = checkTrustSubpacket(sig, depth);
if (subpacketProblem == null) {
CheckResult signerResult =
check(signer, store, depth + 1, false, seen);
CheckResult signerResult = check(signer, depth + 1, false, seen);
if (signerResult.isTrusted()) {
return CheckResult.trusted();
}

View File

@@ -83,7 +83,7 @@ public class PublicKeyStore implements AutoCloseable {
* @param sig signature object.
* @param data signed payload.
* @return the key chosen from {@code keyRings} that was able to verify the
* signature, or null if none was found.
* signature, or {@code null} if none was found.
* @throws PGPException if an error occurred verifying the signature.
*/
public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
@@ -107,7 +107,7 @@ public class PublicKeyStore implements AutoCloseable {
* @param userId user ID being certified.
* @param key key being certified.
* @return the key chosen from {@code keyRings} that was able to verify the
* certification, or null if none was found.
* certification, or {@code null} if none was found.
* @throws PGPException if an error occurred verifying the certification.
*/
public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
@@ -178,15 +178,39 @@ public class PublicKeyStore implements AutoCloseable {
*/
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 @@ public class PublicKeyStore implements AutoCloseable {
}
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 @@ public class PublicKeyStore implements AutoCloseable {
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();

View File

@@ -67,12 +67,18 @@ public abstract class PushCertificateChecker {
}
private final PublicKeyChecker publicKeyChecker;
private final boolean checkNonce;
protected PushCertificateChecker(PublicKeyChecker publicKeyChecker,
boolean checkNonce) {
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;
}
/**
@@ -202,7 +208,10 @@ public abstract class PushCertificateChecker {
CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID())
+ " is not valid"));
}
CheckResult result = publicKeyChecker.check(signer, store);
CheckResult result = publicKeyChecker
.setStore(store)
.setEffectiveTime(sig.getCreationTime())
.check(signer);
if (!result.getProblems().isEmpty()) {
StringBuilder err = new StringBuilder("Invalid public key ")
.append(keyToString(signer))

View File

@@ -54,7 +54,8 @@ public class SignedPushPreReceiveHook implements PreReceiveHook {
if (cert == null) {
return;
}
CheckResult result = checkerFactory.create(user.get(), true)
CheckResult result = checkerFactory.create(user.get())
.setCheckNonce(true)
.check(cert)
.getCheckResult();
if (!isAllowed(result, commands)) {

View File

@@ -95,8 +95,10 @@ public class GpgApiAdapterImpl implements GpgApiAdapter {
IdentifiedUser expectedUser) throws GpgException {
try {
PushCertificate cert = PushCertificateParser.fromString(certStr);
PushCertificateChecker.Result result =
pushCertCheckerFactory.create(expectedUser, false).check(cert);
PushCertificateChecker.Result result = pushCertCheckerFactory
.create(expectedUser)
.setCheckNonce(false)
.check(cert);
PushCertificateInfo info = new PushCertificateInfo();
info.certificate = certStr;
info.key = GpgKeys.toJson(result.getPublicKey(), result.getCheckResult());

View File

@@ -163,7 +163,7 @@ public class GpgKeys implements
found = true;
GpgKeyInfo info = toJson(
keyRing.getPublicKey(),
checkerFactory.create(rsrc.getUser()),
checkerFactory.create(rsrc.getUser(), store),
store);
keys.put(info.id, info);
info.id = null;
@@ -197,7 +197,7 @@ public class GpgKeys implements
try (PublicKeyStore store = storeProvider.get()) {
return toJson(
rsrc.getKeyRing().getPublicKey(),
checkerFactory.create(rsrc.getUser()),
checkerFactory.create().setExpectedUser(rsrc.getUser()),
store);
}
}
@@ -261,7 +261,7 @@ public class GpgKeys implements
static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker,
PublicKeyStore store) throws IOException {
return toJson(key, checker.check(key, store));
return toJson(key, checker.setStore(store).check(key));
}
public static void toJson(GpgKeyInfo info, CheckResult checkResult) {

View File

@@ -36,6 +36,7 @@ import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.gpg.CheckResult;
import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.PostGpgKeys.Input;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -193,7 +194,9 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
for (PGPPublicKeyRing keyRing : keyRings) {
PGPPublicKey key = keyRing.getPublicKey();
// Don't check web of trust; admins can fill in certifications later.
CheckResult result = checkerFactory.create(rsrc.getUser()).check(key);
CheckResult result = checkerFactory.create(rsrc.getUser(), store)
.disableTrust()
.check(key);
if (!result.isOk()) {
throw new BadRequestException(String.format(
"Problems with public key %s:\n%s",
@@ -245,12 +248,12 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
throws IOException {
// Unlike when storing keys, include web-of-trust checks when producing
// result JSON, so the user at least knows of any issues.
GerritPublicKeyChecker checker = checkerFactory.create(user);
PublicKeyChecker checker = checkerFactory.create(user, store);
Map<String, GpgKeyInfo> infos =
Maps.newHashMapWithExpectedSize(keys.size() + deleted.size());
for (PGPPublicKeyRing keyRing : keys) {
PGPPublicKey key = keyRing.getPublicKey();
CheckResult result = checker.check(key, store);
CheckResult result = checker.check(key);
GpgKeyInfo info = GpgKeys.toJson(key, result);
infos.put(info.id, info);
info.id = null;

View File

@@ -14,10 +14,10 @@
package com.google.gerrit.gpg;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.gpg.GerritPublicKeyChecker.toExtIdKey;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
@@ -32,7 +32,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.gpg.testutil.TestKeys;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -67,6 +66,7 @@ import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -171,8 +171,9 @@ public class GerritPublicKeyCheckerTest {
@Test
public void defaultGpgCertificationMatchesEmail() throws Exception {
TestKey key = TestKeys.key5();
GerritPublicKeyChecker checker = checkerFactory.create(user);
TestKey key = validKeyWithSecondUserId();
PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD,
"Key must contain a valid certification for one of the following "
@@ -181,16 +182,18 @@ public class GerritPublicKeyCheckerTest {
+ " username:user");
addExternalId("test", "test", "test5@example.com");
checker = checkerFactory.create(user);
checker = checkerFactory.create(user, store)
.disableTrust();
assertNoProblems(checker.check(key.getPublicKey()));
}
@Test
public void defaultGpgCertificationDoesNotMatchEmail() throws Exception {
addExternalId("test", "test", "nobody@example.com");
GerritPublicKeyChecker checker = checkerFactory.create(user);
PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(TestKeys.key5().getPublicKey()), Status.BAD,
checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
"Key must contain a valid certification for one of the following "
+ "identities:\n"
+ " gerrit:user\n"
@@ -202,16 +205,18 @@ public class GerritPublicKeyCheckerTest {
@Test
public void manualCertificationMatchesExternalId() throws Exception {
addExternalId("foo", "myId", null);
GerritPublicKeyChecker checker = checkerFactory.create(user);
assertNoProblems(checker.check(TestKeys.key5().getPublicKey()));
PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertNoProblems(checker.check(validKeyWithSecondUserId().getPublicKey()));
}
@Test
public void manualCertificationDoesNotMatchExternalId() throws Exception {
addExternalId("foo", "otherId", null);
GerritPublicKeyChecker checker = checkerFactory.create(user);
PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(TestKeys.key5().getPublicKey()), Status.BAD,
checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
"Key must contain a valid certification for one of the following "
+ "identities:\n"
+ " foo:otherId\n"
@@ -225,14 +230,17 @@ public class GerritPublicKeyCheckerTest {
db.accountExternalIds().byAccount(user.getAccountId()));
reloadUser();
TestKey key = TestKeys.key5();
GerritPublicKeyChecker checker = checkerFactory.create(user);
TestKey key = validKeyWithSecondUserId();
PublicKeyChecker checker = checkerFactory.create(user, store)
.disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD,
"No identities found for user; check"
+ " http://test/#/settings/web-identities");
checker = checkerFactory.create();
checker = checkerFactory.create()
.setStore(store)
.disableTrust();
assertProblems(
checker.check(key.getPublicKey()), Status.BAD,
"Key is not associated with any users");
@@ -264,14 +272,14 @@ public class GerritPublicKeyCheckerTest {
add(keyE(), addUser("userE"));
// Checker for A, checking A.
GerritPublicKeyChecker checkerA = checkerFactory.create(user);
assertNoProblems(checkerA.check(keyA.getPublicKey(), 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.
GerritPublicKeyChecker checkerB = checkerFactory.create(userB);
PublicKeyChecker checkerB = checkerFactory.create(userB, store);
assertProblems(
checkerB.check(keyB.getPublicKey(), store), Status.BAD,
checkerB.check(keyB.getPublicKey()), Status.BAD,
"Key is expired");
}
@@ -294,9 +302,9 @@ public class GerritPublicKeyCheckerTest {
add(keyE(), addUser("userE"));
// Checker for A, checking B.
GerritPublicKeyChecker checkerA = checkerFactory.create(user);
PublicKeyChecker checkerA = checkerFactory.create(user, store);
assertProblems(
checkerA.check(keyB.getPublicKey(), store), Status.BAD,
checkerA.check(keyB.getPublicKey()), Status.BAD,
"Key is expired",
"Key must contain a valid certification for one of the following"
+ " identities:\n"
@@ -306,9 +314,9 @@ public class GerritPublicKeyCheckerTest {
+ " username:user");
// Checker for B, checking A.
GerritPublicKeyChecker checkerB = checkerFactory.create(userB);
PublicKeyChecker checkerB = checkerFactory.create(userB, store);
assertProblems(
checkerB.check(keyA.getPublicKey(), store), Status.BAD,
checkerB.check(keyA.getPublicKey()), Status.BAD,
"Key must contain a valid certification for one of the following"
+ " identities:\n"
+ " gerrit:userB\n"
@@ -325,9 +333,9 @@ public class GerritPublicKeyCheckerTest {
TestKey keyA = add(keyA(), user);
TestKey keyB = add(keyB(), addUser("userB"));
GerritPublicKeyChecker checker = checkerFactory.create(user);
PublicKeyChecker checker = checkerFactory.create(user, store);
assertProblems(
checker.check(keyA.getPublicKey(), store), Status.OK,
checker.check(keyA.getPublicKey()), Status.OK,
"No path to a trusted key",
"Certification by " + keyToString(keyB.getPublicKey())
+ " is valid, but key is not trusted",
@@ -352,15 +360,16 @@ public class GerritPublicKeyCheckerTest {
// This checker can check any key, so the only problems come from issues
// with the keys themselves, not having invalid user IDs.
GerritPublicKeyChecker checker = checkerFactory.create();
assertNoProblems(checker.check(keyA.getPublicKey(), store));
PublicKeyChecker checker = checkerFactory.create()
.setStore(store);
assertNoProblems(checker.check(keyA.getPublicKey()));
assertProblems(
checker.check(keyB.getPublicKey(), store), Status.BAD,
checker.check(keyB.getPublicKey()), Status.BAD,
"Key is expired");
assertNoProblems(checker.check(keyC.getPublicKey(), store));
assertNoProblems(checker.check(keyD.getPublicKey(), store));
assertNoProblems(checker.check(keyC.getPublicKey()));
assertNoProblems(checker.check(keyD.getPublicKey()));
assertProblems(
checker.check(keyE.getPublicKey(), store), Status.BAD,
checker.check(keyE.getPublicKey()), Status.BAD,
"Key is expired",
"No path to a trusted key");
}
@@ -382,8 +391,8 @@ public class GerritPublicKeyCheckerTest {
keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB);
add(keyRingB, addUser("userB"));
GerritPublicKeyChecker checkerA = checkerFactory.create(user);
assertProblems(checkerA.check(keyA.getPublicKey(), store), Status.OK,
PublicKeyChecker checkerA = checkerFactory.create(user, store);
assertProblems(checkerA.check(keyA.getPublicKey()), Status.OK,
"No path to a trusted key",
"Certification by " + keyToString(keyB)
+ " is valid, but key is not trusted",
@@ -424,11 +433,13 @@ public class GerritPublicKeyCheckerTest {
}
private void assertProblems(CheckResult result, Status expectedStatus,
String... expectedProblems) throws Exception {
checkArgument(expectedProblems.length > 0);
String first, String... rest) throws Exception {
List<String> expectedProblems = new ArrayList<>();
expectedProblems.add(first);
expectedProblems.addAll(Arrays.asList(rest));
assertThat(result.getStatus()).isEqualTo(expectedStatus);
assertThat(result.getProblems())
.containsExactly((Object[]) expectedProblems)
.containsExactlyElementsIn(expectedProblems)
.inOrder();
}

View File

@@ -15,6 +15,14 @@
package com.google.gerrit.gpg;
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.keyRevokedByExpiredKeyAfterExpiration;
import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyBeforeExpiration;
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;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
@@ -25,11 +33,15 @@ import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyG;
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 com.google.gerrit.gpg.testutil.TestKeys;
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;
@@ -41,9 +53,15 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public class PublicKeyCheckerTest {
@Rule
@@ -72,22 +90,33 @@ public class PublicKeyCheckerTest {
@Test
public void validKey() throws Exception {
assertProblems(TestKeys.key1());
assertNoProblems(validKeyWithoutExpiration());
}
@Test
public void keyExpiringInFuture() throws Exception {
assertProblems(TestKeys.key2());
TestKey k = validKeyWithExpiration();
PublicKeyChecker checker = new PublicKeyChecker()
.setStore(store);
assertNoProblems(checker, k);
checker.setEffectiveTime(parseDate("2015-07-10 12:00:00 -0400"));
assertNoProblems(checker, k);
checker.setEffectiveTime(parseDate("2075-07-10 12:00:00 -0400"));
assertProblems(checker, k, "Key is expired");
}
@Test
public void expiredKey() throws Exception {
assertProblems(TestKeys.key3(), "Key is expired");
public void expiredKeyIsExpired() throws Exception {
assertProblems(expiredKey(), "Key is expired");
}
@Test
public void selfRevokedKey() throws Exception {
assertProblems(TestKeys.key4(), "Key is revoked");
public void selfRevokedKeyIsRevoked() throws Exception {
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
@@ -112,10 +141,10 @@ public class PublicKeyCheckerTest {
save();
PublicKeyChecker checker = newChecker(2, kb, kd);
assertProblems(checker, ka);
assertNoProblems(checker, ka);
assertProblems(checker, kb, "Key is expired");
assertProblems(checker, kc);
assertProblems(checker, kd);
assertNoProblems(checker, kc);
assertNoProblems(checker, kd);
assertProblems(checker, ke, "Key is expired", "No path to a trusted key");
}
@@ -166,17 +195,132 @@ public class PublicKeyCheckerTest {
// J trusts I to a depth of 1, so I itself is valid, but I's certification
// of K is not valid.
assertProblems(checker, ki);
assertNoProblems(checker, ki);
assertProblems(checker, kh,
"No path to a trusted key", notTrusted(ki));
}
private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
List<Fingerprint> fps = new ArrayList<>(trusted.length);
for (TestKey k : trusted) {
fps.add(new Fingerprint(k.getPublicKey().getFingerprint()));
@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.
assertNoProblems(kr.getPublicKey());
}
@Test
public void revokedKeyDueToCompromiseRevokesKeyRetroactively()
throws Exception {
TestKey k = add(revokedCompromisedKey());
add(validKeyWithoutExpiration());
save();
String problem =
"Key is revoked (key material has been compromised): test6 compromised";
assertProblems(k, problem);
SimpleDateFormat df = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
PublicKeyChecker checker = new PublicKeyChecker()
.setStore(store)
.setEffectiveTime(df.parse("2010-01-01 12:00:00"));
assertProblems(checker, k, problem);
}
@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");
}
@Test
public void revokedKeyDueToNoLongerBeingUsedDoesNotRevokeKeyRetroactively()
throws Exception {
TestKey k = add(revokedNoLongerUsedKey());
add(validKeyWithoutExpiration());
save();
assertProblems(k,
"Key is revoked (retired and no longer valid): test7 not used");
PublicKeyChecker checker = new PublicKeyChecker()
.setStore(store)
.setEffectiveTime(parseDate("2010-01-01 12:00:00 -0400"));
assertNoProblems(checker, k);
}
@Test
public void keyRevokedByExpiredKeyAfterExpirationIsNotRevoked()
throws Exception {
TestKey k = add(keyRevokedByExpiredKeyAfterExpiration());
add(expiredKey());
save();
PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
assertNoProblems(checker, k);
}
@Test
public void keyRevokedByExpiredKeyBeforeExpirationIsRevoked()
throws Exception {
TestKey k = add(keyRevokedByExpiredKeyBeforeExpiration());
add(expiredKey());
save();
PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
assertProblems(checker, k,
"Key is revoked (retired and no longer valid): test9 not used");
// Set time between key creation and revocation.
checker.setEffectiveTime(parseDate("2005-08-01 13:00:00 -0400"));
assertNoProblems(checker, k);
}
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 new PublicKeyChecker(maxTrustDepth, fps);
return PGPPublicKeyRing.insertPublicKey(kr, k);
}
private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
Map<Long, Fingerprint> fps = new HashMap<>();
for (TestKey k : trusted) {
Fingerprint fp = new Fingerprint(k.getPublicKey().getFingerprint());
fps.put(fp.getId(), fp);
}
return new PublicKeyChecker()
.enableTrust(maxTrustDepth, fps)
.setStore(store);
}
private TestKey add(TestKey k) {
@@ -201,18 +345,53 @@ public class PublicKeyCheckerTest {
}
private void assertProblems(PublicKeyChecker checker, TestKey k,
String... expected) {
CheckResult result = checker.check(k.getPublicKey(), store);
assertEquals(Arrays.asList(expected), result.getProblems());
String first, String... rest) {
CheckResult result = checker.setStore(store)
.check(k.getPublicKey());
assertEquals(list(first, rest), result.getProblems());
}
private void assertProblems(TestKey tk, String... expected) throws Exception {
CheckResult result = new PublicKeyChecker().check(tk.getPublicKey(), store);
assertEquals(Arrays.asList(expected), result.getProblems());
private void assertNoProblems(PublicKeyChecker checker, TestKey k) {
CheckResult result = checker.setStore(store)
.check(k.getPublicKey());
assertEquals(Collections.emptyList(), result.getProblems());
}
private void assertProblems(TestKey tk, String first, String... rest) {
assertProblems(tk.getPublicKey(), first, rest);
}
private void assertNoProblems(TestKey tk) {
assertNoProblems(tk.getPublicKey());
}
private void assertProblems(PGPPublicKey k, String first, String... rest) {
CheckResult result = new PublicKeyChecker()
.setStore(store)
.check(k);
assertEquals(list(first, rest), result.getProblems());
}
private void assertNoProblems(PGPPublicKey k) {
CheckResult result = new PublicKeyChecker()
.setStore(store)
.check(k);
assertEquals(Collections.emptyList(), result.getProblems());
}
private static String notTrusted(TestKey k) {
return "Certification by " + keyToString(k.getPublicKey())
+ " is valid, but key is not trusted";
}
private static Date parseDate(String str) throws Exception {
return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss Z").parse(str);
}
private static List<String> list(String first, String[] rest) {
List<String> all = new ArrayList<>();
all.add(first);
all.addAll(Arrays.asList(rest));
return all;
}
}

View File

@@ -18,13 +18,15 @@ 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.keyObjectId;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.gpg.testutil.TestKeys;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -61,13 +63,13 @@ public class PublicKeyStoreTest {
@Test
public void testKeyIdToString() throws Exception {
PGPPublicKey key = TestKeys.key1().getPublicKey();
PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
assertEquals("46328A8C", keyIdToString(key.getKeyID()));
}
@Test
public void testKeyToString() throws Exception {
PGPPublicKey key = TestKeys.key1().getPublicKey();
PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
assertEquals("46328A8C Testuser One <test1@example.com>"
+ " (04AE A7ED 2F82 1133 E5B1 28D1 ED06 25DC 4632 8A8C)",
keyToString(key));
@@ -75,7 +77,7 @@ public class PublicKeyStoreTest {
@Test
public void testKeyObjectId() throws Exception {
PGPPublicKey key = TestKeys.key1().getPublicKey();
PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
String objId = keyObjectId(key.getKeyID()).name();
assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
assertEquals(keyIdToString(key.getKeyID()).toLowerCase(),
@@ -84,13 +86,13 @@ public class PublicKeyStoreTest {
@Test
public void testGet() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key1 = validKeyWithoutExpiration();
tr.branch(REFS_GPG_KEYS)
.commit()
.add(keyObjectId(key1.getKeyId()).name(),
key1.getPublicKeyArmored())
.create();
TestKey key2 = TestKeys.key2();
TestKey key2 = validKeyWithExpiration();
tr.branch(REFS_GPG_KEYS)
.commit()
.add(keyObjectId(key2.getKeyId()).name(),
@@ -103,8 +105,8 @@ public class PublicKeyStoreTest {
@Test
public void testGetMultiple() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key2 = TestKeys.key2();
TestKey key1 = validKeyWithoutExpiration();
TestKey key2 = validKeyWithExpiration();
tr.branch(REFS_GPG_KEYS)
.commit()
.add(keyObjectId(key1.getKeyId()).name(),
@@ -117,8 +119,8 @@ public class PublicKeyStoreTest {
@Test
public void save() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key2 = TestKeys.key2();
TestKey key1 = validKeyWithoutExpiration();
TestKey key2 = validKeyWithExpiration();
store.add(key1.getPublicKeyRing());
store.add(key2.getPublicKeyRing());
@@ -130,8 +132,8 @@ public class PublicKeyStoreTest {
@Test
public void saveAppendsToExistingList() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key2 = TestKeys.key2();
TestKey key1 = validKeyWithoutExpiration();
TestKey key2 = validKeyWithExpiration();
tr.branch(REFS_GPG_KEYS)
.commit()
// Mismatched for this key ID, but we can still read it out.
@@ -161,7 +163,7 @@ public class PublicKeyStoreTest {
@Test
public void updateExisting() throws Exception {
TestKey key5 = TestKeys.key5();
TestKey key5 = validKeyWithSecondUserId();
PGPPublicKeyRing keyRing = key5.getPublicKeyRing();
PGPPublicKey key = keyRing.getPublicKey();
store.add(keyRing);
@@ -185,7 +187,7 @@ public class PublicKeyStoreTest {
@Test
public void remove() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key1 = validKeyWithoutExpiration();
store.add(key1.getPublicKeyRing());
assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
assertKeys(key1.getKeyId(), key1);
@@ -197,11 +199,11 @@ public class PublicKeyStoreTest {
@Test
public void removeNonexisting() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key1 = validKeyWithoutExpiration();
store.add(key1.getPublicKeyRing());
assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
TestKey key2 = TestKeys.key2();
TestKey key2 = validKeyWithExpiration();
store.remove(key2.getPublicKey().getFingerprint());
assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
assertKeys(key1.getKeyId(), key1);
@@ -209,7 +211,7 @@ public class PublicKeyStoreTest {
@Test
public void addThenRemove() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key1 = validKeyWithoutExpiration();
store.add(key1.getPublicKeyRing());
store.remove(key1.getPublicKey().getFingerprint());
assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));

View File

@@ -14,24 +14,28 @@
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;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.gpg.testutil.TestKeys;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;
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;
@@ -44,25 +48,34 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public class PushCertificateCheckerTest {
private TestRepository<?> tr;
private InMemoryRepository repo;
private PublicKeyStore store;
private SignedPushConfig signedPushConfig;
private PushCertificateChecker checker;
@Before
public void setUp() throws Exception {
TestKey key1 = TestKeys.key1();
TestKey key3 = TestKeys.key3();
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();
TestKey key1 = validKeyWithoutExpiration();
TestKey key3 = expiredKey();
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);
@@ -70,41 +83,45 @@ public class PushCertificateCheckerTest {
}
private PushCertificateChecker newChecker(boolean checkNonce) {
return new PushCertificateChecker(new PublicKeyChecker(), checkNonce) {
PublicKeyChecker keyChecker = new PublicKeyChecker().setStore(store);
return new PushCertificateChecker(keyChecker) {
@Override
protected Repository getRepository() {
return tr.getRepository();
return repo;
}
@Override
protected boolean shouldClose(Repository repo) {
return false;
}
};
}.setCheckNonce(checkNonce);
}
@Test
public void validCert() throws Exception {
PushCertificate cert = newSignedCert(validNonce(), TestKeys.key1());
assertProblems(cert);
PushCertificate cert =
newSignedCert(validNonce(), validKeyWithoutExpiration());
assertNoProblems(cert);
}
@Test
public void invalidNonce() throws Exception {
PushCertificate cert = newSignedCert("invalid-nonce", TestKeys.key1());
PushCertificate cert =
newSignedCert("invalid-nonce", validKeyWithoutExpiration());
assertProblems(cert, "Invalid nonce");
}
@Test
public void invalidNonceNotChecked() throws Exception {
checker = newChecker(false);
PushCertificate cert = newSignedCert("invalid-nonce", TestKeys.key1());
assertProblems(cert);
PushCertificate cert =
newSignedCert("invalid-nonce", validKeyWithoutExpiration());
assertNoProblems(cert);
}
@Test
public void missingKey() throws Exception {
TestKey key2 = TestKeys.key2();
TestKey key2 = validKeyWithExpiration();
PushCertificate cert = newSignedCert(validNonce(), key2);
assertProblems(cert,
"No public keys found for key ID " + keyIdToString(key2.getKeyId()));
@@ -112,20 +129,34 @@ public class PushCertificateCheckerTest {
@Test
public void invalidKey() throws Exception {
TestKey key3 = TestKeys.key3();
TestKey key3 = expiredKey();
PushCertificate cert = newSignedCert(validNonce(), key3);
assertProblems(cert,
"Invalid public key " + keyToString(key3.getPublicKey())
+ ":\n Key is expired");
}
@Test
public void signatureByExpiredKeyBeforeExpiration() throws Exception {
TestKey key3 = expiredKey();
Date now = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss Z")
.parse("2005-07-10 12:00:00 -0400");
PushCertificate cert = newSignedCert(validNonce(), key3, now);
assertNoProblems(cert);
}
private String validNonce() {
return signedPushConfig.getNonceGenerator()
.createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
.createNonce(repo, System.currentTimeMillis() / 1000);
}
private PushCertificate newSignedCert(String nonce, TestKey signingKey)
throws Exception {
return newSignedCert(nonce, signingKey, null);
}
private PushCertificate newSignedCert(String nonce, TestKey signingKey,
Date now) throws Exception {
PushCertificateIdent ident = new PushCertificateIdent(
signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60);
String payload = "certificate version 0.1\n"
@@ -139,6 +170,14 @@ public class PushCertificateCheckerTest {
PGPSignatureGenerator gen = new PGPSignatureGenerator(
new BcPGPContentSignerBuilder(
signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1));
if (now != null) {
PGPSignatureSubpacketGenerator subGen =
new PGPSignatureSubpacketGenerator();
subGen.setSignatureCreationTime(false, now);
gen.setHashedSubpackets(subGen.generate());
}
gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey());
gen.update(payload.getBytes(UTF_8));
PGPSignature sig = gen.generate();
@@ -153,13 +192,21 @@ public class PushCertificateCheckerTest {
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);
}
private void assertProblems(PushCertificate cert, String... expected)
throws Exception {
private void assertProblems(PushCertificate cert, String first,
String... rest) throws Exception {
List<String> expected = new ArrayList<>();
expected.add(first);
expected.addAll(Arrays.asList(rest));
CheckResult result = checker.check(cert).getCheckResult();
assertEquals(Arrays.asList(expected), result.getProblems());
assertEquals(expected, result.getProblems());
}
private void assertNoProblems(PushCertificate cert) {
CheckResult result = checker.check(cert).getCheckResult();
assertEquals(Collections.emptyList(), result.getProblems());
}
}

View File

@@ -20,9 +20,9 @@ import com.google.common.collect.ImmutableList;
public class TestKeys {
public static ImmutableList<TestKey> allValidKeys() {
return ImmutableList.of(
TestKeys.key1(),
TestKeys.key2(),
TestKeys.key5());
validKeyWithoutExpiration(),
validKeyWithExpiration(),
validKeyWithSecondUserId());
}
/**
@@ -35,7 +35,7 @@ public class TestKeys {
* sub 2048R/F0AF69C0 2015-07-08
* </pre>
*/
public static TestKey key1() {
public static TestKey validKeyWithoutExpiration() {
return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
+ "\n"
@@ -135,7 +135,7 @@ public class TestKeys {
* sub 2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
* </pre>
*/
public static final TestKey key2() {
public static final TestKey validKeyWithExpiration() {
return new TestKey(
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
@@ -236,7 +236,7 @@ public class TestKeys {
* uid Testuser Three &lt;test3@example.com&gt;
* </pre>
*/
public static final TestKey key3() {
public static final TestKey expiredKey() {
return new TestKey(
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
@@ -337,7 +337,7 @@ public class TestKeys {
* uid Testuser Four &lt;test4@example.com&gt;
* </pre>
*/
public static final TestKey key4() {
public static final TestKey selfRevokedKey() {
return new TestKey(
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
@@ -445,7 +445,7 @@ public class TestKeys {
* sub 2048R/C781A9E3 2015-07-30
* </pre>
*/
public static TestKey key5() {
public static TestKey validKeyWithSecondUserId() {
return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
+ "\n"
@@ -542,6 +542,487 @@ public class TestKeys {
+ "-----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 &lt;test6@example.com&gt;
* </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"
+ "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\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 &lt;test7@example.com&gt;
* </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");
}
/**
* Key revoked by an expired key, after that key's expiration.
* <p>
* Revoked by {@link #expiredKey()}.
*
* <pre>
* pub 2048R/78BF7D7E 2005-08-01 [revoked: 2015-10-20]
* Key fingerprint = 916F AB22 5BE7 7585 F59A 994C 001A DF8B 78BF 7D7E
* uid Testuser Eight &lt;test8@example.com&gt;
* </pre>
*/
public static TestKey keyRevokedByExpiredKeyAfterExpiration() throws Exception {
return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
+ "\n"
+ "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\n"
+ "0d0PyzZT3676mQwmFoebMR9vACAeHjvDxD4F\n"
+ "=ihWb\n"
+ "-----END PGP PUBLIC KEY BLOCK-----\n",
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
+ "\n"
+ "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\n"
+ "DCYWh5sxH28AIB4eO8PEPgU=\n"
+ "=cSfw\n"
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
}
/**
* Key revoked by an expired key, before that key's expiration.
* <p>
* Revoked by {@link #expiredKey()}.
*
* <pre>
* pub 2048R/C43BF2E1 2005-08-01 [revoked: 2005-08-01]
* Key fingerprint = 916D 6AD6 36A5 CBA6 B5A6 7274 6040 8661 C43B F2E1
* uid Testuser Nine &lt;test9@example.com&gt;
* </pre>
*/
public static TestKey keyRevokedByExpiredKeyBeforeExpiration() throws Exception {
return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
+ "\n"
+ "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\n"
+ "REW68wsnWjwTtrPG8bqo6cCsOzqGYVt81eU=\n"
+ "=FnZg\n"
+ "-----END PGP PUBLIC KEY BLOCK-----\n",
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+ "Version: GnuPG v1\n"
+ "\n"
+ "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\n"
+ "s8bxuqjpwKw7OoZhW3zV5Q==\n"
+ "=JxsF\n"
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
}
}

View File

@@ -17,7 +17,6 @@ package com.google.gerrit.server.query.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;