Dissolve gerrit-gpg top-level directory
Change-Id: Ib102fce79dc9041fc9f091def4eacaaaddd71bcf
This commit is contained in:
		
				
					committed by
					
						
						Dave Borowitz
					
				
			
			
				
	
			
			
			
						parent
						
							a84150efe0
						
					
				
				
					commit
					4f4f034b00
				
			
							
								
								
									
										19
									
								
								java/com/google/gerrit/gpg/BUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								java/com/google/gerrit/gpg/BUILD
									
									
									
									
									
										Normal 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",
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										58
									
								
								java/com/google/gerrit/gpg/BouncyCastleUtil.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								java/com/google/gerrit/gpg/BouncyCastleUtil.java
									
									
									
									
									
										Normal 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() {}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								java/com/google/gerrit/gpg/CheckResult.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								java/com/google/gerrit/gpg/CheckResult.java
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								java/com/google/gerrit/gpg/Fingerprint.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								java/com/google/gerrit/gpg/Fingerprint.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										252
									
								
								java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
									
									
									
									
									
										Normal 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()));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								java/com/google/gerrit/gpg/GpgModule.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								java/com/google/gerrit/gpg/GpgModule.java
									
									
									
									
									
										Normal 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));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										474
									
								
								java/com/google/gerrit/gpg/PublicKeyChecker.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								java/com/google/gerrit/gpg/PublicKeyChecker.java
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										421
									
								
								java/com/google/gerrit/gpg/PublicKeyStore.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								java/com/google/gerrit/gpg/PublicKeyStore.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										217
									
								
								java/com/google/gerrit/gpg/PushCertificateChecker.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								java/com/google/gerrit/gpg/PushCertificateChecker.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										160
									
								
								java/com/google/gerrit/gpg/SignedPushModule.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								java/com/google/gerrit/gpg/SignedPushModule.java
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										103
									
								
								java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								java/com/google/gerrit/gpg/api/GpgApiModule.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								java/com/google/gerrit/gpg/api/GpgApiModule.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										98
									
								
								java/com/google/gerrit/gpg/server/DeleteGpgKey.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								java/com/google/gerrit/gpg/server/DeleteGpgKey.java
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								java/com/google/gerrit/gpg/server/GpgKey.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								java/com/google/gerrit/gpg/server/GpgKey.java
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										251
									
								
								java/com/google/gerrit/gpg/server/GpgKeys.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								java/com/google/gerrit/gpg/server/GpgKeys.java
									
									
									
									
									
										Normal 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										289
									
								
								java/com/google/gerrit/gpg/server/PostGpgKeys.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								java/com/google/gerrit/gpg/server/PostGpgKeys.java
									
									
									
									
									
										Normal 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user