Refactor signed push support
Separate out classes for reading public keys from a repository; checking the public keys; and checking push certificates. This minimizes the amount of Gerrit-specific code which needs to go in the actual pre-receive hook, and makes testing much more feasible. Add lots of tests. When verifying a signature, iterate over all possible keys to find one that can verify the signature, and only then check the key. Move these various classes into a "gpg" subpackage. We use gpg rather than pgp (as Bouncy Castle does) for consistency with C git, which refers to its OpenPGP support consistently as "gpg". Written with a minimum of dependencies (e.g. no Guava or AutoValue) for ease of upstreaming into JGit. Change-Id: I88588a2d33a5e9ea3a75900a0db6ab07269826e8
This commit is contained in:
@@ -59,7 +59,7 @@ public class RefNames {
|
||||
|
||||
/**
|
||||
* Special ref for GPG public keys used by {@link
|
||||
* com.google.gerrit.server.git.SignedPushPreReceiveHook}.
|
||||
* com.google.gerrit.server.git.gpg.SignedPushPreReceiveHook}.
|
||||
*/
|
||||
public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
|
||||
|
||||
|
@@ -77,9 +77,9 @@ import com.google.gerrit.server.git.GitModule;
|
||||
import com.google.gerrit.server.git.MergeUtil;
|
||||
import com.google.gerrit.server.git.NotesBranchUtil;
|
||||
import com.google.gerrit.server.git.ReceivePackInitializer;
|
||||
import com.google.gerrit.server.git.SignedPushModule;
|
||||
import com.google.gerrit.server.git.TagCache;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
import com.google.gerrit.server.git.gpg.SignedPushModule;
|
||||
import com.google.gerrit.server.git.validators.CommitValidationListener;
|
||||
import com.google.gerrit.server.git.validators.CommitValidators;
|
||||
import com.google.gerrit.server.git.validators.MergeValidationListener;
|
||||
|
@@ -30,7 +30,7 @@ import com.google.gerrit.server.account.Realm;
|
||||
import com.google.gerrit.server.change.ArchiveFormat;
|
||||
import com.google.gerrit.server.change.GetArchive;
|
||||
import com.google.gerrit.server.change.Submit;
|
||||
import com.google.gerrit.server.git.SignedPushModule;
|
||||
import com.google.gerrit.server.git.gpg.SignedPushModule;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
|
@@ -1,325 +0,0 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git;
|
||||
|
||||
import static org.bouncycastle.openpgp.PGPSignature.CERTIFICATION_REVOCATION;
|
||||
import static org.bouncycastle.openpgp.PGPSignature.DEFAULT_CERTIFICATION;
|
||||
import static org.bouncycastle.openpgp.PGPSignature.POSITIVE_CERTIFICATION;
|
||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
import com.google.gerrit.server.config.AllUsersName;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.notes.Note;
|
||||
import org.eclipse.jgit.notes.NoteMap;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.transport.PreReceiveHook;
|
||||
import org.eclipse.jgit.transport.PushCertificate;
|
||||
import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
|
||||
import org.eclipse.jgit.transport.PushCertificateIdent;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
import org.eclipse.jgit.transport.ReceivePack;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Pre-receive hook to validate signed pushes.
|
||||
* <p>
|
||||
* If configured, prior to processing any push using {@link ReceiveCommits},
|
||||
* requires that any push certificate present must be valid.
|
||||
*/
|
||||
@Singleton
|
||||
public class SignedPushPreReceiveHook implements PreReceiveHook {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SignedPushPreReceiveHook.class);
|
||||
|
||||
private final GitRepositoryManager repoManager;
|
||||
private final AllUsersName allUsers;
|
||||
|
||||
@Inject
|
||||
public SignedPushPreReceiveHook(
|
||||
GitRepositoryManager repoManager,
|
||||
AllUsersName allUsers) {
|
||||
this.repoManager = repoManager;
|
||||
this.allUsers = allUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreReceive(ReceivePack rp,
|
||||
Collection<ReceiveCommand> commands) {
|
||||
try (Writer msgOut = new OutputStreamWriter(rp.getMessageOutputStream())) {
|
||||
PushCertificate cert = rp.getPushCertificate();
|
||||
if (cert == null) {
|
||||
return;
|
||||
}
|
||||
if (cert.getNonceStatus() != NonceStatus.OK) {
|
||||
rejectInvalid(commands);
|
||||
return;
|
||||
}
|
||||
verifySignature(cert, commands, msgOut);
|
||||
} catch (IOException e) {
|
||||
log.error("Error verifying push certificate", e);
|
||||
reject(commands, "push cert error");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifySignature(PushCertificate cert,
|
||||
Collection<ReceiveCommand> commands, Writer msgOut) throws IOException {
|
||||
PGPSignature sig = readSignature(cert);
|
||||
if (sig == null) {
|
||||
msgOut.write("Invalid signature format\n");
|
||||
rejectInvalid(commands);
|
||||
return;
|
||||
}
|
||||
PGPPublicKey key = readPublicKey(sig.getKeyID(), cert.getPusherIdent());
|
||||
if (key == null) {
|
||||
msgOut.write("No valid public key found for ID "
|
||||
+ keyIdToString(sig.getKeyID()) + "\n");
|
||||
rejectInvalid(commands);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
sig.init(new BcPGPContentVerifierBuilderProvider(), key);
|
||||
sig.update(Constants.encode(cert.toText()));
|
||||
if (!sig.verify()) {
|
||||
msgOut.write("Push certificate signature does not match\n");
|
||||
rejectInvalid(commands);
|
||||
}
|
||||
return;
|
||||
} catch (PGPException e) {
|
||||
msgOut.write(
|
||||
"Push certificate verification error: " + e.getMessage() + "\n");
|
||||
rejectInvalid(commands);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private PGPSignature readSignature(PushCertificate cert) throws IOException {
|
||||
ArmoredInputStream in = new ArmoredInputStream(
|
||||
new ByteArrayInputStream(Constants.encode(cert.getSignature())));
|
||||
PGPObjectFactory factory = new BcPGPObjectFactory(in);
|
||||
PGPSignature sig = null;
|
||||
|
||||
Object obj;
|
||||
while ((obj = factory.nextObject()) != null) {
|
||||
if (!(obj instanceof PGPSignatureList)) {
|
||||
log.error("Unexpected packet in push cert: {}",
|
||||
obj.getClass().getSimpleName());
|
||||
return null;
|
||||
}
|
||||
if (sig != null) {
|
||||
log.error("Multiple signature packets found in push cert");
|
||||
return null;
|
||||
}
|
||||
PGPSignatureList sigs = (PGPSignatureList) obj;
|
||||
if (sigs.size() != 1) {
|
||||
log.error("Expected 1 signature in push cert, found {}", sigs.size());
|
||||
return null;
|
||||
}
|
||||
sig = sigs.get(0);
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
|
||||
private PGPPublicKey readPublicKey(long keyId,
|
||||
PushCertificateIdent expectedIdent) throws IOException {
|
||||
try (Repository repo = repoManager.openRepository(allUsers);
|
||||
RevWalk rw = new RevWalk(repo)) {
|
||||
Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_GPG_KEYS);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
NoteMap notes = NoteMap.read(
|
||||
rw.getObjectReader(), rw.parseCommit(ref.getObjectId()));
|
||||
Note note = notes.getNote(keyObjectId(keyId));
|
||||
if (note == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (InputStream objIn =
|
||||
rw.getObjectReader().open(note.getData(), OBJ_BLOB).openStream();
|
||||
ArmoredInputStream in = new ArmoredInputStream(objIn)) {
|
||||
PGPObjectFactory factory = new BcPGPObjectFactory(in);
|
||||
PGPPublicKey matched = null;
|
||||
Object obj;
|
||||
while ((obj = factory.nextObject()) != null) {
|
||||
if (!(obj instanceof PGPPublicKeyRing)) {
|
||||
// TODO(dborowitz): Support assertions signed by a trusted key.
|
||||
log.info("Ignoring {} packet in {}",
|
||||
obj.getClass().getSimpleName(), note.getName());
|
||||
continue;
|
||||
}
|
||||
PGPPublicKeyRing keyRing = (PGPPublicKeyRing) obj;
|
||||
PGPPublicKey key = keyRing.getPublicKey(keyId);
|
||||
if (key == null) {
|
||||
log.warn("Public key ring in {} does not contain key ID {}",
|
||||
note.getName(), keyObjectId(keyId));
|
||||
continue;
|
||||
}
|
||||
if (matched != null) {
|
||||
// TODO(dborowitz): Try all keys.
|
||||
log.warn("Ignoring key with duplicate ID: {}", toString(key));
|
||||
continue;
|
||||
}
|
||||
if (!verifyPublicKey(key, expectedIdent)) {
|
||||
continue;
|
||||
}
|
||||
matched = key;
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyPublicKey(PGPPublicKey key,
|
||||
PushCertificateIdent ident) {
|
||||
if (key.isRevoked()) {
|
||||
// TODO(dborowitz): isRevoked is overeager:
|
||||
// http://www.bouncycastle.org/jira/browse/BJB-45
|
||||
log.warn("Key is revoked: {}", toString(key));
|
||||
return false;
|
||||
}
|
||||
|
||||
long validSecs = key.getValidSeconds();
|
||||
if (validSecs != 0) {
|
||||
DateTime created = new DateTime(key.getCreationTime());
|
||||
DateTime now = new DateTime(TimeUtil.nowTs());
|
||||
if (new Duration(created, now).getStandardSeconds() > validSecs) {
|
||||
log.warn("Key is expired: {}", toString(key));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return verifyPublicKeyCertifications(key, ident);
|
||||
}
|
||||
|
||||
private boolean verifyPublicKeyCertifications(PGPPublicKey key,
|
||||
PushCertificateIdent ident) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSignature> sigs = key.getSignaturesForID(ident.getUserId());
|
||||
if (sigs == null) {
|
||||
sigs = Collections.emptyIterator();
|
||||
}
|
||||
boolean valid = false;
|
||||
boolean revoked = false;
|
||||
try {
|
||||
while (sigs.hasNext()) {
|
||||
PGPSignature sig = sigs.next();
|
||||
if (sig.getKeyID() != key.getKeyID()) {
|
||||
// TODO(dborowitz): Support certifications by other trusted keys?
|
||||
continue;
|
||||
} else if (sig.getSignatureType() != DEFAULT_CERTIFICATION
|
||||
&& sig.getSignatureType() != POSITIVE_CERTIFICATION
|
||||
&& sig.getSignatureType() != CERTIFICATION_REVOCATION) {
|
||||
continue;
|
||||
}
|
||||
sig.init(new BcPGPContentVerifierBuilderProvider(), key);
|
||||
if (sig.verifyCertification(ident.getUserId(), key)) {
|
||||
if (sig.getSignatureType() == CERTIFICATION_REVOCATION) {
|
||||
revoked = true;
|
||||
} else {
|
||||
valid = true;
|
||||
}
|
||||
} else {
|
||||
log.warn("Invalid signature for pusher identity {} in key: {}",
|
||||
ident.getUserId(), toString(key));
|
||||
}
|
||||
}
|
||||
} catch (PGPException e) {
|
||||
log.warn("Error in signature verification for public key", e);
|
||||
}
|
||||
|
||||
if (revoked) {
|
||||
log.warn("Pusher identity {} is revoked in key {}",
|
||||
ident.getUserId(), toString(key));
|
||||
return false;
|
||||
} else if (!valid) {
|
||||
log.warn(
|
||||
"Key does not contain valid certification for pusher identity {}: {}",
|
||||
ident.getUserId(), toString(key));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static ObjectId keyObjectId(long keyId) {
|
||||
// Right-pad key IDs in network byte order to ObjectId length. This allows
|
||||
// us to reuse the fanout code in NoteMap for free. (If we ever fix the
|
||||
// fanout code to work with variable-length byte strings, we will need to
|
||||
// fall back to this key format during a transition period.)
|
||||
ByteBuffer buf = ByteBuffer.wrap(new byte[Constants.OBJECT_ID_LENGTH]);
|
||||
buf.putLong(keyId);
|
||||
return ObjectId.fromRaw(buf.array());
|
||||
}
|
||||
|
||||
static String toString(PGPPublicKey key) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<String> it = key.getUserIDs();
|
||||
ByteBuffer buf = ByteBuffer.wrap(key.getFingerprint());
|
||||
return String.format(
|
||||
"%s %s(%04X %04X %04X %04X %04X %04X %04X %04X %04X %04X)",
|
||||
keyIdToString(key.getKeyID()),
|
||||
it.hasNext() ? it.next() + " " : "",
|
||||
buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
|
||||
buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
|
||||
buf.getShort(), buf.getShort());
|
||||
}
|
||||
|
||||
private static void reject(Collection<ReceiveCommand> commands,
|
||||
String reason) {
|
||||
for (ReceiveCommand cmd : commands) {
|
||||
if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
|
||||
cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String keyIdToString(long keyId) {
|
||||
// Match key ID format from gpg --list-keys.
|
||||
return String.format("%08X", (int) keyId);
|
||||
}
|
||||
|
||||
private static void rejectInvalid(Collection<ReceiveCommand> commands) {
|
||||
reject(commands, "invalid push cert");
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Result of checking an object like a key or signature. */
|
||||
public class CheckResult {
|
||||
private final List<String> problems;
|
||||
|
||||
CheckResult(String... problems) {
|
||||
this(Arrays.asList(problems));
|
||||
}
|
||||
|
||||
CheckResult(List<String> problems) {
|
||||
this.problems = Collections.unmodifiableList(new ArrayList<>(problems));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the result is entirely ok, i.e. has passed any verification
|
||||
* or validation checks.
|
||||
*/
|
||||
public boolean isOk() {
|
||||
return problems.isEmpty();
|
||||
}
|
||||
|
||||
/** @return any problems encountered during checking. */
|
||||
public List<String> getProblems() {
|
||||
return problems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder(getClass().getSimpleName())
|
||||
.append('[');
|
||||
for (int i = 0; i < problems.size(); i++) {
|
||||
if (i > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(problems.get(i));
|
||||
}
|
||||
return sb.append(']').toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
|
||||
import static org.bouncycastle.openpgp.PGPSignature.CERTIFICATION_REVOCATION;
|
||||
import static org.bouncycastle.openpgp.PGPSignature.DEFAULT_CERTIFICATION;
|
||||
import static org.bouncycastle.openpgp.PGPSignature.POSITIVE_CERTIFICATION;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/** Checker for GPG public keys for use in a push certificate. */
|
||||
public class PublicKeyChecker {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(PublicKeyChecker.class);
|
||||
|
||||
/**
|
||||
* Check a public key.
|
||||
*
|
||||
* @param key the public key.
|
||||
* @param expectedKeyId the key ID that the caller expects.
|
||||
* @param expectedUserId a user ID that the caller expects to be present and
|
||||
* correct.
|
||||
*/
|
||||
public final CheckResult check(PGPPublicKey key, long expectedKeyId,
|
||||
String expectedUserId) {
|
||||
List<String> problems = new ArrayList<>();
|
||||
if (key.getKeyID() != expectedKeyId) {
|
||||
problems.add(
|
||||
"Public key does not match ID " + keyIdToString(expectedKeyId));
|
||||
}
|
||||
if (key.isRevoked()) {
|
||||
// TODO(dborowitz): isRevoked is overeager:
|
||||
// http://www.bouncycastle.org/jira/browse/BJB-45
|
||||
problems.add("Key is revoked");
|
||||
}
|
||||
|
||||
long validSecs = key.getValidSeconds();
|
||||
if (validSecs != 0) {
|
||||
long createdSecs = key.getCreationTime().getTime() / 1000;
|
||||
long nowSecs = System.currentTimeMillis() / 1000;
|
||||
if (nowSecs - createdSecs > validSecs) {
|
||||
problems.add("Key is expired");
|
||||
}
|
||||
}
|
||||
checkCertifications(key, expectedUserId, problems);
|
||||
checkCustom(key, expectedKeyId, expectedUserId, problems);
|
||||
return new CheckResult(problems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform custom checks.
|
||||
* <p>
|
||||
* Default implementation does nothing, but may be overridden by subclasses.
|
||||
*
|
||||
* @param key the public key.
|
||||
* @param expectedKeyId the key ID that the caller expects.
|
||||
* @param expectedUserId a user ID that the caller expects to be present and
|
||||
* correct.
|
||||
* @param problems list to which any problems should be added.
|
||||
*/
|
||||
public void checkCustom(PGPPublicKey key, long expectedKeyId,
|
||||
String expectedUserId, List<String> problems) {
|
||||
// Default implementation does nothing.
|
||||
}
|
||||
|
||||
// TODO(dborowitz): Remove some/all of these checks.
|
||||
private static void checkCertifications(PGPPublicKey key, String userId,
|
||||
List<String> problems) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
|
||||
if (sigs == null) {
|
||||
sigs = Collections.emptyIterator();
|
||||
}
|
||||
boolean ok = false;
|
||||
boolean revoked = false;
|
||||
try {
|
||||
while (sigs.hasNext()) {
|
||||
PGPSignature sig = sigs.next();
|
||||
if (sig.getKeyID() != key.getKeyID()) {
|
||||
// TODO(dborowitz): Support certifications by other trusted keys?
|
||||
continue;
|
||||
} else if (sig.getSignatureType() != DEFAULT_CERTIFICATION
|
||||
&& sig.getSignatureType() != POSITIVE_CERTIFICATION
|
||||
&& sig.getSignatureType() != CERTIFICATION_REVOCATION) {
|
||||
continue;
|
||||
}
|
||||
sig.init(new BcPGPContentVerifierBuilderProvider(), key);
|
||||
if (sig.verifyCertification(userId, key)) {
|
||||
if (sig.getSignatureType() == CERTIFICATION_REVOCATION) {
|
||||
revoked = true;
|
||||
} else {
|
||||
ok = true;
|
||||
}
|
||||
} else {
|
||||
problems.add("Invalid signature for User ID " + userId);
|
||||
}
|
||||
}
|
||||
} catch (PGPException e) {
|
||||
problems.add("Error in certifications");
|
||||
log.warn("Error in certification verification for public key: "
|
||||
+ keyToString(key), e);
|
||||
}
|
||||
|
||||
if (revoked) {
|
||||
problems.add("User ID " + userId + " is revoked");
|
||||
} else if (!ok) {
|
||||
problems.add("No certification for User ID " + userId);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,170 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.notes.Note;
|
||||
import org.eclipse.jgit.notes.NoteMap;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Store of GPG public keys in git notes.
|
||||
* <p>
|
||||
* Keys are stored in filenames based on their hex key ID, padded out to 40
|
||||
* characters to match the length of a SHA-1. (This is to easily reuse existing
|
||||
* fanout code in {@link NoteMap}, and may be changed later after an appropriate
|
||||
* transition.)
|
||||
* <p>
|
||||
* The contents of each file is an ASCII armored stream containing one or more
|
||||
* public key rings matching the ID. Multiple keys are supported because forging
|
||||
* a key ID is possible, but such a key cannot be used to verify signatures
|
||||
* produced with the correct key.
|
||||
* <p>
|
||||
* No additional checks are performed on the key after reading; callers should
|
||||
* only trust keys after checking with a {@link PublicKeyChecker}.
|
||||
*/
|
||||
public class PublicKeyStore implements AutoCloseable {
|
||||
private final Repository repo;
|
||||
private ObjectReader reader;
|
||||
private NoteMap notes;
|
||||
|
||||
/** @param repo repository to read keys from. */
|
||||
public PublicKeyStore(Repository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
reader = null;
|
||||
notes = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void load() throws IOException {
|
||||
close();
|
||||
reader = repo.newObjectReader();
|
||||
|
||||
Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_GPG_KEYS);
|
||||
if (ref == null) {
|
||||
return;
|
||||
}
|
||||
try (RevWalk rw = new RevWalk(reader)) {
|
||||
notes = NoteMap.read(reader, rw.parseCommit(ref.getObjectId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read public keys with the given key ID.
|
||||
* <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 keyId key ID.
|
||||
* @return any keys found that could be successfully parsed.
|
||||
* @throws PGPException if an error occurred parsing the key data.
|
||||
* @throws IOException if an error occurred reading the repository data.
|
||||
*/
|
||||
public PGPPublicKeyRingCollection get(long keyId)
|
||||
throws PGPException, IOException {
|
||||
if (reader == null) {
|
||||
load();
|
||||
}
|
||||
if (notes == null) {
|
||||
return empty();
|
||||
}
|
||||
Note note = notes.getNote(keyObjectId(keyId));
|
||||
if (note == null) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
List<PGPPublicKeyRing> keys = new ArrayList<>();
|
||||
try (InputStream in = reader.open(note.getData(), OBJ_BLOB).openStream()) {
|
||||
while (true) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<Object> it =
|
||||
new BcPGPObjectFactory(new ArmoredInputStream(in)).iterator();
|
||||
if (!it.hasNext()) {
|
||||
break;
|
||||
}
|
||||
Object obj = it.next();
|
||||
if (obj instanceof PGPPublicKeyRing) {
|
||||
keys.add((PGPPublicKeyRing) obj);
|
||||
}
|
||||
checkState(!it.hasNext(),
|
||||
"expected one PGP object per ArmoredInputStream");
|
||||
}
|
||||
return new PGPPublicKeyRingCollection(keys);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dborowitz): put method.
|
||||
|
||||
private static PGPPublicKeyRingCollection empty()
|
||||
throws PGPException, IOException {
|
||||
return new PGPPublicKeyRingCollection(
|
||||
Collections.<PGPPublicKeyRing> emptyList());
|
||||
}
|
||||
|
||||
static String keyToString(PGPPublicKey key) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<String> it = key.getUserIDs();
|
||||
ByteBuffer buf = ByteBuffer.wrap(key.getFingerprint());
|
||||
return String.format(
|
||||
"%s %s(%04X %04X %04X %04X %04X %04X %04X %04X %04X %04X)",
|
||||
keyIdToString(key.getKeyID()),
|
||||
it.hasNext() ? it.next() + " " : "",
|
||||
buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
|
||||
buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
|
||||
buf.getShort(), buf.getShort());
|
||||
}
|
||||
|
||||
static String keyIdToString(long keyId) {
|
||||
// Match key ID format from gpg --list-keys.
|
||||
return String.format("%08X", (int) keyId);
|
||||
}
|
||||
|
||||
static ObjectId keyObjectId(long keyId) {
|
||||
ByteBuffer buf = ByteBuffer.wrap(new byte[Constants.OBJECT_ID_LENGTH]);
|
||||
buf.putLong(keyId);
|
||||
return ObjectId.fromRaw(buf.array());
|
||||
}
|
||||
}
|
@@ -0,0 +1,165 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.PushCertificate;
|
||||
import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Checker for push certificates. */
|
||||
public abstract class PushCertificateChecker {
|
||||
private final PublicKeyChecker publicKeyChecker;
|
||||
|
||||
protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
|
||||
this.publicKeyChecker = publicKeyChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a push certificate.
|
||||
*
|
||||
* @return result of the check.
|
||||
* @throws PGPException if an error occurred during GPG checks.
|
||||
* @throws IOException if an error occurred reading from the repository.
|
||||
*/
|
||||
public final CheckResult check(PushCertificate cert) throws PGPException, IOException {
|
||||
if (cert.getNonceStatus() != NonceStatus.OK) {
|
||||
return new CheckResult("Invalid nonce");
|
||||
}
|
||||
PGPSignature sig = readSignature(cert);
|
||||
if (sig == null) {
|
||||
return new CheckResult("Invalid signature format");
|
||||
}
|
||||
Repository repo = getRepository();
|
||||
List<String> problems = new ArrayList<>();
|
||||
try (PublicKeyStore store = new PublicKeyStore(repo)) {
|
||||
checkSignature(sig, cert, store.get(sig.getKeyID()), problems);
|
||||
checkCustom(repo, problems);
|
||||
return new CheckResult(problems);
|
||||
} finally {
|
||||
if (shouldClose(repo)) {
|
||||
repo.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the repository that this checker should operate on.
|
||||
* <p>
|
||||
* This method is called once per call to {@link #check(PushCertificate)}.
|
||||
*
|
||||
* @return the repository.
|
||||
* @throws IOException if an error occurred reading the repository.
|
||||
*/
|
||||
protected abstract Repository getRepository() throws IOException;
|
||||
|
||||
/**
|
||||
* @param repo a repository previously returned by {@link #getRepository()}.
|
||||
* @return whether this repository should be closed before returning from
|
||||
* {@link #check(PushCertificate)}.
|
||||
*/
|
||||
protected abstract boolean shouldClose(Repository repo);
|
||||
|
||||
/**
|
||||
* Perform custom checks.
|
||||
* <p>
|
||||
* Default implementation does nothing, but may be overridden by subclasses.
|
||||
*
|
||||
* @param repo a repository previously returned by {@link #getRepository()}.
|
||||
* @param problems list to which any problems should be added.
|
||||
*/
|
||||
protected void checkCustom(Repository repo, List<String> problems) {
|
||||
// Default implementation does nothing.
|
||||
}
|
||||
|
||||
private PGPSignature readSignature(PushCertificate cert) throws IOException {
|
||||
ArmoredInputStream in = new ArmoredInputStream(
|
||||
new ByteArrayInputStream(Constants.encode(cert.getSignature())));
|
||||
PGPObjectFactory factory = new BcPGPObjectFactory(in);
|
||||
Object obj;
|
||||
while ((obj = factory.nextObject()) != null) {
|
||||
if (obj instanceof PGPSignatureList) {
|
||||
PGPSignatureList sigs = (PGPSignatureList) obj;
|
||||
if (!sigs.isEmpty()) {
|
||||
return sigs.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void checkSignature(PGPSignature sig,
|
||||
PushCertificate cert, PGPPublicKeyRingCollection keys,
|
||||
List<String> problems) {
|
||||
List<String> deferredProblems = new ArrayList<>();
|
||||
boolean anyKeys = false;
|
||||
for (PGPPublicKeyRing kr : keys) {
|
||||
PGPPublicKey k = kr.getPublicKey();
|
||||
anyKeys = true;
|
||||
try {
|
||||
sig.init(new BcPGPContentVerifierBuilderProvider(), k);
|
||||
sig.update(Constants.encode(cert.toText()));
|
||||
if (!sig.verify()) {
|
||||
// TODO(dborowitz): Privacy issues with exposing fingerprint/user ID
|
||||
// of keys having the same ID as the pusher's key?
|
||||
deferredProblems.add(
|
||||
"Signature not valid with public key: " + keyToString(k));
|
||||
continue;
|
||||
}
|
||||
CheckResult result = publicKeyChecker.check(
|
||||
k, sig.getKeyID(), cert.getPusherIdent().getUserId());
|
||||
if (result.isOk()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder err = new StringBuilder("Invalid public key (")
|
||||
.append(keyToString(k))
|
||||
.append("):");
|
||||
for (int i = 0; i < result.getProblems().size(); i++) {
|
||||
err.append('\n').append(" ").append(result.getProblems().get(i));
|
||||
}
|
||||
problems.add(err.toString());
|
||||
return;
|
||||
} catch (PGPException e) {
|
||||
deferredProblems.add(
|
||||
"Error checking signature with public key (" + keyToString(k)
|
||||
+ ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (!anyKeys) {
|
||||
problems.add(
|
||||
"No public keys found for Key ID " + keyIdToString(sig.getKeyID()));
|
||||
} else {
|
||||
problems.addAll(deferredProblems);
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,13 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git;
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gerrit.extensions.registration.DynamicSet;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.gerrit.server.git.ReceivePackInitializer;
|
||||
import com.google.gerrit.server.project.ProjectCache;
|
||||
import com.google.gerrit.server.project.ProjectState;
|
||||
import com.google.gerrit.server.util.BouncyCastleUtil;
|
@@ -0,0 +1,98 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import com.google.gerrit.server.config.AllUsersName;
|
||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||
import com.google.gerrit.server.git.ReceiveCommits;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.PreReceiveHook;
|
||||
import org.eclipse.jgit.transport.PushCertificate;
|
||||
import org.eclipse.jgit.transport.ReceiveCommand;
|
||||
import org.eclipse.jgit.transport.ReceivePack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Pre-receive hook to check signed pushes.
|
||||
* <p>
|
||||
* If configured, prior to processing any push using {@link ReceiveCommits},
|
||||
* requires that any push certificate present must be valid.
|
||||
*/
|
||||
@Singleton
|
||||
public class SignedPushPreReceiveHook implements PreReceiveHook {
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(SignedPushPreReceiveHook.class);
|
||||
|
||||
private final GitRepositoryManager repoManager;
|
||||
private final AllUsersName allUsers;
|
||||
|
||||
@Inject
|
||||
public SignedPushPreReceiveHook(
|
||||
GitRepositoryManager repoManager,
|
||||
AllUsersName allUsers) {
|
||||
this.repoManager = repoManager;
|
||||
this.allUsers = allUsers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreReceive(ReceivePack rp,
|
||||
Collection<ReceiveCommand> commands) {
|
||||
try {
|
||||
PushCertificate cert = rp.getPushCertificate();
|
||||
if (cert == null) {
|
||||
return;
|
||||
}
|
||||
PushCertificateChecker checker = new PushCertificateChecker(
|
||||
new PublicKeyChecker()) {
|
||||
@Override
|
||||
protected Repository getRepository() throws IOException {
|
||||
return repoManager.openRepository(allUsers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldClose(Repository repo) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
CheckResult result = checker.check(cert);
|
||||
if (!result.isOk()) {
|
||||
for (String problem : result.getProblems()) {
|
||||
rp.sendMessage(problem);
|
||||
}
|
||||
reject(commands, "invalid push cert");
|
||||
}
|
||||
} catch (PGPException | IOException e) {
|
||||
log.error("Error checking push certificate", e);
|
||||
reject(commands, "push cert error");
|
||||
}
|
||||
}
|
||||
|
||||
private static void reject(Collection<ReceiveCommand> commands,
|
||||
String reason) {
|
||||
for (ReceiveCommand cmd : commands) {
|
||||
if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
|
||||
cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -30,8 +30,8 @@ import com.google.gerrit.server.config.PluginConfig;
|
||||
import com.google.gerrit.server.config.PluginConfigFactory;
|
||||
import com.google.gerrit.server.config.ProjectConfigEntry;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.SignedPushModule;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
import com.google.gerrit.server.git.gpg.SignedPushModule;
|
||||
import com.google.inject.util.Providers;
|
||||
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
|
@@ -1,90 +0,0 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.gerrit.server.git.SignedPushPreReceiveHook.keyIdToString;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
public class SignedPushPreReceiveHookTest {
|
||||
// ./pubring.gpg
|
||||
// -------------
|
||||
// pub 1024R/30A5A053 2015-06-16 [expires: 2015-06-17]
|
||||
// Key fingerprint = 96D6 DE78 E6D8 DA49 9387 1F31 FA09 A0C4 30A5 A053
|
||||
// uid A U. Thor <a_u_thor@example.com>
|
||||
// sub 1024R/D6831DC8 2015-06-16 [expires: 2015-06-17]
|
||||
private static final String PUBKEY =
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "mI0EVYCBUQEEALCKzuY6M68RRRm6PS1F322lpHSHTdW9PIURm5B//tbfS32EN6lM\n"
|
||||
+ "ISwJxhanpZanv2o4mbV3V8oLT3jMVDPJ3dqmOZJdJs37l+dxCVJ3ycFe1LHtT2oT\n"
|
||||
+ "eRyC5PxD7UY5PdDe97mjp7yrp/bx1hE6XqGV0nDGrkJXc8A35u3WzIF5ABEBAAG0\n"
|
||||
+ "IEEgVS4gVGhvciA8YV91X3Rob3JAZXhhbXBsZS5jb20+iL4EEwECACgFAlWAgVEC\n"
|
||||
+ "GwMFCQABUYAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPoJoMQwpaBTjhoD\n"
|
||||
+ "/0MRCX1zBjEKIfzFYeSEg/OcSLbAkUD7un5YTfpgds3oUNIKlIgovWO24TQxrCCu\n"
|
||||
+ "5pSzN/WfRSzPFhj9HahY/5yh+EGd6HmIU2v/k5I3LwTPEOcZUi1SzOScSv6JOO9Q\n"
|
||||
+ "3srVilCu3h6TNW1UGBNjfOr1NdmkWfsUZcjsEc/XrfBGuI0EVYCBUQEEAL0UP9jJ\n"
|
||||
+ "eLj3klCCa2tmwdgyFiSf9T+Yoed4I3v3ag2F0/CWrCJr3e1ogSs4Bdts0WptI+Nu\n"
|
||||
+ "QIq40AYszewq55dTcB4lbNAYE4svVYQ5AGz78iKzljaBFhyT6ePdZ5wfb+8Jqu1l\n"
|
||||
+ "7wRwzRI5Jn3OXCmdGm/dmoUNG136EA9A4ZLLABEBAAGIpQQYAQIADwUCVYCBUQIb\n"
|
||||
+ "DAUJAAFRgAAKCRD6CaDEMKWgU5JTA/9XjwPFZ5NseNROMhYZMmje1/ixISb2jaVc\n"
|
||||
+ "9m9RLCl8Y3RCY9NNdU5FinTIX9LsRTrJlW6FSG5sin8mwx9jq0eGE1TBEKND5klT\n"
|
||||
+ "TmsG0jx1dZG9kWDy6lPnIWw2/4W+N0fK/Cw6WEL1Xg7RLi4NQ9Bi2WoxJii9bWMv\n"
|
||||
+ "yy35U6UfPQ==\n"
|
||||
+ "=0GL9\n"
|
||||
+ "-----END PGP PUBLIC KEY BLOCK-----\n";
|
||||
|
||||
private PGPPublicKey key;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ArmoredInputStream in = new ArmoredInputStream(
|
||||
new ByteArrayInputStream(Constants.encode(PUBKEY)));
|
||||
PGPPublicKeyRing keyRing =
|
||||
new PGPPublicKeyRing(in, new BcKeyFingerprintCalculator());
|
||||
key = keyRing.getPublicKey();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyIdToString() throws Exception {
|
||||
assertThat(keyIdToString(key.getKeyID()))
|
||||
.isEqualTo("30A5A053");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyToString() throws Exception {
|
||||
assertThat(SignedPushPreReceiveHook.toString(key))
|
||||
.isEqualTo("30A5A053 A U. Thor <a_u_thor@example.com>"
|
||||
+ " (96D6 DE78 E6D8 DA49 9387 1F31 FA09 A0C4 30A5 A053)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyObjectId() throws Exception {
|
||||
String objId = SignedPushPreReceiveHook.keyObjectId(key.getKeyID()).name();
|
||||
assertThat(objId).isEqualTo("fa09a0c430a5a053000000000000000000000000");
|
||||
assertThat(objId.substring(8, 16))
|
||||
.isEqualTo(keyIdToString(key.getKeyID()).toLowerCase());
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PublicKeyCheckerTest {
|
||||
private PublicKeyChecker checker;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
checker = new PublicKeyChecker();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validKey() throws Exception {
|
||||
assertProblems(TestKey.key1());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wrongKeyId() throws Exception {
|
||||
TestKey k = TestKey.key1();
|
||||
long badId = k.getKeyId() + 1;
|
||||
CheckResult result = checker.check(
|
||||
k.getPublicKey(), badId, k.getFirstUserId());
|
||||
assertEquals(
|
||||
Arrays.asList("Public key does not match ID 46328A8D"),
|
||||
result.getProblems());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wrongUserId() throws Exception {
|
||||
TestKey k = TestKey.key1();
|
||||
CheckResult result = checker.check(
|
||||
k.getPublicKey(), k.getKeyId(), "test2@example.com");
|
||||
assertEquals(
|
||||
Arrays.asList("No certification for User ID test2@example.com"),
|
||||
result.getProblems());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyExpiringInFuture() throws Exception {
|
||||
assertProblems(TestKey.key2());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expiredKey() throws Exception {
|
||||
assertProblems(TestKey.key3(), "Key is expired");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selfRevokedKey() throws Exception {
|
||||
assertProblems(TestKey.key4(), "Key is revoked");
|
||||
}
|
||||
|
||||
private void assertProblems(TestKey tk, String... expected) throws Exception {
|
||||
CheckResult result = checker.check(
|
||||
tk.getPublicKey(), tk.getKeyId(), tk.getFirstUserId());
|
||||
assertEquals(Arrays.asList(expected), result.getProblems());
|
||||
}
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyObjectId;
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
|
||||
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
||||
import org.eclipse.jgit.junit.TestRepository;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class PublicKeyStoreTest {
|
||||
private TestRepository<?> tr;
|
||||
private PublicKeyStore store;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
tr = new TestRepository<>(new InMemoryRepository(
|
||||
new DfsRepositoryDescription("pubkeys")));
|
||||
store = new PublicKeyStore(tr.getRepository());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyIdToString() throws Exception {
|
||||
PGPPublicKey key = TestKey.key1().getPublicKey();
|
||||
assertEquals("46328A8C", keyIdToString(key.getKeyID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyToString() throws Exception {
|
||||
PGPPublicKey key = TestKey.key1().getPublicKey();
|
||||
assertEquals("46328A8C Testuser One <test1@example.com>"
|
||||
+ " (04AE A7ED 2F82 1133 E5B1 28D1 ED06 25DC 4632 8A8C)",
|
||||
keyToString(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeyObjectId() throws Exception {
|
||||
PGPPublicKey key = TestKey.key1().getPublicKey();
|
||||
String objId = keyObjectId(key.getKeyID()).name();
|
||||
assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
|
||||
assertEquals(keyIdToString(key.getKeyID()).toLowerCase(),
|
||||
objId.substring(8, 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGet() throws Exception {
|
||||
TestKey key1 = TestKey.key1();
|
||||
tr.branch(RefNames.REFS_GPG_KEYS)
|
||||
.commit()
|
||||
.add(keyObjectId(key1.getKeyId()).name(),
|
||||
key1.getPublicKeyArmored())
|
||||
.create();
|
||||
TestKey key2 = TestKey.key2();
|
||||
tr.branch(RefNames.REFS_GPG_KEYS)
|
||||
.commit()
|
||||
.add(keyObjectId(key2.getKeyId()).name(),
|
||||
key2.getPublicKeyArmored())
|
||||
.create();
|
||||
|
||||
assertKeys(key1.getKeyId(), key1);
|
||||
assertKeys(key2.getKeyId(), key2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMultiple() throws Exception {
|
||||
TestKey key1 = TestKey.key1();
|
||||
TestKey key2 = TestKey.key2();
|
||||
tr.branch(RefNames.REFS_GPG_KEYS)
|
||||
.commit()
|
||||
.add(keyObjectId(key1.getKeyId()).name(),
|
||||
key1.getPublicKeyArmored()
|
||||
// Mismatched for this key ID, but we can still read it out.
|
||||
+ key2.getPublicKeyArmored())
|
||||
.create();
|
||||
assertKeys(key1.getKeyId(), key1, key2);
|
||||
}
|
||||
|
||||
private void assertKeys(long keyId, TestKey... expected)
|
||||
throws Exception {
|
||||
Set<String> expectedStrings = new TreeSet<>();
|
||||
for (TestKey k : expected) {
|
||||
expectedStrings.add(keyToString(k.getPublicKey()));
|
||||
}
|
||||
PGPPublicKeyRingCollection actual = store.get(keyId);
|
||||
Set<String> actualStrings = new TreeSet<>();
|
||||
for (PGPPublicKeyRing k : actual) {
|
||||
actualStrings.add(keyToString(k.getPublicKey()));
|
||||
}
|
||||
assertEquals(expectedStrings, actualStrings);
|
||||
}
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyIdToString;
|
||||
import static com.google.gerrit.server.git.gpg.PublicKeyStore.keyToString;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.RefNames;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.bcpg.BCPGOutputStream;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
||||
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.Repository;
|
||||
import org.eclipse.jgit.transport.PushCertificate;
|
||||
import org.eclipse.jgit.transport.PushCertificateIdent;
|
||||
import org.eclipse.jgit.transport.PushCertificateParser;
|
||||
import org.eclipse.jgit.transport.SignedPushConfig;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PushCertificateCheckerTest {
|
||||
private TestRepository<?> tr;
|
||||
private SignedPushConfig signedPushConfig;
|
||||
private PushCertificateChecker checker;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
TestKey key1 = TestKey.key1();
|
||||
TestKey key3 = TestKey.key3();
|
||||
tr = new TestRepository<>(new InMemoryRepository(
|
||||
new DfsRepositoryDescription("repo")));
|
||||
tr.branch(RefNames.REFS_GPG_KEYS).commit()
|
||||
.add(PublicKeyStore.keyObjectId(key1.getPublicKey().getKeyID()).name(),
|
||||
key1.getPublicKeyArmored())
|
||||
.add(PublicKeyStore.keyObjectId(key3.getPublicKey().getKeyID()).name(),
|
||||
key3.getPublicKeyArmored())
|
||||
.create();
|
||||
signedPushConfig = new SignedPushConfig();
|
||||
signedPushConfig.setCertNonceSeed("sekret");
|
||||
signedPushConfig.setCertNonceSlopLimit(60 * 24);
|
||||
|
||||
checker = new PushCertificateChecker(new PublicKeyChecker()) {
|
||||
@Override
|
||||
protected Repository getRepository() {
|
||||
return tr.getRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldClose(Repository repo) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validCert() throws Exception {
|
||||
PushCertificate cert = newSignedCert(validNonce(), TestKey.key1());
|
||||
assertProblems(cert);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidNonce() throws Exception {
|
||||
PushCertificate cert = newSignedCert("invalid-nonce", TestKey.key1());
|
||||
assertProblems(cert, "Invalid nonce");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingKey() throws Exception {
|
||||
TestKey key2 = TestKey.key2();
|
||||
PushCertificate cert = newSignedCert(validNonce(), key2);
|
||||
assertProblems(cert,
|
||||
"No public keys found for Key ID " + keyIdToString(key2.getKeyId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidKey() throws Exception {
|
||||
TestKey key3 = TestKey.key3();
|
||||
PushCertificate cert = newSignedCert(validNonce(), key3);
|
||||
assertProblems(cert,
|
||||
"Invalid public key (" + keyToString(key3.getPublicKey())
|
||||
+ "):\n Key is expired");
|
||||
}
|
||||
|
||||
private String validNonce() {
|
||||
return signedPushConfig.getNonceGenerator()
|
||||
.createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
|
||||
}
|
||||
|
||||
private PushCertificate newSignedCert(String nonce, TestKey signingKey)
|
||||
throws Exception {
|
||||
PushCertificateIdent ident = new PushCertificateIdent(
|
||||
signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60);
|
||||
String payload = "certificate version 0.1\n"
|
||||
+ "pusher " + ident.getRaw() + "\n"
|
||||
+ "pushee test://localhost/repo.git\n"
|
||||
+ "nonce " + nonce + "\n"
|
||||
+ "\n"
|
||||
+ "0000000000000000000000000000000000000000"
|
||||
+ " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
+ " refs/heads/master\n";
|
||||
PGPSignatureGenerator gen = new PGPSignatureGenerator(
|
||||
new BcPGPContentSignerBuilder(
|
||||
signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1));
|
||||
gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey());
|
||||
gen.update(payload.getBytes(UTF_8));
|
||||
PGPSignature sig = gen.generate();
|
||||
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
try (BCPGOutputStream out = new BCPGOutputStream(
|
||||
new ArmoredOutputStream(bout))) {
|
||||
sig.encode(out);
|
||||
}
|
||||
|
||||
String cert = payload + new String(bout.toByteArray(), UTF_8);
|
||||
Reader reader =
|
||||
new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
|
||||
PushCertificateParser parser =
|
||||
new PushCertificateParser(tr.getRepository(), signedPushConfig);
|
||||
return parser.parse(reader);
|
||||
}
|
||||
|
||||
private void assertProblems(PushCertificate cert, String... expected)
|
||||
throws Exception {
|
||||
CheckResult result = checker.check(cert);
|
||||
assertEquals(Arrays.asList(expected), result.getProblems());
|
||||
}
|
||||
}
|
@@ -0,0 +1,496 @@
|
||||
// Copyright (C) 2015 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package com.google.gerrit.server.git.gpg;
|
||||
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
class TestKey {
|
||||
/**
|
||||
* A valid key with no expiration.
|
||||
*
|
||||
* <pre>
|
||||
* pub 2048R/46328A8C 2015-07-08
|
||||
* Key fingerprint = 04AE A7ED 2F82 1133 E5B1 28D1 ED06 25DC 4632 8A8C
|
||||
* uid Testuser One <test1@example.com>
|
||||
* sub 2048R/F0AF69C0 2015-07-08
|
||||
* </pre>
|
||||
*/
|
||||
static TestKey key1() throws PGPException, IOException {
|
||||
return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "mQENBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
|
||||
+ "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
|
||||
+ "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
|
||||
+ "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
|
||||
+ "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
|
||||
+ "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAG0IFRlc3R1c2VyIE9uZSA8\n"
|
||||
+ "dGVzdDFAZXhhbXBsZS5jb20+iQE4BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJ\n"
|
||||
+ "CgsEFgIDAQIeAQIXgAAKCRDtBiXcRjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8Lq\n"
|
||||
+ "yUpBrDp3P06QDGpKGFMAovBuh+NLH76VKNIzQLQC8rdTj651fLcLMuJ1enQ3Rblg\n"
|
||||
+ "RKr1oc+wqqtFHr4QyOQjE/N3C9GQjEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMx\n"
|
||||
+ "jRcHbM9KQnsE5Z4fh4wmN5ynG+5nbaF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX\n"
|
||||
+ "7Qkzze+scAlc9E/EWRJQIFcxnxV/SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjy\n"
|
||||
+ "W0lGHnh/ZqH6XGVcGUaJZZ2uHTck1+czuVVShNcXPW1W20T6E9UqzHbJHN0guQEN\n"
|
||||
+ "BFWdTIkBCACoLVdPr3gpQwzI+2NGXjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjN\n"
|
||||
+ "vYkS/+/oGtVEmiYOiAVTwmkjCYkKGDgNcCiJVekiPAN6JryVv488wRc999b5LpFE\n"
|
||||
+ "fhLGwI0YxjcS4KFFnpMC3wSb6tJUnHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIb\n"
|
||||
+ "nuyrk3ydEcS4ZeGD+w+taIxMc9F1DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3m\n"
|
||||
+ "rBCo97sE95yKcq98ZMIWuQtTcEccZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11Vl\n"
|
||||
+ "IQ9QFSj6ruqoKrYvNZuDDLD1lHvZPD4/ABEBAAGJAR8EGAECAAkFAlWdTIkCGwwA\n"
|
||||
+ "CgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUsj/16fGiF\n"
|
||||
+ "rRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl+xqsgpEj\n"
|
||||
+ "Fhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs3YI19Ci/\n"
|
||||
+ "FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnxqhH4wfHB\n"
|
||||
+ "PGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1H2PPSxrA\n"
|
||||
+ "0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
|
||||
+ "=o/aU\n"
|
||||
+ "-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "lQOYBFWdTIkBCADOaygDKjLuRX6LXAvBAYB91cmTf1MSlmEy+qsG3c9ijjQixPkr\n"
|
||||
+ "atdYkocrrT2S0R9UGjksTOI2WN5S0lQfLA1RSk63KURQE+OF+IfNqdD6nQdLBs1w\n"
|
||||
+ "va+GDj/uvuI05I0oXf/M7POdFphutrS4EUDBnFPj6ns/0C2sTRTxliD+Y9Y9a84V\n"
|
||||
+ "DfVVUbJB6wc3LP3L6ImT+cSM7dLq3hZHya+9FNeYPmPYnBrkJyqf2NDd38Sddsro\n"
|
||||
+ "7smw/GgCZHnnuVNS4C7NsHr6900VKC+JDtdx+fqptixcAEJWiGoQfWqU+hYmia3p\n"
|
||||
+ "9+Xw02+3FcjOT6ONUCmHX+xlz0pXW4iIYlPpABEBAAEAB/wLoOXEJ+Buo+OZHjpb\n"
|
||||
+ "SSZf8GdGs+mOJoKbSJvR6zT/rFsrikUvOPmgt8B9qWjKmJVXO5L09+/Wd/MuX0L1\n"
|
||||
+ "7plhdvowP1bl2/j5VyLvZx2qwKXkiCGStFzrBGp9nKtJp4Z8O69pb//ZXaiAtDJC\n"
|
||||
+ "HFa1kYT4VgFTevrXtg/z/C0np4Yjx0mZpw4nfISEeHCiYCyRa/B8R1+Pc4uIcoSo\n"
|
||||
+ "G3aq6Ow9m/LGvw0MRO5qHvqoF41TLPQpGKjKEsCBKHF1qh0tOOUHnLGrvbmdFnGr\n"
|
||||
+ "UXJpRkLdRTnj8ufvA4XVZhImzL+lD+ALtjlV14xh8nsNKYL42880GFl5Cl0OtBcE\n"
|
||||
+ "lgQBBADPJ6kHdvUYOe0zugRdukBSYLkZcYwRiphom7dZuavYICIu6B14ljEONzVD\n"
|
||||
+ "mPhi2lDOawZOURKwYd9S4K11XWLsTYe7XEwkc+1Fpvu4L/JqnJTTnnvbx05ZsqD5\n"
|
||||
+ "j9tybPlrTuLrf2ctfcC03Z55wfo6azsbf89yrr6QX0+l9dlkYQQA/xcMdQJ0Z5vm\n"
|
||||
+ "kvyaCPsQzJc/8noVO9PMv7xJm14gJWK7Px3y2eBidzpCbVVFnGWW6CPb3qKerB5U\n"
|
||||
+ "pwcF4gCFWyP9C2YtnB0hgqixIPfR+UO8gpqdY6MP8NPspoXouffRn+Zic/P6Cxje\n"
|
||||
+ "/MGxNQBeRtqb2IGh1xZ8v/8tmmmxHIkEAP74HkGETcXmlj3/6RlwTBUAovPARSn7\n"
|
||||
+ "LDtOCPezg6mQmble1BvnTnAwOHKJVqjx+3qsGqMe8OGGXAxZPSU1xSmOShBFrpDp\n"
|
||||
+ "xArE67arE17pT1lyD/gmHRuqnNMvgRrwz1mDm3G2ohWkCVixEiB+8vPQfbZrJBgQ\n"
|
||||
+ "WxOF4RCo2WWyRKa0IFRlc3R1c2VyIE9uZSA8dGVzdDFAZXhhbXBsZS5jb20+iQE4\n"
|
||||
+ "BBMBAgAiBQJVnUyJAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDtBiXc\n"
|
||||
+ "RjKKjHblB/9RaFO5+GTDIphAL/aVj2u+d8LqyUpBrDp3P06QDGpKGFMAovBuh+NL\n"
|
||||
+ "H76VKNIzQLQC8rdTj651fLcLMuJ1enQ3RblgRKr1oc+wqqtFHr4QyOQjE/N3C9GQ\n"
|
||||
+ "jEzfqn4qnp5KtZxYFnlvU5NGehid7M1HTZMxjRcHbM9KQnsE5Z4fh4wmN5ynG+5n\n"
|
||||
+ "baF4O9otPOpFzYRvIhxFmHscWyOgRaMZiYEX7Qkzze+scAlc9E/EWRJQIFcxnxV/\n"
|
||||
+ "SYIT4qCTT1g2aKA8OCBO/ZTOleH8SzvTODjyW0lGHnh/ZqH6XGVcGUaJZZ2uHTck\n"
|
||||
+ "1+czuVVShNcXPW1W20T6E9UqzHbJHN0gnQOYBFWdTIkBCACoLVdPr3gpQwzI+2NG\n"
|
||||
+ "XjdtoyqYoPlgfeyI2M1XQD/7+rLZTbi14ZjNvYkS/+/oGtVEmiYOiAVTwmkjCYkK\n"
|
||||
+ "GDgNcCiJVekiPAN6JryVv488wRc999b5LpFEfhLGwI0YxjcS4KFFnpMC3wSb6tJU\n"
|
||||
+ "nHRLVoE5d8icdiaOpgYdp7uqWkSx2oxqHgIbnuyrk3ydEcS4ZeGD+w+taIxMc9F1\n"
|
||||
+ "DS9kiXALD7xWgUkmqZLEQoNgF6KlwCHXRd3mrBCo97sE95yKcq98ZMIWuQtTcEcc\n"
|
||||
+ "ZsN/6jlsei+9RI0tqs+FbZnIFm/go9zk11VlIQ9QFSj6ruqoKrYvNZuDDLD1lHvZ\n"
|
||||
+ "PD4/ABEBAAEAB/4kQnJauehcbRpqktjaqSGmP9HFSp+50CyZbLUJJM8m0uyQsZMr\n"
|
||||
+ "k9JQOZc+Q3RERNTKj7m41Fbhsj7c0Qd856/eJdp3kdBME0hko8lxN/X4EWGjeLYe\n"
|
||||
+ "z41+iPgfZhCF0Oa66TecPQ5RRihGPaDPoVPpkmMWMt9L7KVviBg1eJ6bobVIY5hu\n"
|
||||
+ "a7KFJHZQcCI1OvdJ0cx89KDSbnH8iMM6Kmw1bE3D2FEaWctuKLBo5PNRgyTJvdBd\n"
|
||||
+ "PSf56/Rc6csPqmOntQi2Yn8n47eCOTclHNuygSTJeHPpymVuWbhMq6fhJat/xA+V\n"
|
||||
+ "kyT8I2c45RQb0dKId+wEytjbKw8AI6Q3GXqhBADOhsr9M+JWc4MpD43mCDZACN4v\n"
|
||||
+ "RBRxSrJvO/V6HqQPmKYRmr9Gk3vxgF0zCf5zB1QeBiXpTpShxV87RIbUYReOyavp\n"
|
||||
+ "87zH6/SkRxQJiBEpQh5Fu5CoAaxGOivxbPqdWHrBY6jvqkrRoMPNiFJ6/ty5w9jx\n"
|
||||
+ "i9kGm9PelQGu2SdLNwQA0HbGo8sC8h5TSTEDCkFHRYzVYONx+32AlkCsJX9mEt0E\n"
|
||||
+ "nG8d97Ay24JsbnuXSq04FJrqzjOVyHLUffpXnAGELJZVNCIparSyqIaj43UG/oPc\n"
|
||||
+ "ICPmR7zI9G49ICUPSzI7+S2+BwjbiHRQcP0zmxbH92G4abYwKfk7dsDpGyVM+TkD\n"
|
||||
+ "/2nUiV0CRqnGipeiLWNjW/Md0ufkwqBvCWxrtxj0rQCyvBOVg3B6DocVNzgOOYa1\n"
|
||||
+ "ji3We5A9mSP40JBmMfk2veFrDdsGn4G+OpzMxKQtNfYemqjALfZ2zTdax0mXPXy6\n"
|
||||
+ "Gl0jUgSGrxGm8QnRLsrRx7G7ZKnvkcS+YsdQ8dbtzvJtQfiJAR8EGAECAAkFAlWd\n"
|
||||
+ "TIkCGwwACgkQ7QYl3EYyiox+HAf/Z/OCQO3jxALAcn3oUb1g/IlHm6qZv7RJOFUs\n"
|
||||
+ "j/16fGiFrRTP15zMXzyqV+L/LGV/owvOsdD/o7boZz4C/U98COx0Nl1jOrmPATOl\n"
|
||||
+ "+xqsgpEjFhk+eAR7exO2XxW+u2g4cYoSMosIOX5w1GrdsxQeaZDwiSJMEOR2cVLs\n"
|
||||
+ "3YI19Ci/FuzActZ0wJNk0nlNF6l8CAbzwN6pM9OIc/iBIwDjz92KUco0NF8XKZnx\n"
|
||||
+ "qhH4wfHBPGkTx8RwOvELUTDMtvYnG5R0QtND0RbOnmp4ZVZmeOjKSLo1mZliUZB1\n"
|
||||
+ "H2PPSxrA0oLr8+wLntz1SU7uS4ddvhSQW+j2M/0pa352KUwmrw==\n"
|
||||
+ "=MuAn\n"
|
||||
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* A valid key expiring in 2065.
|
||||
*
|
||||
* <pre>
|
||||
* pub 2048R/378A0AED 2015-07-08 [expires: 2065-06-25]
|
||||
* Key fingerprint = C378 369A CBCD 34CC 138D 90B1 4531 1A6F 378A 0AED
|
||||
* uid Testuser Two <test2@example.com>
|
||||
* sub 2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
|
||||
* </pre>
|
||||
*/
|
||||
static final TestKey key2() throws PGPException, IOException {
|
||||
return new TestKey(
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "mQENBFWdTP8BCADRxNpasIv0jtNXTK6VYIS2VJ2Xk0ZD6gtxeoXCpjQ+TsB9fxh3\n"
|
||||
+ "vAMPt2Zu5LqoGwygKOJj1zquG8xk7GUCCHJk3+qG8xxB1xGtSz2vLyfRm7fOZmHj\n"
|
||||
+ "3W/C/25lynSPDrfvcwvwA4PN8iP5EWbWU10L6WOZGMwwwtVDUSEouSOw2LEepxLV\n"
|
||||
+ "rkKuZcyHaivheDbUlZliwe9rGXd4hh1h4qyNQWG3q+ytlL28sVkOzUh6IMBTvqhe\n"
|
||||
+ "IRsvxvaVSLV8jRVKfUTqw0g57ft4ZD2/L46yUTXzr9aUCBjTNxvWLlyboqql/D8P\n"
|
||||
+ "inp51h3cvAg7NW5RdG1GEYmylH8SygT5utPxABEBAAG0IFRlc3R1c2VyIFR3byA8\n"
|
||||
+ "dGVzdDJAZXhhbXBsZS5jb20+iQE+BBMBAgAoBQJVnUz/AhsDBQld/A8ABgsJCAcD\n"
|
||||
+ "AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBFMRpvN4oK7UZqCACWwQL/YvBK4b0m+R0d\n"
|
||||
+ "UdvAXeBx7DwOAnAodis9ZVqChb7RxcZQxF1Ti9mtCBPPQGuEs5wE2Ocrrq+L13r6\n"
|
||||
+ "bgW+1WOB1tZSDVxwL1PnZFw/SyADRIDCZrOHiAkp82UnZwWAkk39GzNJtt1wTYDZ\n"
|
||||
+ "FMTFUr2SPscXk1k7muS+ZfEFwNPD4tODo/poJKDYEJ80Z5UXXFQLDtsfdeIXMFIT\n"
|
||||
+ "449CYoq8XBMBfvyWl/LLpw0r3JI6pV/YdH3Oeuz8XkkEVzRxaxB6Zmeo5jSwjR/T\n"
|
||||
+ "8TKDGwwiuwiiT3SfkFSVdcjKulRuXSRNs1Ouf7/UC3cq4bG2WXWa85X1+HQRm7iu\n"
|
||||
+ "RHSOuQENBFWdTP8BCADhhGxAA0pX5yBHwIgM1j0gw2h5nSsopDrO6t/sbRUcNxnR\n"
|
||||
+ "tBScgKZnP0sjRTYEUIwmZuseHMBohtVCuMaDt06qyZDvDk/98j3AeE5t2dgFnOIe\n"
|
||||
+ "qCrm/6aejbFcQOpxe6U29KJRCAxuwNtB15X1VH1Kj7B0gRSTu13n/5sUsi2lunoZ\n"
|
||||
+ "oIvpIe9tZH4aXitCY2MCQH+hTyCyNBzlEa44kWz6LxUsPdo7I6rXkTr6Ot7wQh+9\n"
|
||||
+ "7HCe042GIq65h0apgujyjhJidjch5ur1mngaSNSEyvbji2MGC+cd3wAIstG5a7xP\n"
|
||||
+ "d9MncY5Q/eH+hn96694k5bckottSyGm/3f2Ihfj1ABEBAAGJASUEGAECAA8FAlWd\n"
|
||||
+ "TP8CGwwFCV38DwAACgkQRTEabzeKCu1FNwgAif4eK2v7R3QubL2S6wmb1nsgRMgV\n"
|
||||
+ "YoxGBeUk2EK6WZ5IPor93ySd0ixRVNMRmJ8BLH3EMjZQTzkDG+BH6zFyxo6lLHw9\n"
|
||||
+ "NxQjI06tqQWgyyK0mEweVwB/zqtxiB4lNUpsNbqOZWnBJ3d6o1SsnD2Q3uwvP5fb\n"
|
||||
+ "fSIgdmUk3c0VMdgA+KzWjPD/PJIPujE+ckHhjn5cbDNw35/FuyhkLJfqlOG7SPvM\n"
|
||||
+ "NmCdJ1Pcqju9t7sf6b0BGPDOCL4gpuWKK7HJz9WxngNb3FSziLbyPLk13ynADO+v\n"
|
||||
+ "EOR44LPyXE9kVxPusazsXlt9ayTOhELhwzw7sGFFu8E17Cpn7GnVj3tN9A==\n"
|
||||
+ "=1e/A\n"
|
||||
+ "-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "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\n"
|
||||
+ "9A==\n"
|
||||
+ "=qbV3\n"
|
||||
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* A key that expired in 2006.
|
||||
*
|
||||
* <pre>
|
||||
* pub 2048R/17DE1ACD 2005-07-08 [expired: 2006-07-08]
|
||||
* Key fingerprint = 1D9E EB79 DD38 B049 939D 9CAF 3CEC 781B 17DE 1ACD
|
||||
* uid Testuser Three <test3@example.com>
|
||||
* </pre>
|
||||
*/
|
||||
static final TestKey key3() throws PGPException, IOException {
|
||||
return new TestKey(
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "mQENBELOp0UBCACxholOPWuKhK+TYb88nvLUSCMvTLIFEpb5u3Eavr0wiluEzq6H\n"
|
||||
+ "55nswAD3dQm8DWxA7yUlEYjPr5btpw7V9441bb1+qtgZMJ10RTdEb/WjyctdGA99\n"
|
||||
+ "uOKBEarWbt8W+w6lyJ9NXy5bS/x5EwHHfoTFp4ff6ffHI5hbx1a00K8oxmitgd0X\n"
|
||||
+ "Mx86UmauFNJYupZOZG9gEcP4RbRp7e2pm4Jy1WLEOeg9Fdgm5e5Hj2nMkCSZ9BKV\n"
|
||||
+ "cxuOllSVzM/Zp0/4+RS9R57jKo3/V74Whwh9yQNgL9UxdNk7L0eGqvaT3EjXxjOc\n"
|
||||
+ "RCeJiucGN/0W2iq+V01/QGspp4SKtAogWBozABEBAAG0IlRlc3R1c2VyIFRocmVl\n"
|
||||
+ "IDx0ZXN0M0BleGFtcGxlLmNvbT6JAT4EEwECACgFAkLOp0UCGwMFCQHhM4AGCwkI\n"
|
||||
+ "BwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEDzseBsX3hrNYg0H/2CMm5/JDQNSuRFC\n"
|
||||
+ "ECWLrcOeimuvwbmkonNzOkvKbGXl73GStISAksRWAHBQED1rEPC0NkFCDeVZO7df\n"
|
||||
+ "SYLlsqKwV6uSh05Ra0F5XeniC12YpAyzoQyCGRS2wLaS822j0zUPXA8XLaO2blCu\n"
|
||||
+ "R+8sNu/oecMRcFK4S9NaApi3vdqBNhLiN1/Lpqn1LfB8uIO+eaUf4PmCWbaPgzSk\n"
|
||||
+ "qcPfKZmocNXdgLV5Q80n3hc2y2nrl+vDW2M+eVZuDHAok2BOD9uGKFfLAbaXLbX5\n"
|
||||
+ "btBW2L0UHtoEyiqkRfD6lX2laSLQmA6+eup7e4GS+s0vXBuVh8XEYddV6Yjt8H7/\n"
|
||||
+ "2thO41K5AQ0EQs6nRQEIAM/833UHK1DuFlOm7/n18dRMvs7BkXvg+hPquKWMG3be\n"
|
||||
+ "eE4sh1NG5DbRCdo6iacZLarWr3FDz7J9+wswRhtHCh3pGHEuaJk52vRjQxlkNh5F\n"
|
||||
+ "p5u2R4WF546bWqX45xPdLfHVTPyWB9q7aVxE+6Q+MHa6lMoyTVnTVCOy3nshiihw\n"
|
||||
+ "dxLsxaga+QmaL0bAR+dRcO6ucj7TDQXz1AJAVp26c0LXV9iErhFuuybUZKT0a9Aj\n"
|
||||
+ "FoumMZ6l+k30sSdjSjpBMsNvPos0dTPPRXUMu77o5sj+pHa4o8WctgB3o7BHQELp\n"
|
||||
+ "KgujZ2sKC9Nm395u6Q4cqUWihzb/Y7rIRuNHJarI7vUAEQEAAYkBJQQYAQIADwUC\n"
|
||||
+ "Qs6nRQIbDAUJAeEzgAAKCRA87HgbF94azRiBB/4vAyOOjUjK3lDWjHGs7mvEWJI/\n"
|
||||
+ "1MeLlGPswCSInJBa+HMiMI4tzq+hu5ejGThojNbmnL96GdzfDkMlP4Feyxb2rjtb\n"
|
||||
+ "NrD/R5tlXHmjX/QLzep4LCeMziP80fu8qUeiOej/Ecdny0w365PlMdt10RaYR8VE\n"
|
||||
+ "ZX/DAie6JfElnfQcG5q8TIOH3i71qxV+kIoPqKWfQ0MXrNEJ3BYFfDGdUt8U1Kq9\n"
|
||||
+ "OuIHVRgGS7mMSyjgNqqp7MBeMY+PFFZaZel5yoYVjb9d3L8XvVv2eoa/jPj5FUEU\n"
|
||||
+ "kE9uxNmwaD1PiV8DvBTYI+eQL4qzfu+3NTG2SfgQYtj5oiGHw8aL3U6QHDJb\n"
|
||||
+ "=d/Xp\n"
|
||||
+ "-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "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\n"
|
||||
+ "HDJb\n"
|
||||
+ "=RrXv\n"
|
||||
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* A self-revoked key with no expiration.
|
||||
*
|
||||
* <pre>
|
||||
* pub 2048R/7CA87821 2015-07-08 [revoked: 2015-07-08]
|
||||
* Key fingerprint = E328 CAB1 1F7E B1BC 1451 ABA5 0855 2A17 7CA8 7821
|
||||
* uid Testuser Four <test4@example.com>
|
||||
* </pre>
|
||||
*/
|
||||
static final TestKey key4() throws PGPException, IOException {
|
||||
return new TestKey(
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "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\n"
|
||||
+ "=477N\n"
|
||||
+ "-----END PGP PUBLIC KEY BLOCK-----\n",
|
||||
"-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
|
||||
+ "Version: GnuPG v1\n"
|
||||
+ "\n"
|
||||
+ "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\n"
|
||||
+ "=5aNq\n"
|
||||
+ "-----END PGP PRIVATE KEY BLOCK-----\n");
|
||||
}
|
||||
|
||||
// TODO(dborowitz): Figure out how to get gpg to revoke a key for someone
|
||||
// else.
|
||||
|
||||
private final String pubArmored;
|
||||
private final String secArmored;
|
||||
private final PGPPublicKey pub;
|
||||
private final PGPSecretKey sec;
|
||||
|
||||
private TestKey(String pubArmored, String secArmored)
|
||||
throws PGPException, IOException {
|
||||
this.pubArmored = pubArmored;
|
||||
this.secArmored = secArmored;
|
||||
BcKeyFingerprintCalculator fc = new BcKeyFingerprintCalculator();
|
||||
this.pub = new PGPPublicKeyRing(newStream(pubArmored), fc).getPublicKey();
|
||||
this.sec = new PGPSecretKeyRing(newStream(secArmored), fc).getSecretKey();
|
||||
}
|
||||
|
||||
String getPublicKeyArmored() {
|
||||
return pubArmored;
|
||||
}
|
||||
|
||||
String getSecretKeyArmored() {
|
||||
return secArmored;
|
||||
}
|
||||
|
||||
PGPPublicKey getPublicKey() {
|
||||
return pub;
|
||||
}
|
||||
|
||||
PGPSecretKey getSecretKey() {
|
||||
return sec;
|
||||
}
|
||||
|
||||
long getKeyId() {
|
||||
return pub.getKeyID();
|
||||
}
|
||||
|
||||
String getFirstUserId() {
|
||||
return (String) pub.getUserIDs().next();
|
||||
}
|
||||
|
||||
PGPPrivateKey getPrivateKey() throws PGPException {
|
||||
return sec.extractPrivateKey(
|
||||
new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider())
|
||||
// All test keys have no passphrase.
|
||||
.build(new char[0]));
|
||||
}
|
||||
|
||||
private static ArmoredInputStream newStream(String armored)
|
||||
throws IOException {
|
||||
return new ArmoredInputStream(
|
||||
new ByteArrayInputStream(Constants.encode(armored)));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user