Dissolve gerrit-gpg top-level directory

Change-Id: Ib102fce79dc9041fc9f091def4eacaaaddd71bcf
This commit is contained in:
David Ostrovsky
2017-08-23 00:09:04 +02:00
committed by Dave Borowitz
parent a84150efe0
commit 4f4f034b00
35 changed files with 67 additions and 66 deletions

View File

@@ -0,0 +1,19 @@
java_library(
name = "gpg",
srcs = glob(["**/*.java"]),
visibility = ["//visibility:public"],
deps = [
"//gerrit-server:server",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/reviewdb:server",
"//lib:guava",
"//lib:gwtorm",
"//lib/bouncycastle:bcpg-neverlink",
"//lib/bouncycastle:bcprov-neverlink",
"//lib/guice",
"//lib/guice:guice-assistedinject",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/log:api",
],
)

View File

@@ -0,0 +1,58 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPPublicKey;
/** Utility methods for Bouncy Castle. */
public class BouncyCastleUtil {
/**
* Check for Bouncy Castle PGP support.
*
* <p>As a side effect, adds {@link BouncyCastleProvider} as a security provider.
*
* @return whether Bouncy Castle PGP support is enabled.
*/
public static boolean havePGP() {
try {
Class.forName(PGPPublicKey.class.getName());
addBouncyCastleProvider();
return true;
} catch (NoClassDefFoundError
| ClassNotFoundException
| SecurityException
| NoSuchMethodException
| InstantiationException
| IllegalAccessException
| InvocationTargetException
| ClassCastException noBouncyCastle) {
return false;
}
}
private static void addBouncyCastleProvider()
throws ClassNotFoundException, SecurityException, NoSuchMethodException,
InstantiationException, IllegalAccessException, InvocationTargetException {
Class<?> clazz = Class.forName(BouncyCastleProvider.class.getName());
Constructor<?> constructor = clazz.getConstructor();
Security.addProvider((java.security.Provider) constructor.newInstance());
}
private BouncyCastleUtil() {}
}

View File

@@ -0,0 +1,93 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
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 {
static CheckResult ok(String... problems) {
return create(Status.OK, problems);
}
static CheckResult bad(String... problems) {
return create(Status.BAD, problems);
}
static CheckResult trusted() {
return new CheckResult(Status.TRUSTED, Collections.<String>emptyList());
}
static CheckResult create(Status status, String... problems) {
List<String> problemList =
problems.length > 0
? Collections.unmodifiableList(Arrays.asList(problems))
: Collections.<String>emptyList();
return new CheckResult(status, problemList);
}
static CheckResult create(Status status, List<String> problems) {
return new CheckResult(status, Collections.unmodifiableList(new ArrayList<>(problems)));
}
static CheckResult create(List<String> problems) {
return new CheckResult(
problems.isEmpty() ? Status.OK : Status.BAD, Collections.unmodifiableList(problems));
}
private final Status status;
private final List<String> problems;
private CheckResult(Status status, List<String> problems) {
if (status == null) {
throw new IllegalArgumentException("status must not be null");
}
this.status = status;
this.problems = problems;
}
/** @return whether the result has status {@link Status#OK} or better. */
public boolean isOk() {
return status.compareTo(Status.OK) >= 0;
}
/** @return whether the result has status {@link Status#TRUSTED} or better. */
public boolean isTrusted() {
return status.compareTo(Status.TRUSTED) >= 0;
}
/** @return the status enum value associated with the object. */
public Status getStatus() {
return status;
}
/** @return any problems encountered during checking. */
public List<String> getProblems() {
return problems;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('[').append(status);
for (int i = 0; i < problems.size(); i++) {
sb.append(i == 0 ? ": " : ", ").append(problems.get(i));
}
return sb.append(']').toString();
}
}

View File

@@ -0,0 +1,120 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jgit.util.NB;
public class Fingerprint {
private final byte[] fp;
public static String toString(byte[] fp) {
checkLength(fp);
return String.format(
"%04X %04X %04X %04X %04X %04X %04X %04X %04X %04X",
NB.decodeUInt16(fp, 0),
NB.decodeUInt16(fp, 2),
NB.decodeUInt16(fp, 4),
NB.decodeUInt16(fp, 6),
NB.decodeUInt16(fp, 8),
NB.decodeUInt16(fp, 10),
NB.decodeUInt16(fp, 12),
NB.decodeUInt16(fp, 14),
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);
return fp;
}
/**
* Wrap a fingerprint byte array.
*
* <p>The newly created Fingerprint object takes ownership of the byte array, which must not be
* subsequently modified. (Most callers, such as hex decoders and {@code
* org.bouncycastle.openpgp.PGPPublicKey#getFingerprint()}, already produce fresh byte arrays).
*
* @param fp 20-byte fingerprint byte array to wrap.
*/
public Fingerprint(byte[] fp) {
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;
}
public boolean equalsBytes(byte[] bytes) {
return Arrays.equals(fp, bytes);
}
@Override
public int hashCode() {
// Same hash code as ObjectId: second int word.
return NB.decodeInt32(fp, 4);
}
@Override
public boolean equals(Object o) {
return (o instanceof Fingerprint) && equalsBytes(((Fingerprint) o).fp);
}
@Override
public String toString() {
return toString(fp);
}
public long getId() {
return getId(fp);
}
}

View File

@@ -0,0 +1,252 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.PushCertificateIdent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Checker for GPG public keys including Gerrit-specific checks.
*
* <p>For Gerrit, keys must contain a self-signed user ID certification matching a trusted external
* ID in the database, or an email address thereof.
*/
public class GerritPublicKeyChecker extends PublicKeyChecker {
private static final Logger log = LoggerFactory.getLogger(GerritPublicKeyChecker.class);
@Singleton
public static class Factory {
private final Provider<InternalAccountQuery> accountQueryProvider;
private final String webUrl;
private final IdentifiedUser.GenericFactory userFactory;
private final int maxTrustDepth;
private final ImmutableMap<Long, Fingerprint> trusted;
@Inject
Factory(
@GerritServerConfig Config cfg,
Provider<InternalAccountQuery> accountQueryProvider,
IdentifiedUser.GenericFactory userFactory,
@CanonicalWebUrl String webUrl) {
this.accountQueryProvider = accountQueryProvider;
this.webUrl = webUrl;
this.userFactory = userFactory;
this.maxTrustDepth = cfg.getInt("receive", null, "maxTrustDepth", 0);
String[] strs = cfg.getStringList("receive", null, "trustedKey");
if (strs.length != 0) {
Map<Long, Fingerprint> fps = Maps.newHashMapWithExpectedSize(strs.length);
for (String str : strs) {
str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
fps.put(fp.getId(), fp);
}
trusted = ImmutableMap.copyOf(fps);
} else {
trusted = null;
}
}
public GerritPublicKeyChecker create() {
return new GerritPublicKeyChecker(this);
}
public GerritPublicKeyChecker create(IdentifiedUser expectedUser, PublicKeyStore store) {
GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
checker.setExpectedUser(expectedUser);
checker.setStore(store);
return checker;
}
}
private final Provider<InternalAccountQuery> accountQueryProvider;
private final String webUrl;
private final IdentifiedUser.GenericFactory userFactory;
private IdentifiedUser expectedUser;
private GerritPublicKeyChecker(Factory factory) {
this.accountQueryProvider = factory.accountQueryProvider;
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
public CheckResult checkCustom(PGPPublicKey key, int depth) {
try {
if (depth == 0 && expectedUser != null) {
return checkIdsForExpectedUser(key);
}
return checkIdsForArbitraryUser(key);
} catch (PGPException | OrmException e) {
String msg = "Error checking user IDs for key";
log.warn(msg + " " + keyIdToString(key.getKeyID()), e);
return CheckResult.bad(msg);
}
}
private CheckResult checkIdsForExpectedUser(PGPPublicKey key) throws PGPException {
Set<String> allowedUserIds = getAllowedUserIds(expectedUser);
if (allowedUserIds.isEmpty()) {
return CheckResult.bad(
"No identities found for user; check " + webUrl + "#" + PageLinks.SETTINGS_WEBIDENT);
}
if (hasAllowedUserId(key, allowedUserIds)) {
return CheckResult.trusted();
}
return CheckResult.bad(missingUserIds(allowedUserIds));
}
private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException {
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
if (accountStates.isEmpty()) {
return CheckResult.bad("Key is not associated with any users");
}
if (accountStates.size() > 1) {
return CheckResult.bad("Key is associated with multiple users");
}
IdentifiedUser user = userFactory.create(accountStates.get(0));
Set<String> allowedUserIds = getAllowedUserIds(user);
if (allowedUserIds.isEmpty()) {
return CheckResult.bad("No identities found for user");
}
if (hasAllowedUserId(key, allowedUserIds)) {
return CheckResult.trusted();
}
return CheckResult.bad("Key does not contain any valid certifications for user's identities");
}
private boolean hasAllowedUserId(PGPPublicKey key, Set<String> allowedUserIds)
throws PGPException {
Iterator<String> userIds = key.getUserIDs();
while (userIds.hasNext()) {
String userId = userIds.next();
if (isAllowed(userId, allowedUserIds)) {
Iterator<PGPSignature> sigs = getSignaturesForId(key, userId);
while (sigs.hasNext()) {
if (isValidCertification(key, sigs.next(), userId)) {
return true;
}
}
}
}
return false;
}
private Iterator<PGPSignature> getSignaturesForId(PGPPublicKey key, String userId) {
Iterator<PGPSignature> result = key.getSignaturesForID(userId);
return result != null ? result : Collections.emptyIterator();
}
private Set<String> getAllowedUserIds(IdentifiedUser user) {
Set<String> result = new HashSet<>();
result.addAll(user.getEmailAddresses());
for (ExternalId extId : user.state().getExternalIds()) {
if (extId.isScheme(SCHEME_GPGKEY)) {
continue; // Omit GPG keys.
}
result.add(extId.key().get());
}
return result;
}
private static boolean isAllowed(String userId, Set<String> allowedUserIds) {
return allowedUserIds.contains(userId)
|| allowedUserIds.contains(PushCertificateIdent.parse(userId).getEmailAddress());
}
private static boolean isValidCertification(PGPPublicKey key, PGPSignature sig, String userId)
throws PGPException {
if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION
&& sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) {
return false;
}
if (sig.getKeyID() != key.getKeyID()) {
return false;
}
// TODO(dborowitz): Handle certification revocations:
// - Is there a revocation by either this key or another key trusted by the
// server?
// - Does such a revocation postdate all other valid certifications?
sig.init(new BcPGPContentVerifierBuilderProvider(), key);
return sig.verifyCertification(userId, key);
}
private static String missingUserIds(Set<String> allowedUserIds) {
StringBuilder sb =
new StringBuilder(
"Key must contain a valid certification for one of the following identities:\n");
Iterator<String> sorted = allowedUserIds.stream().sorted().iterator();
while (sorted.hasNext()) {
sb.append(" ").append(sorted.next());
if (sorted.hasNext()) {
sb.append('\n');
}
}
return sb.toString();
}
static ExternalId.Key toExtIdKey(PGPPublicKey key) {
return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import org.eclipse.jgit.lib.Repository;
public class GerritPushCertificateChecker extends PushCertificateChecker {
public interface Factory {
GerritPushCertificateChecker create(IdentifiedUser expectedUser);
}
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
@Inject
GerritPushCertificateChecker(
GerritPublicKeyChecker.Factory keyCheckerFactory,
GitRepositoryManager repoManager,
AllUsersName allUsers,
@Assisted IdentifiedUser expectedUser) {
super(keyCheckerFactory.create().setExpectedUser(expectedUser));
this.repoManager = repoManager;
this.allUsers = allUsers;
}
@Override
protected Repository getRepository() throws IOException {
return repoManager.openRepository(allUsers);
}
@Override
protected boolean shouldClose(Repository repo) {
return true;
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.gpg.api.GpgApiModule;
import com.google.gerrit.server.EnableSignedPush;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GpgModule extends FactoryModule {
private static final Logger log = LoggerFactory.getLogger(GpgModule.class);
private final Config cfg;
public GpgModule(Config cfg) {
this.cfg = cfg;
}
@Override
protected void configure() {
boolean configEnableSignedPush = cfg.getBoolean("receive", null, "enableSignedPush", false);
boolean configEditGpgKeys = cfg.getBoolean("gerrit", null, "editGpgKeys", true);
boolean havePgp = BouncyCastleUtil.havePGP();
boolean enableSignedPush = configEnableSignedPush && havePgp;
bindConstant().annotatedWith(EnableSignedPush.class).to(enableSignedPush);
if (configEnableSignedPush && !havePgp) {
log.info("Bouncy Castle PGP not installed; signed push verification is disabled");
}
if (enableSignedPush) {
install(new SignedPushModule());
factory(GerritPushCertificateChecker.Factory.class);
}
install(new GpgApiModule(enableSignedPush && configEditGpgKeys));
}
}

View File

@@ -0,0 +1,474 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
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;
/** 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 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, keyed by fingerprint; may not be empty. To
* construct a map, see {@link Fingerprint#byId(Iterable)}.
* @return a reference to this object.
*/
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("at least one trusted key is required");
}
this.maxTrustDepth = maxTrustDepth;
this.trusted = trusted;
return this;
}
/** 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");
}
this.store = store;
return this;
}
/**
* 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) {
if (store == null) {
throw new IllegalStateException("PublicKeyStore is required");
}
return check(key, 0, true, trusted != null ? new HashSet<Fingerprint>() : null);
}
/**
* Perform custom checks.
*
* <p>Default implementation reports no problems, but may be overridden by subclasses.
*
* @param key the public key.
* @param depth the depth from the initial key passed to {@link #check( 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, 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()) {
trustResult = CheckResult.create(trustResult.getStatus(), "Key is not trusted");
}
List<String> problems =
new ArrayList<>(
basicResult.getProblems().size()
+ customResult.getProblems().size()
+ trustResult.getProblems().size());
problems.addAll(basicResult.getProblems());
problems.addAll(customResult.getProblems());
problems.addAll(trustResult.getProblems());
Status status;
if (basicResult.getStatus() == BAD
|| customResult.getStatus() == BAD
|| trustResult.getStatus() == BAD) {
// Any BAD result and the final result is BAD.
status = BAD;
} else if (trustResult.getStatus() == TRUSTED) {
// basicResult is BAD or OK, whereas trustResult is BAD or TRUSTED. If
// TRUSTED, we trust the final result.
status = TRUSTED;
} else {
// All results were OK or better, but trustResult was not TRUSTED. Don't
// let subclasses bypass checkWebOfTrust by returning TRUSTED; just return
// OK here.
status = OK;
}
return CheckResult.create(status, problems);
}
private CheckResult checkBasic(PGPPublicKey key, Date now) {
List<String> problems = new ArrayList<>(2);
gatherRevocationProblems(key, now, problems);
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.isLongLength(), 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.isLongLength(), 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) {
// Trust checking not configured, server trusts all OK keys.
return CheckResult.trusted();
}
Fingerprint fp = new Fingerprint(key.getFingerprint());
if (seen.contains(fp)) {
return CheckResult.ok("Key is trusted in a cycle");
}
seen.add(fp);
Fingerprint trustedFp = trusted.get(key.getKeyID());
if (trustedFp != null && trustedFp.equals(fp)) {
return CheckResult.trusted(); // Directly trusted.
} else if (depth >= maxTrustDepth) {
return CheckResult.ok("No path of depth <= " + maxTrustDepth + " to a trusted key");
}
List<CheckResult> signerResults = new ArrayList<>();
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.
Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
while (sigs.hasNext()) {
PGPSignature sig = sigs.next();
// TODO(dborowitz): Handle CERTIFICATION_REVOCATION.
if (sig.getSignatureType() != PGPSignature.DEFAULT_CERTIFICATION
&& sig.getSignatureType() != PGPSignature.POSITIVE_CERTIFICATION) {
continue; // Not a certification.
}
PGPPublicKey signer = getSigner(store, sig, userId, key, signerResults);
// TODO(dborowitz): Require self certification.
if (signer == null || Arrays.equals(signer.getFingerprint(), key.getFingerprint())) {
continue;
}
String subpacketProblem = checkTrustSubpacket(sig, depth);
if (subpacketProblem == null) {
CheckResult signerResult = check(signer, depth + 1, false, seen);
if (signerResult.isTrusted()) {
return CheckResult.trusted();
}
}
signerResults.add(
CheckResult.ok(
"Certification by " + keyToString(signer) + " is valid, but key is not trusted"));
}
}
List<String> problems = new ArrayList<>();
problems.add("No path to a trusted key");
for (CheckResult signerResult : signerResults) {
problems.addAll(signerResult.getProblems());
}
return CheckResult.create(OK, problems);
}
private static PGPPublicKey getSigner(
PublicKeyStore store,
PGPSignature sig,
String userId,
PGPPublicKey key,
List<CheckResult> results) {
try {
PGPPublicKeyRingCollection signers = store.get(sig.getKeyID());
if (!signers.getKeyRings().hasNext()) {
results.add(
CheckResult.ok(
"Key "
+ keyIdToString(sig.getKeyID())
+ " used for certification is not in store"));
return null;
}
PGPPublicKey signer = PublicKeyStore.getSigner(signers, sig, userId, key);
if (signer == null) {
results.add(
CheckResult.ok("Certification by " + keyIdToString(sig.getKeyID()) + " is not valid"));
return null;
}
return signer;
} catch (PGPException | IOException e) {
results.add(
CheckResult.ok("Error checking certification by " + keyIdToString(sig.getKeyID())));
return null;
}
}
private String checkTrustSubpacket(PGPSignature sig, int depth) {
SignatureSubpacket trustSub =
sig.getHashedSubPackets().getSubpacket(SignatureSubpacketTags.TRUST_SIG);
if (trustSub == null || trustSub.getData().length != 2) {
return "Certification is missing trust information";
}
byte amount = trustSub.getData()[1];
if (amount < COMPLETE_TRUST) {
return "Certification does not fully trust key";
}
byte level = trustSub.getData()[0];
int required = depth + 1;
if (level < required) {
return "Certification trusts to depth " + level + ", but depth " + required + " is required";
}
return null;
}
}

View File

@@ -0,0 +1,421 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import static com.google.common.base.Preconditions.checkState;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.bcpg.ArmoredOutputStream;
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.bc.BcPGPObjectFactory;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.NB;
/**
* 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 static final ObjectId EMPTY_TREE =
ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904");
/** Ref where GPG public keys are stored. */
public static final String REFS_GPG_KEYS = "refs/meta/gpg-keys";
/**
* Choose the public key that produced a signature.
*
* <p>
*
* @param keyRings candidate keys.
* @param sig signature object.
* @param data signed payload.
* @return the key chosen from {@code keyRings} that was able to verify the signature, or {@code
* null} if none was found.
* @throws PGPException if an error occurred verifying the signature.
*/
public static PGPPublicKey getSigner(
Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, byte[] data) throws PGPException {
for (PGPPublicKeyRing kr : keyRings) {
PGPPublicKey k = kr.getPublicKey();
sig.init(new BcPGPContentVerifierBuilderProvider(), k);
sig.update(data);
if (sig.verify()) {
return k;
}
}
return null;
}
/**
* Choose the public key that produced a certification.
*
* <p>
*
* @param keyRings candidate keys.
* @param sig signature object.
* @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
* {@code null} if none was found.
* @throws PGPException if an error occurred verifying the certification.
*/
public static PGPPublicKey getSigner(
Iterable<PGPPublicKeyRing> keyRings, PGPSignature sig, String userId, PGPPublicKey key)
throws PGPException {
for (PGPPublicKeyRing kr : keyRings) {
PGPPublicKey k = kr.getPublicKey();
sig.init(new BcPGPContentVerifierBuilderProvider(), k);
if (sig.verifyCertification(userId, key)) {
return k;
}
}
return null;
}
private final Repository repo;
private ObjectReader reader;
private RevCommit tip;
private NoteMap notes;
private Map<Fingerprint, PGPPublicKeyRing> toAdd;
private Set<Fingerprint> toRemove;
/** @param repo repository to read keys from. */
public PublicKeyStore(Repository repo) {
this.repo = repo;
toAdd = new HashMap<>();
toRemove = new HashSet<>();
}
@Override
public void close() {
reset();
}
private void reset() {
if (reader != null) {
reader.close();
reader = null;
notes = null;
}
}
private void load() throws IOException {
reset();
reader = repo.newObjectReader();
Ref ref = repo.getRefDatabase().exactRef(REFS_GPG_KEYS);
if (ref == null) {
return;
}
try (RevWalk rw = new RevWalk(reader)) {
tip = rw.parseCommit(ref.getObjectId());
notes = NoteMap.read(reader, tip);
}
}
/**
* 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 {
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 Collections.emptyList();
}
Note note = notes.getNote(keyObjectId(keyId));
if (note == null) {
return Collections.emptyList();
}
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) {
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 keys;
}
}
/**
* Add a public key to the store.
*
* <p>Multiple calls may be made to buffer keys in memory, and they are not saved until {@link
* #save(CommitBuilder)} is called.
*
* @param keyRing a key ring containing exactly one public master key.
*/
public void add(PGPPublicKeyRing keyRing) {
int numMaster = 0;
for (PGPPublicKey key : keyRing) {
if (key.isMasterKey()) {
numMaster++;
}
}
// We could have an additional sanity check to ensure all subkeys belong to
// this master key, but that requires doing actual signature verification
// here. The alternative is insane but harmless.
if (numMaster != 1) {
throw new IllegalArgumentException("Exactly 1 master key is required, found " + numMaster);
}
Fingerprint fp = new Fingerprint(keyRing.getPublicKey().getFingerprint());
toAdd.put(fp, keyRing);
toRemove.remove(fp);
}
/**
* Remove a public key from the store.
*
* <p>Multiple calls may be made to buffer deletes in memory, and they are not saved until {@link
* #save(CommitBuilder)} is called.
*
* @param fingerprint the fingerprint of the key to remove.
*/
public void remove(byte[] fingerprint) {
Fingerprint fp = new Fingerprint(fingerprint);
toAdd.remove(fp);
toRemove.add(fp);
}
/**
* Save pending keys to the store.
*
* <p>One commit is created and the ref updated. The pending list is cleared if and only if the
* ref update succeeds, which allows for easy retries in case of lock failure.
*
* @param cb commit builder with at least author and identity populated; tree and parent are
* ignored.
* @return result of the ref update.
*/
public RefUpdate.Result save(CommitBuilder cb) throws PGPException, IOException {
if (toAdd.isEmpty() && toRemove.isEmpty()) {
return RefUpdate.Result.NO_CHANGE;
}
if (reader == null) {
load();
}
if (notes == null) {
notes = NoteMap.newEmptyMap();
}
ObjectId newTip;
try (ObjectInserter ins = repo.newObjectInserter()) {
for (PGPPublicKeyRing keyRing : toAdd.values()) {
saveToNotes(ins, keyRing);
}
for (Fingerprint fp : toRemove) {
deleteFromNotes(ins, fp);
}
cb.setTreeId(notes.writeTree(ins));
if (cb.getTreeId().equals(tip != null ? tip.getTree() : EMPTY_TREE)) {
return RefUpdate.Result.NO_CHANGE;
}
if (tip != null) {
cb.setParentId(tip);
}
if (cb.getMessage() == null) {
int n = toAdd.size() + toRemove.size();
cb.setMessage(String.format("Update %d public key%s", n, n != 1 ? "s" : ""));
}
newTip = ins.insert(cb);
ins.flush();
}
RefUpdate ru = repo.updateRef(PublicKeyStore.REFS_GPG_KEYS);
ru.setExpectedOldObjectId(tip);
ru.setNewObjectId(newTip);
ru.setRefLogIdent(cb.getCommitter());
ru.setRefLogMessage("Store public keys", true);
RefUpdate.Result result = ru.update();
reset();
switch (result) {
case FAST_FORWARD:
case NEW:
case NO_CHANGE:
toAdd.clear();
toRemove.clear();
break;
case FORCED:
case IO_FAILURE:
case LOCK_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
break;
}
return result;
}
private void saveToNotes(ObjectInserter ins, PGPPublicKeyRing keyRing)
throws PGPException, IOException {
long keyId = keyRing.getPublicKey().getKeyID();
PGPPublicKeyRingCollection existing = get(keyId);
List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size() + 1);
boolean replaced = false;
for (PGPPublicKeyRing kr : existing) {
if (sameKey(keyRing, kr)) {
toWrite.add(keyRing);
replaced = true;
} else {
toWrite.add(kr);
}
}
if (!replaced) {
toWrite.add(keyRing);
}
notes.set(keyObjectId(keyId), ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
}
private void deleteFromNotes(ObjectInserter ins, Fingerprint fp)
throws PGPException, IOException {
long keyId = fp.getId();
PGPPublicKeyRingCollection existing = get(keyId);
List<PGPPublicKeyRing> toWrite = new ArrayList<>(existing.size());
for (PGPPublicKeyRing kr : existing) {
if (!fp.equalsBytes(kr.getPublicKey().getFingerprint())) {
toWrite.add(kr);
}
}
if (toWrite.size() == existing.size()) {
return;
} else if (!toWrite.isEmpty()) {
notes.set(keyObjectId(keyId), ins.insert(OBJ_BLOB, keysToArmored(toWrite)));
} else {
notes.remove(keyObjectId(keyId));
}
}
private static boolean sameKey(PGPPublicKeyRing kr1, PGPPublicKeyRing kr2) {
return Arrays.equals(kr1.getPublicKey().getFingerprint(), kr2.getPublicKey().getFingerprint());
}
private static byte[] keysToArmored(List<PGPPublicKeyRing> keys) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(4096 * keys.size());
for (PGPPublicKeyRing kr : keys) {
try (ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
kr.encode(aout);
}
}
return out.toByteArray();
}
public static String keyToString(PGPPublicKey key) {
Iterator<String> it = key.getUserIDs();
return String.format(
"%s %s(%s)",
keyIdToString(key.getKeyID()),
it.hasNext() ? it.next() + " " : "",
Fingerprint.toString(key.getFingerprint()));
}
public static String keyIdToString(long keyId) {
// Match key ID format from gpg --list-keys.
return String.format("%08X", (int) keyId);
}
static ObjectId keyObjectId(long keyId) {
byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
NB.encodeInt64(buf, 0, keyId);
return ObjectId.fromRaw(buf);
}
}

View File

@@ -0,0 +1,217 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.BAD;
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 com.google.common.base.Joiner;
import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Checker for push certificates. */
public abstract class PushCertificateChecker {
private static final Logger log = LoggerFactory.getLogger(PushCertificateChecker.class);
public static class Result {
private final PGPPublicKey key;
private final CheckResult checkResult;
private Result(PGPPublicKey key, CheckResult checkResult) {
this.key = key;
this.checkResult = checkResult;
}
public PGPPublicKey getPublicKey() {
return key;
}
public CheckResult getCheckResult() {
return checkResult;
}
}
private final PublicKeyChecker publicKeyChecker;
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;
}
/**
* Check a push certificate.
*
* @return result of the check.
*/
public final Result check(PushCertificate cert) {
if (checkNonce && cert.getNonceStatus() != NonceStatus.OK) {
return new Result(null, CheckResult.bad("Invalid nonce"));
}
List<CheckResult> results = new ArrayList<>(2);
Result sigResult = null;
try {
PGPSignature sig = readSignature(cert);
if (sig != null) {
@SuppressWarnings("resource")
Repository repo = getRepository();
try (PublicKeyStore store = new PublicKeyStore(repo)) {
sigResult = checkSignature(sig, cert, store);
results.add(checkCustom(repo));
} finally {
if (shouldClose(repo)) {
repo.close();
}
}
} else {
results.add(CheckResult.bad("Invalid signature format"));
}
} catch (PGPException | IOException e) {
String msg = "Internal error checking push certificate";
log.error(msg, e);
results.add(CheckResult.bad(msg));
}
return combine(sigResult, results);
}
private static Result combine(Result sigResult, List<CheckResult> results) {
// Combine results:
// - If any input result is BAD, the final result is bad.
// - If sigResult is TRUSTED and no other result is BAD, the final result
// is TRUSTED.
// - Otherwise, the result is OK.
List<String> problems = new ArrayList<>();
boolean bad = false;
for (CheckResult result : results) {
problems.addAll(result.getProblems());
bad |= result.getStatus() == BAD;
}
Status status = bad ? BAD : OK;
PGPPublicKey key;
if (sigResult != null) {
key = sigResult.getPublicKey();
CheckResult cr = sigResult.getCheckResult();
problems.addAll(cr.getProblems());
if (cr.getStatus() == BAD) {
status = BAD;
} else if (!bad && cr.getStatus() == TRUSTED) {
status = TRUSTED;
}
} else {
key = null;
}
return new Result(key, CheckResult.create(status, problems));
}
/**
* 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 reports no problems, but may be overridden by subclasses.
*
* @param repo a repository previously returned by {@link #getRepository()}.
* @return the result of the custom check.
*/
protected CheckResult checkCustom(Repository repo) {
return CheckResult.ok();
}
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 Result checkSignature(PGPSignature sig, PushCertificate cert, PublicKeyStore store)
throws PGPException, IOException {
PGPPublicKeyRingCollection keys = store.get(sig.getKeyID());
if (!keys.getKeyRings().hasNext()) {
return new Result(
null,
CheckResult.bad("No public keys found for key ID " + keyIdToString(sig.getKeyID())));
}
PGPPublicKey signer = PublicKeyStore.getSigner(keys, sig, Constants.encode(cert.toText()));
if (signer == null) {
return new Result(
null, CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID()) + " is not valid"));
}
CheckResult result =
publicKeyChecker.setStore(store).setEffectiveTime(sig.getCreationTime()).check(signer);
if (!result.getProblems().isEmpty()) {
StringBuilder err =
new StringBuilder("Invalid public key ")
.append(keyToString(signer))
.append(":\n ")
.append(Joiner.on("\n ").join(result.getProblems()));
return new Result(signer, CheckResult.create(result.getStatus(), err.toString()));
}
return new Result(signer, result);
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.EnableSignedPush;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ReceivePackInitializer;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PreReceiveHook;
import org.eclipse.jgit.transport.PreReceiveHookChain;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.SignedPushConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class SignedPushModule extends AbstractModule {
private static final Logger log = LoggerFactory.getLogger(SignedPushModule.class);
@Override
protected void configure() {
if (!BouncyCastleUtil.havePGP()) {
throw new ProvisionException("Bouncy Castle PGP not installed");
}
bind(PublicKeyStore.class).toProvider(StoreProvider.class);
DynamicSet.bind(binder(), ReceivePackInitializer.class).to(Initializer.class);
}
@Singleton
private static class Initializer implements ReceivePackInitializer {
private final SignedPushConfig signedPushConfig;
private final SignedPushPreReceiveHook hook;
private final ProjectCache projectCache;
@Inject
Initializer(
@GerritServerConfig Config cfg,
@EnableSignedPush boolean enableSignedPush,
SignedPushPreReceiveHook hook,
ProjectCache projectCache) {
this.hook = hook;
this.projectCache = projectCache;
if (enableSignedPush) {
String seed = cfg.getString("receive", null, "certNonceSeed");
if (Strings.isNullOrEmpty(seed)) {
seed = randomString(64);
}
signedPushConfig = new SignedPushConfig();
signedPushConfig.setCertNonceSeed(seed);
signedPushConfig.setCertNonceSlopLimit(
cfg.getInt("receive", null, "certNonceSlop", 5 * 60));
} else {
signedPushConfig = null;
}
}
@Override
public void init(Project.NameKey project, ReceivePack rp) {
ProjectState ps = projectCache.get(project);
if (!ps.isEnableSignedPush()) {
rp.setSignedPushConfig(null);
return;
} else if (signedPushConfig == null) {
log.error(
"receive.enableSignedPush is true for project {} but"
+ " false in gerrit.config, so signed push verification is"
+ " disabled",
project.get());
rp.setSignedPushConfig(null);
return;
}
rp.setSignedPushConfig(signedPushConfig);
List<PreReceiveHook> hooks = new ArrayList<>(3);
if (ps.isRequireSignedPush()) {
hooks.add(SignedPushPreReceiveHook.Required.INSTANCE);
}
hooks.add(hook);
hooks.add(rp.getPreReceiveHook());
rp.setPreReceiveHook(PreReceiveHookChain.newChain(hooks));
}
}
@Singleton
private static class StoreProvider implements Provider<PublicKeyStore> {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
@Inject
StoreProvider(GitRepositoryManager repoManager, AllUsersName allUsers) {
this.repoManager = repoManager;
this.allUsers = allUsers;
}
@Override
public PublicKeyStore get() {
final Repository repo;
try {
repo = repoManager.openRepository(allUsers);
} catch (IOException e) {
throw new ProvisionException("Cannot open " + allUsers, e);
}
return new PublicKeyStore(repo) {
@Override
public void close() {
try {
super.close();
} finally {
repo.close();
}
}
};
}
}
private static String randomString(int len) {
Random random;
try {
random = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append((char) random.nextInt());
}
return sb.toString();
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collection;
import org.eclipse.jgit.transport.PreReceiveHook;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
/**
* Pre-receive hook to check signed pushes.
*
* <p>If configured, prior to processing any push using {@code ReceiveCommits}, requires that any
* push certificate present must be valid.
*/
@Singleton
public class SignedPushPreReceiveHook implements PreReceiveHook {
public static class Required implements PreReceiveHook {
public static final Required INSTANCE = new Required();
@Override
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
if (rp.getPushCertificate() == null) {
rp.sendMessage("ERROR: Signed push is required");
reject(commands, "push cert error");
}
}
private Required() {}
}
private final Provider<IdentifiedUser> user;
private final GerritPushCertificateChecker.Factory checkerFactory;
@Inject
public SignedPushPreReceiveHook(
Provider<IdentifiedUser> user, GerritPushCertificateChecker.Factory checkerFactory) {
this.user = user;
this.checkerFactory = checkerFactory;
}
@Override
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
PushCertificate cert = rp.getPushCertificate();
if (cert == null) {
return;
}
CheckResult result =
checkerFactory.create(user.get()).setCheckNonce(true).check(cert).getCheckResult();
if (!isAllowed(result, commands)) {
for (String problem : result.getProblems()) {
rp.sendMessage(problem);
}
reject(commands, "invalid push cert");
}
}
private static boolean isAllowed(CheckResult result, Collection<ReceiveCommand> commands) {
if (onlyMagicBranches(commands)) {
// Only pushing magic branches: allow a valid push certificate even if the
// key is not ultimately trusted. Assume anyone with Submit permission to
// the branch is able to verify during review that the code is legitimate.
return result.isOk();
}
// Directly updating one or more refs: require a trusted key.
return result.isTrusted();
}
private static boolean onlyMagicBranches(Iterable<ReceiveCommand> commands) {
for (ReceiveCommand c : commands) {
if (!MagicBranch.isMagicBranch(c.getRefName())) {
return false;
}
}
return true;
}
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);
}
}
}
}

View File

@@ -0,0 +1,114 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg.api;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.PushCertificateInfo;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.gpg.GerritPushCertificateChecker;
import com.google.gerrit.gpg.PushCertificateChecker;
import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gerrit.gpg.server.PostGpgKeys;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.api.accounts.GpgApiAdapter;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.PushCertificateParser;
public class GpgApiAdapterImpl implements GpgApiAdapter {
private final Provider<PostGpgKeys> postGpgKeys;
private final Provider<GpgKeys> gpgKeys;
private final GpgKeyApiImpl.Factory gpgKeyApiFactory;
private final GerritPushCertificateChecker.Factory pushCertCheckerFactory;
@Inject
GpgApiAdapterImpl(
Provider<PostGpgKeys> postGpgKeys,
Provider<GpgKeys> gpgKeys,
GpgKeyApiImpl.Factory gpgKeyApiFactory,
GerritPushCertificateChecker.Factory pushCertCheckerFactory) {
this.postGpgKeys = postGpgKeys;
this.gpgKeys = gpgKeys;
this.gpgKeyApiFactory = gpgKeyApiFactory;
this.pushCertCheckerFactory = pushCertCheckerFactory;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account)
throws RestApiException, GpgException {
try {
return gpgKeys.get().list().apply(account);
} catch (OrmException | PGPException | IOException e) {
throw new GpgException(e);
}
}
@Override
public Map<String, GpgKeyInfo> putGpgKeys(
AccountResource account, List<String> add, List<String> delete)
throws RestApiException, GpgException {
GpgKeysInput in = new GpgKeysInput();
in.add = add;
in.delete = delete;
try {
return postGpgKeys.get().apply(account, in);
} catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
throw new GpgException(e);
}
}
@Override
public GpgKeyApi gpgKey(AccountResource account, IdString idStr)
throws RestApiException, GpgException {
try {
return gpgKeyApiFactory.create(gpgKeys.get().parse(account, idStr));
} catch (PGPException | OrmException | IOException e) {
throw new GpgException(e);
}
}
@Override
public PushCertificateInfo checkPushCertificate(String certStr, IdentifiedUser expectedUser)
throws GpgException {
try {
PushCertificate cert = PushCertificateParser.fromString(certStr);
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());
return info;
} catch (IOException e) {
throw new GpgException(e);
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg.api;
import static com.google.gerrit.gpg.server.GpgKey.GPG_KEY_KIND;
import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.PushCertificateInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.gpg.server.DeleteGpgKey;
import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gerrit.gpg.server.PostGpgKeys;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.api.accounts.GpgApiAdapter;
import java.util.List;
import java.util.Map;
public class GpgApiModule extends RestApiModule {
private final boolean enabled;
public GpgApiModule(boolean enabled) {
this.enabled = enabled;
}
@Override
protected void configure() {
if (!enabled) {
bind(GpgApiAdapter.class).to(NoGpgApi.class);
return;
}
bind(GpgApiAdapter.class).to(GpgApiAdapterImpl.class);
factory(GpgKeyApiImpl.Factory.class);
DynamicMap.mapOf(binder(), GPG_KEY_KIND);
child(ACCOUNT_KIND, "gpgkeys").to(GpgKeys.class);
post(ACCOUNT_KIND, "gpgkeys").to(PostGpgKeys.class);
get(GPG_KEY_KIND).to(GpgKeys.Get.class);
delete(GPG_KEY_KIND).to(DeleteGpgKey.class);
}
private static class NoGpgApi implements GpgApiAdapter {
private static final String MSG = "GPG key APIs disabled";
@Override
public boolean isEnabled() {
return false;
}
@Override
public Map<String, GpgKeyInfo> listGpgKeys(AccountResource account) {
throw new NotImplementedException(MSG);
}
@Override
public Map<String, GpgKeyInfo> putGpgKeys(
AccountResource account, List<String> add, List<String> delete) {
throw new NotImplementedException(MSG);
}
@Override
public GpgKeyApi gpgKey(AccountResource account, IdString idStr) {
throw new NotImplementedException(MSG);
}
@Override
public PushCertificateInfo checkPushCertificate(String certStr, IdentifiedUser expectedUser) {
throw new NotImplementedException(MSG);
}
}
}

View File

@@ -0,0 +1,64 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg.api;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.gpg.server.DeleteGpgKey;
import com.google.gerrit.gpg.server.GpgKey;
import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class GpgKeyApiImpl implements GpgKeyApi {
public interface Factory {
GpgKeyApiImpl create(GpgKey rsrc);
}
private final GpgKeys.Get get;
private final DeleteGpgKey delete;
private final GpgKey rsrc;
@Inject
GpgKeyApiImpl(GpgKeys.Get get, DeleteGpgKey delete, @Assisted GpgKey rsrc) {
this.get = get;
this.delete = delete;
this.rsrc = rsrc;
}
@Override
public GpgKeyInfo get() throws RestApiException {
try {
return get.apply(rsrc);
} catch (IOException e) {
throw new RestApiException("Cannot get GPG key", e);
}
}
@Override
public void delete() throws RestApiException {
try {
delete.apply(rsrc, new Input());
} catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete GPG key", e);
}
}
}

View File

@@ -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.gpg.server;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
private final Provider<PersonIdent> serverIdent;
private final Provider<PublicKeyStore> storeProvider;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject
DeleteGpgKey(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<PublicKeyStore> storeProvider,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent;
this.storeProvider = storeProvider;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
@Override
public Response<?> apply(GpgKey rsrc, Input input)
throws ResourceConflictException, PGPException, OrmException, IOException,
ConfigInvalidException {
PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
externalIdsUpdateFactory
.create()
.delete(
rsrc.getUser().getAccountId(),
ExternalId.Key.create(
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
try (PublicKeyStore store = storeProvider.get()) {
store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint());
CommitBuilder cb = new CommitBuilder();
PersonIdent committer = serverIdent.get();
cb.setAuthor(rsrc.getUser().newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
cb.setCommitter(committer);
cb.setMessage("Delete public key " + keyIdToString(key.getKeyID()));
RefUpdate.Result saveResult = store.save(cb);
switch (saveResult) {
case NO_CHANGE:
case FAST_FORWARD:
break;
case FORCED:
case IO_FAILURE:
case LOCK_FAILURE:
case NEW:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
throw new ResourceConflictException("Failed to delete public key: " + saveResult);
}
}
return Response.none();
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg.server;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.inject.TypeLiteral;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
public class GpgKey extends AccountResource {
public static final TypeLiteral<RestView<GpgKey>> GPG_KEY_KIND =
new TypeLiteral<RestView<GpgKey>>() {};
private final PGPPublicKeyRing keyRing;
public GpgKey(IdentifiedUser user, PGPPublicKeyRing keyRing) {
super(user);
this.keyRing = keyRing;
}
public PGPPublicKeyRing getKeyRing() {
return keyRing;
}
}

View File

@@ -0,0 +1,251 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg.server;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.gpg.BouncyCastleUtil;
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.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.eclipse.jgit.util.NB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
private static final Logger log = LoggerFactory.getLogger(GpgKeys.class);
public static final String MIME_TYPE = "application/pgp-keys";
private final DynamicMap<RestView<GpgKey>> views;
private final Provider<CurrentUser> self;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
private final ExternalIds externalIds;
@Inject
GpgKeys(
DynamicMap<RestView<GpgKey>> views,
Provider<CurrentUser> self,
Provider<PublicKeyStore> storeProvider,
GerritPublicKeyChecker.Factory checkerFactory,
ExternalIds externalIds) {
this.views = views;
this.self = self;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
this.externalIds = externalIds;
}
@Override
public ListGpgKeys list() throws ResourceNotFoundException, AuthException {
return new ListGpgKeys();
}
@Override
public GpgKey parse(AccountResource parent, IdString id)
throws ResourceNotFoundException, PGPException, OrmException, IOException {
checkVisible(self, parent);
String str = CharMatcher.whitespace().removeFrom(id.get()).toUpperCase();
if ((str.length() != 8 && str.length() != 40)
|| !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
throw new ResourceNotFoundException(id);
}
byte[] fp = parseFingerprint(id.get(), getGpgExtIds(parent));
try (PublicKeyStore store = storeProvider.get()) {
long keyId = keyId(fp);
for (PGPPublicKeyRing keyRing : store.get(keyId)) {
PGPPublicKey key = keyRing.getPublicKey();
if (Arrays.equals(key.getFingerprint(), fp)) {
return new GpgKey(parent.getUser(), keyRing);
}
}
}
throw new ResourceNotFoundException(id);
}
static byte[] parseFingerprint(String str, Iterable<ExternalId> existingExtIds)
throws ResourceNotFoundException {
str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
if ((str.length() != 8 && str.length() != 40)
|| !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
throw new ResourceNotFoundException(str);
}
byte[] fp = null;
for (ExternalId extId : existingExtIds) {
String fpStr = extId.key().id();
if (!fpStr.endsWith(str)) {
continue;
} else if (fp != null) {
throw new ResourceNotFoundException("Multiple keys found for " + str);
}
fp = BaseEncoding.base16().decode(fpStr);
if (str.length() == 40) {
break;
}
}
if (fp == null) {
throw new ResourceNotFoundException(str);
}
return fp;
}
@Override
public DynamicMap<RestView<GpgKey>> views() {
return views;
}
public class ListGpgKeys implements RestReadView<AccountResource> {
@Override
public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
throws OrmException, PGPException, IOException, ResourceNotFoundException {
checkVisible(self, rsrc);
Map<String, GpgKeyInfo> keys = new HashMap<>();
try (PublicKeyStore store = storeProvider.get()) {
for (ExternalId extId : getGpgExtIds(rsrc)) {
String fpStr = extId.key().id();
byte[] fp = BaseEncoding.base16().decode(fpStr);
boolean found = false;
for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
if (Arrays.equals(keyRing.getPublicKey().getFingerprint(), fp)) {
found = true;
GpgKeyInfo info =
toJson(
keyRing.getPublicKey(), checkerFactory.create(rsrc.getUser(), store), store);
keys.put(info.id, info);
info.id = null;
break;
}
}
if (!found) {
log.warn("No public key stored for fingerprint {}", Fingerprint.toString(fp));
}
}
}
return keys;
}
}
@Singleton
public static class Get implements RestReadView<GpgKey> {
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
@Inject
Get(Provider<PublicKeyStore> storeProvider, GerritPublicKeyChecker.Factory checkerFactory) {
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
}
@Override
public GpgKeyInfo apply(GpgKey rsrc) throws IOException {
try (PublicKeyStore store = storeProvider.get()) {
return toJson(
rsrc.getKeyRing().getPublicKey(),
checkerFactory.create().setExpectedUser(rsrc.getUser()),
store);
}
}
}
private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException {
return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
}
private static long keyId(byte[] fp) {
return NB.decodeInt64(fp, fp.length - 8);
}
static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc)
throws ResourceNotFoundException {
if (!BouncyCastleUtil.havePGP()) {
throw new ResourceNotFoundException("GPG not enabled");
}
if (self.get() != rsrc.getUser()) {
throw new ResourceNotFoundException();
}
}
public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult) throws IOException {
GpgKeyInfo info = new GpgKeyInfo();
if (key != null) {
info.id = PublicKeyStore.keyIdToString(key.getKeyID());
info.fingerprint = Fingerprint.toString(key.getFingerprint());
Iterator<String> userIds = key.getUserIDs();
info.userIds = ImmutableList.copyOf(userIds);
try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
ArmoredOutputStream aout = new ArmoredOutputStream(out)) {
// This is not exactly the key stored in the store, but is equivalent. In
// particular, it will have a Bouncy Castle version string. The armored
// stream reader in PublicKeyStore doesn't give us an easy way to extract
// the original ASCII armor.
key.encode(aout);
info.key = new String(out.toByteArray(), UTF_8);
}
}
info.status = checkResult.getStatus();
info.problems = checkResult.getProblems();
return info;
}
static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker, PublicKeyStore store)
throws IOException {
return toJson(key, checker.setStore(store).check(key));
}
public static void toJson(GpgKeyInfo info, CheckResult checkResult) {
info.status = checkResult.getStatus();
info.problems = checkResult.getProblems();
}
}

View File

@@ -0,0 +1,289 @@
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.gpg.server;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.accounts.GpgKeysInput;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
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.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.mail.send.AddKeySender;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput> {
private final Logger log = LoggerFactory.getLogger(getClass());
private final Provider<PersonIdent> serverIdent;
private final Provider<CurrentUser> self;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
private final AddKeySender.Factory addKeyFactory;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final ExternalIds externalIds;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject
PostGpgKeys(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<CurrentUser> self,
Provider<PublicKeyStore> storeProvider,
GerritPublicKeyChecker.Factory checkerFactory,
AddKeySender.Factory addKeyFactory,
Provider<InternalAccountQuery> accountQueryProvider,
ExternalIds externalIds,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent;
this.self = self;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
this.addKeyFactory = addKeyFactory;
this.accountQueryProvider = accountQueryProvider;
this.externalIds = externalIds;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
@Override
public Map<String, GpgKeyInfo> apply(AccountResource rsrc, GpgKeysInput input)
throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
PGPException, OrmException, IOException, ConfigInvalidException {
GpgKeys.checkVisible(self, rsrc);
Collection<ExternalId> existingExtIds =
externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
try (PublicKeyStore store = storeProvider.get()) {
Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
for (PGPPublicKeyRing keyRing : newKeys) {
PGPPublicKey key = keyRing.getPublicKey();
ExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
Account account = getAccountByExternalId(extIdKey);
if (account != null) {
if (!account.getId().equals(rsrc.getUser().getAccountId())) {
throw new ResourceConflictException("GPG key already associated with another account");
}
} else {
newExtIds.add(ExternalId.create(extIdKey, rsrc.getUser().getAccountId()));
}
}
storeKeys(rsrc, newKeys, toRemove);
List<ExternalId.Key> extIdKeysToRemove =
toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList());
externalIdsUpdateFactory
.create()
.replace(rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
return toJson(newKeys, toRemove, store, rsrc.getUser());
}
}
private Set<Fingerprint> readKeysToRemove(
GpgKeysInput input, Collection<ExternalId> existingExtIds) {
if (input.delete == null || input.delete.isEmpty()) {
return ImmutableSet.of();
}
Set<Fingerprint> fingerprints = Sets.newHashSetWithExpectedSize(input.delete.size());
for (String id : input.delete) {
try {
fingerprints.add(new Fingerprint(GpgKeys.parseFingerprint(id, existingExtIds)));
} catch (ResourceNotFoundException e) {
// Skip removal.
}
}
return fingerprints;
}
private List<PGPPublicKeyRing> readKeysToAdd(GpgKeysInput input, Set<Fingerprint> toRemove)
throws BadRequestException, IOException {
if (input.add == null || input.add.isEmpty()) {
return ImmutableList.of();
}
List<PGPPublicKeyRing> keyRings = new ArrayList<>(input.add.size());
for (String armored : input.add) {
try (InputStream in = new ByteArrayInputStream(armored.getBytes(UTF_8));
ArmoredInputStream ain = new ArmoredInputStream(in)) {
@SuppressWarnings("unchecked")
List<Object> objs = Lists.newArrayList(new BcPGPObjectFactory(ain));
if (objs.size() != 1 || !(objs.get(0) instanceof PGPPublicKeyRing)) {
throw new BadRequestException("Expected exactly one PUBLIC KEY BLOCK");
}
PGPPublicKeyRing keyRing = (PGPPublicKeyRing) objs.get(0);
if (toRemove.contains(new Fingerprint(keyRing.getPublicKey().getFingerprint()))) {
throw new BadRequestException(
"Cannot both add and delete key: " + keyToString(keyRing.getPublicKey()));
}
keyRings.add(keyRing);
}
}
return keyRings;
}
private void storeKeys(
AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Set<Fingerprint> toRemove)
throws BadRequestException, ResourceConflictException, PGPException, IOException {
try (PublicKeyStore store = storeProvider.get()) {
List<String> addedKeys = new ArrayList<>();
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(), store).disableTrust().check(key);
if (!result.isOk()) {
throw new BadRequestException(
String.format(
"Problems with public key %s:\n%s",
keyToString(key), Joiner.on('\n').join(result.getProblems())));
}
addedKeys.add(PublicKeyStore.keyToString(key));
store.add(keyRing);
}
for (Fingerprint fp : toRemove) {
store.remove(fp.get());
}
CommitBuilder cb = new CommitBuilder();
PersonIdent committer = serverIdent.get();
cb.setAuthor(rsrc.getUser().newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
cb.setCommitter(committer);
RefUpdate.Result saveResult = store.save(cb);
switch (saveResult) {
case NEW:
case FAST_FORWARD:
case FORCED:
try {
addKeyFactory.create(rsrc.getUser(), addedKeys).send();
} catch (EmailException e) {
log.error(
"Cannot send GPG key added message to "
+ rsrc.getUser().getAccount().getPreferredEmail(),
e);
}
break;
case NO_CHANGE:
break;
case IO_FAILURE:
case LOCK_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
case REJECTED_MISSING_OBJECT:
case REJECTED_OTHER_REASON:
default:
// TODO(dborowitz): Backoff and retry on LOCK_FAILURE.
throw new ResourceConflictException("Failed to save public keys: " + saveResult);
}
}
}
private ExternalId.Key toExtIdKey(byte[] fp) {
return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
}
private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
if (accountStates.isEmpty()) {
return null;
}
if (accountStates.size() > 1) {
StringBuilder msg = new StringBuilder();
msg.append("GPG key ").append(extIdKey.get()).append(" associated with multiple accounts: ");
Joiner.on(", ")
.appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
log.error(msg.toString());
throw new IllegalStateException(msg.toString());
}
return accountStates.get(0).getAccount();
}
private Map<String, GpgKeyInfo> toJson(
Collection<PGPPublicKeyRing> keys,
Set<Fingerprint> deleted,
PublicKeyStore store,
IdentifiedUser user)
throws IOException {
// Unlike when storing keys, include web-of-trust checks when producing
// result JSON, so the user at least knows of any issues.
PublicKeyChecker checker = checkerFactory.create(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);
GpgKeyInfo info = GpgKeys.toJson(key, result);
infos.put(info.id, info);
info.id = null;
}
for (Fingerprint fp : deleted) {
infos.put(keyIdToString(fp.getId()), new GpgKeyInfo());
}
return infos;
}
}