Merge changes If5b48838,I1a530ed9

* changes:
  Upgrade joda-time to 2.8
  Upgrade Lucene to version 5.2.1
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index c94da7c..e7c5f22 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2771,6 +2771,40 @@
   maxObjectSizeLimit = 40 m
 ----
 
+[[receive.enableSignedPush]]receive.enableSignedPush::
++
+If true, server-side signed push validation is enabled.
++
+When a client pushes with `git push --signed`, this ensures that the
+push certificate is valid and signed with a valid public key stored in
+the `refs/gpg-keys` branch of `All-Users`.
++
+Defaults to false.
+
+[[receive.certNonceSeed]]receive.certNonceSeed::
++
+If set to a non-empty value and server-side signed push validation is
+link:#receive.enableSignedPush[enabled], use this value as the seed to
+the HMAC SHA-1 nonce generator. If unset, a 64-byte random seed will be
+generated at server startup.
++
+As this is used as the seed of a cryptographic algorithm, it is
+recommended to be placed in link:#secure-config[`secure.config`].
++
+Defaults to unset.
+
+[[receive.certNonceSlop]]receive.certNonceSlop::
++
+When validating the nonce passed as part of the signed push protocol,
+accept valid nonces up to this many seconds old. This allows
+certificate verification to work over HTTP where there is a lag between
+the HTTP response providing the nonce to sign and the next request
+containing the signed nonce. This can be significant on large
+repositories, since the lag also includes the time to count objects on
+the client.
++
+Default is 5 minutes.
+
 [[receive.checkMagicRefs]]receive.checkMagicRefs::
 +
 If true, Gerrit will verify the destination repository has
@@ -2972,6 +3006,16 @@
 +
 By default, true, allowing notifications to be sent.
 
+[[sendemail.allowRegisterNewEmail]]sendemail.allowRegisterNewEmail::
++
+Whether users are allowed to register new email addresses.
++
+In addition for the HTTP authentication type
+link:#auth.httpemailheader[auth.httpemailheader] must *not* be set to
+enable registration of new email addresses.
++
+By default, true.
+
 [[sendemail.connectTimeout]]sendemail.connectTimeout::
 +
 The connection timeout of opening a socket connected to a
@@ -3640,7 +3684,7 @@
 By default "Anonymous Coward" is used.
 
 
-== File `etc/secure.config`
+== [[secure.config]]File `etc/secure.config`
 The optional file `'$site_path'/etc/secure.config` overrides (or
 supplements) the settings supplied by `'$site_path'/etc/gerrit.config`.
 The file should be readable only by the daemon process and can be
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 276117b..0d3ff58 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -154,6 +154,16 @@
 +
 The default value for this is true, false disables the checks.
 
+[[receive.enableSignedPush]]receive.enableSignedPush::
++
+Controls whether server-side signed push validation is enabled on the
+project. Only has an effect if signed push validation is enabled on the
+server; see the link:config-gerrit.html#receive.enableSignedPush[global
+configuration] for details.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 1b3e98f..6abfb9e 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -84,8 +84,8 @@
 . link:#subproject[Release Subprojects]
 . link:#build-gerrit[Build the Gerrit Release]
 . link:#publish-gerrit[Publish the Gerrit Release]
-.. link:#extension-and-plugin-api[Publish the Extension and Plugin API Jars]
-.. link:#publish-gerrit-war[Publish the Gerrit WAR (with Core Plugins)]
+.. link:#publish-to-maven-central[Publish the Gerrit artifacts to Maven Central]
+.. link:#publish-to-google-storage[Publish the Gerrit WAR to Google Storage]
 .. link:#push-stable[Push the Stable Branch]
 .. link:#push-tag[Push the Release Tag]
 .. link:#upload-documentation[Upload the Documentation]
@@ -168,15 +168,6 @@
 [[publish-gerrit]]
 === Publish the Gerrit Release
 
-[[publish-gerrit-war]]
-==== Publish the Gerrit WAR (with Core Plugins)
-
-* Upload the WAR to the Google Cloud Storage
-
-** go to https://console.developers.google.com/project/164060093628/storage/gerrit-releases/
-** make sure you are signed in with your Gmail account
-** manually upload the Gerrit WAR file by using the `Upload` button
-
 [[publish-to-maven-central]]
 ==== Publish the Gerrit artifacts to Maven Central
 
@@ -306,6 +297,13 @@
 ** Select `com.google.gerrit` as `Project`.
 
 
+[[publish-to-google-storage]]
+==== Publish the Gerrit WAR to the Google Cloud Storage
+
+* go to https://console.developers.google.com/project/164060093628/storage/gerrit-releases/
+* make sure you are signed in with your Gmail account
+* manually upload the Gerrit WAR file by using the `Upload` button
+
 [[push-stable]]
 ==== Push the Stable Branch
 
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index af7cd88..19f73fa 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
index 8deee62..fec4a58 100755
--- a/Documentation/replace_macros.py
+++ b/Documentation/replace_macros.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 88aec2e..7b5dd44 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -1277,6 +1277,20 @@
 The number of open files.
 |============================
 
+[[receive-info]]
+=== ReceiveInfo
+The `ReceiveInfo` entity contains information about the configuration
+of git-receive-pack behavior on the server.
+
+[options="header",cols="1,^1,5"]
+|=======================================
+|Field Name        ||Description
+|`enableSignedPush`|optional|
+Whether signed push validation support is enabled on the server; see the
+link:config-gerrit.html#receive.certNonceSeed[global configuration] for
+details.
+|=======================================
+
 [[server-info]]
 === ServerInfo
 The `ServerInfo` entity contains information about the configuration of
@@ -1306,6 +1320,9 @@
 |`gitweb `                 |optional|
 Information about the link:config-gerrit.html#gitweb[gitweb]
 configuration as link:#git-web-info[GitwebInfo] entity.
+|`receive`                 |optional|
+Information about the receive-pack configuration as a
+link:#receive-info[ReceiveInfo] entity.
 |`sshd`                    |optional|
 Information about the configuration from the
 link:config-gerrit.html#sshd[sshd] section as link:#sshd-info[SshdInfo]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index a25c7bb..4658e2c 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -731,6 +731,7 @@
     "use_content_merge": "INHERIT",
     "use_signed_off_by": "INHERIT",
     "create_new_change_for_all_not_in_target": "INHERIT",
+    "enable_signed_push": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -774,6 +775,11 @@
       "configured_value": "TRUE",
       "inherited_value": true
     },
+    "enable_signed_push": {
+      "value": true,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "max_object_size_limit": {
       "value": "10m",
       "configured_value": "10m",
@@ -1902,6 +1908,9 @@
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
 to a branch or tag.
+|`enable_signed_push`                      |optional|
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+signed push validation is enabled on the project.
 |`max_object_size_limit`     ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
diff --git a/contrib/git-push-review b/contrib/git-push-review
index 898b023..e77785a 100755
--- a/contrib/git-push-review
+++ b/contrib/git-push-review
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2014 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 9ae95b5..d837565 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -23,8 +23,8 @@
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.api.projects.PutDescriptionInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
 
 import org.junit.Test;
 
@@ -51,21 +51,25 @@
             .name);
   }
 
-  @Test(expected = RestApiException.class)
-  public void createProjectFooBar() throws Exception {
+  @Test
+  public void createProjectWithMismatchedInput() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("foo");
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("name must match input.name");
     gApi.projects()
         .name("bar")
         .create(in);
   }
 
-  @Test(expected = ResourceConflictException.class)
+  @Test
   public void createProjectDuplicate() throws Exception {
     ProjectInput in = new ProjectInput();
     in.name = name("baz");
     gApi.projects()
         .create(in);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Project already exists");
     gApi.projects()
         .create(in);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 86f543a..6fbd678 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -42,6 +42,7 @@
   String useContributorAgreements();
   String useSignedOffBy();
   String createNewChangeForAllNotInTarget();
+  String enableSignedPush();
   String requireChangeID();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index affbe61..8aaa95e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -24,6 +24,7 @@
 useContributorAgreements = Require a valid contributor agreement to upload
 useSignedOffBy = Require <code>Signed-off-by</code> in commit message
 createNewChangeForAllNotInTarget = Create a new change for every commit not in the target branch
+enableSignedPush = Enable signed push
 requireChangeID = Require <code>Change-Id</code> in commit message
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 104ef93..e6f262c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -82,6 +82,7 @@
   private ListBox state;
   private ListBox contentMerge;
   private ListBox newChangeForAllNotInTarget;
+  private ListBox enableSignedPush;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
   private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -162,6 +163,9 @@
     submitType.setEnabled(isOwner);
     setEnabledForUseContentMerge();
     newChangeForAllNotInTarget.setEnabled(isOwner);
+    if (enableSignedPush != null) {
+      enableSignedPush.setEnabled(isOwner);
+    }
     descTxt.setEnabled(isOwner);
     contributorAgreements.setEnabled(isOwner);
     signedOffBy.setEnabled(isOwner);
@@ -226,6 +230,12 @@
     saveEnabler.listenTo(requireChangeID);
     grid.addHtml(Util.C.requireChangeID(), requireChangeID);
 
+    if (Gerrit.info().receive().enableSignedPush()) {
+      enableSignedPush = newInheritedBooleanBox();
+      saveEnabler.listenTo(enableSignedPush);
+      grid.add(Util.C.enableSignedPush(), enableSignedPush);
+    }
+
     maxObjectSizeLimit = new NpTextBox();
     saveEnabler.listenTo(maxObjectSizeLimit);
     effectiveMaxObjectSizeLimit = new Label();
@@ -349,6 +359,9 @@
     setBool(contentMerge, result.useContentMerge());
     setBool(newChangeForAllNotInTarget, result.createNewChangeForAllNotInTarget());
     setBool(requireChangeID, result.requireChangeId());
+    if (enableSignedPush != null) {
+      setBool(enableSignedPush, result.enableSignedPush());
+    }
     setSubmitType(result.submitType());
     setState(result.state());
     maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
@@ -618,9 +631,12 @@
   private void doSave() {
     enableForm(false);
     saveProject.setEnabled(false);
+    InheritableBoolean sp = enableSignedPush != null
+        ? getBool(enableSignedPush) : null;
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
+        sp,
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
index 72b1f4b..1030f81 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
@@ -26,6 +26,7 @@
   public final native SshdInfo sshd() /*-{ return this.sshd; }-*/;
   public final native SuggestInfo suggest() /*-{ return this.suggest; }-*/;
   public final native UserConfigInfo user() /*-{ return this.user; }-*/;
+  public final native ReceiveInfo receive() /*-{ return this.receive; }-*/;
 
   public final boolean hasContactStore() {
     return contactStore() != null;
@@ -74,4 +75,12 @@
     protected UserConfigInfo() {
     }
   }
+
+  public static class ReceiveInfo extends JavaScriptObject {
+    public final native boolean enableSignedPush()
+    /*-{ return this.enable_signed_push || false; }-*/;
+
+    protected ReceiveInfo() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index b91c5de..2c0bab3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -50,6 +50,9 @@
   public final native InheritedBooleanInfo useSignedOffBy()
   /*-{ return this.use_signed_off_by; }-*/;
 
+  public final native InheritedBooleanInfo enableSignedPush()
+  /*-{ return this.enable_signed_push; }-*/;
+
   public final SubmitType submitType() {
     return SubmitType.valueOf(submitTypeRaw());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index d81dfe5..53eba42 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -99,7 +99,9 @@
       InheritableBoolean useContributorAgreements,
       InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
       InheritableBoolean createNewChangeForAllNotInTarget,
-      InheritableBoolean requireChangeId, String maxObjectSizeLimit,
+      InheritableBoolean requireChangeId,
+      InheritableBoolean enableSignedPush,
+      String maxObjectSizeLimit,
       SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
       AsyncCallback<ConfigInfo> cb) {
@@ -110,6 +112,9 @@
     in.setUseSignedOffBy(useSignedOffBy);
     in.setRequireChangeId(requireChangeId);
     in.setCreateNewChangeForAllNotInTarget(createNewChangeForAllNotInTarget);
+    if (enableSignedPush != null) {
+      in.setEnableSignedPush(enableSignedPush);
+    }
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -230,6 +235,12 @@
     private final native void setCreateNewChangeForAllNotInTargetRaw(String v)
     /*-{ if(v)this.create_new_change_for_all_not_in_target=v; }-*/;
 
+    final void setEnableSignedPush(InheritableBoolean v) {
+      setEnableSignedPushRaw(v.name());
+    }
+    private final native void setEnableSignedPushRaw(String v)
+    /*-{ if(v)this.enable_signed_push=v; }-*/;
+
     final native void setMaxObjectSizeLimit(String l)
     /*-{ if(l)this.max_object_size_limit=l; }-*/;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 7bff4b7..d6f7b72 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -523,7 +523,7 @@
       InstantiationException, InvocationTargetException, MethodNotAllowedException {
     if (isType(JSON_TYPE, req.getContentType())) {
       try (BufferedReader br = req.getReader();
-          JsonReader json = new JsonReader(br);) {
+          JsonReader json = new JsonReader(br)) {
         json.setLenient(true);
 
         JsonToken first;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index 209998a..ce1b27f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -96,6 +96,8 @@
 
   protected InheritableBoolean createNewChangeForAllNotInTarget;
 
+  protected InheritableBoolean enableSignedPush;
+
   protected Project() {
   }
 
@@ -108,6 +110,7 @@
     requireChangeID = InheritableBoolean.INHERIT;
     useContentMerge = InheritableBoolean.INHERIT;
     createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
+    enableSignedPush = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -171,6 +174,14 @@
     this.createNewChangeForAllNotInTarget = useAllNotInTarget;
   }
 
+  public InheritableBoolean getEnableSignedPush() {
+    return enableSignedPush;
+  }
+
+  public void setEnableSignedPush(InheritableBoolean enable) {
+    enableSignedPush = enable;
+  }
+
   public void setMaxObjectSizeLimit(final String limit) {
     maxObjectSizeLimit = limit;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index da66929..a36d716 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -57,6 +57,12 @@
 
   public static final String EDIT_PREFIX = "edit-";
 
+  /**
+   * Special ref for GPG public keys used by {@link
+   * com.google.gerrit.server.git.SignedPushPreReceiveHook}.
+   */
+  public static final String REFS_GPG_KEYS = REFS + "gpg-keys";
+
   public static String fullName(String ref) {
     return ref.startsWith(REFS) ? ref : REFS_HEADS + ref;
   }
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 3bc55e9..bea53ec 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -213,6 +213,9 @@
     '//lib:grappa',
     '//lib:gwtorm',
     '//lib:truth',
+    '//lib/bouncycastle:bcprov',
+    '//lib/bouncycastle:bcpg',
+    '//lib/bouncycastle:bcpkix',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/jgit:jgit',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index d302a8e..a0fadb5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -94,17 +94,16 @@
       throw new AuthException("not allowed to use no_confirmation");
     }
 
+    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+      throw new MethodNotAllowedException("realm does not allow adding emails");
+    }
+
     return apply(rsrc.getUser(), input);
   }
 
   public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
       throws AuthException, BadRequestException, ResourceConflictException,
-      ResourceNotFoundException, OrmException, EmailException,
-      MethodNotAllowedException {
-    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
-      throw new MethodNotAllowedException("realm does not allow adding emails");
-    }
-
+      ResourceNotFoundException, OrmException, EmailException {
     if (input.email != null && !email.equals(input.email)) {
       throw new BadRequestException("email address must match URL");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 362a39f..7669cf9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.mail.EmailSettings;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -27,13 +28,17 @@
 @Singleton
 public class DefaultRealm extends AbstractRealm {
   private final EmailExpander emailExpander;
+  private final EmailSettings emailSettings;
   private final AccountByEmailCache byEmail;
   private final AuthConfig authConfig;
 
   @Inject
-  DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail, final AuthConfig authConfig) {
+  DefaultRealm(EmailExpander emailExpander,
+      EmailSettings emailSettings,
+      AccountByEmailCache byEmail,
+      AuthConfig authConfig) {
     this.emailExpander = emailExpander;
+    this.emailSettings = emailSettings;
     this.byEmail = byEmail;
     this.authConfig = authConfig;
   }
@@ -47,12 +52,18 @@
         case FULL_NAME:
           return Strings.emptyToNull(authConfig.getHttpDisplaynameHeader()) == null;
         case REGISTER_NEW_EMAIL:
-          return Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
+          return emailSettings.allowRegisterNewEmail
+              && Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
         default:
           return true;
       }
     } else {
-      return true;
+      switch (field) {
+        case REGISTER_NEW_EMAIL:
+          return emailSettings.allowRegisterNewEmail;
+        default:
+          return true;
+      }
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 889965a..27779f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.mail.EmailSettings;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -75,9 +76,10 @@
 
   @Inject
   LdapRealm(
-      final Helper helper,
-      final AuthConfig authConfig,
-      final EmailExpander emailExpander,
+      Helper helper,
+      AuthConfig authConfig,
+      EmailExpander emailExpander,
+      EmailSettings emailSettings,
       @Named(LdapModule.GROUP_CACHE) final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
       @Named(LdapModule.USERNAME_CACHE) final LoadingCache<String, Optional<Account.Id>> usernameCache,
       @GerritServerConfig final Config config) {
@@ -96,6 +98,9 @@
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
       readOnlyAccountFields.add(Account.FieldName.USER_NAME);
     }
+    if (!emailSettings.allowRegisterNewEmail) {
+      readOnlyAccountFields.add(Account.FieldName.REGISTER_NEW_EMAIL);
+    }
 
     fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
index 9e243b0..093506c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -90,7 +90,7 @@
         TimeUnit.MILLISECONDS);
     this.cache =
         CacheBuilder.newBuilder().maximumSize(1)
-            .expireAfterWrite(expiration, TimeUnit.MILLISECONDS)
+            .refreshAfterWrite(expiration, TimeUnit.MILLISECONDS)
             .build(new CacheLoader<Boolean, IndexSearcher>() {
               @Override
               public IndexSearcher load(Boolean key) throws Exception {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 7d2b306..a6a7c9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
+import com.google.gerrit.server.git.SignedPushModule;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -178,6 +179,7 @@
     install(new NoteDbModule());
     install(new PrologModule());
     install(new SshAddressesModule());
+    install(new SignedPushModule());
     install(ThreadLocalRequestContext.module());
 
     bind(AccountResolver.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 673147f..7c39e6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.GetArchive;
+import com.google.gerrit.server.git.SignedPushModule;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -93,6 +94,7 @@
     info.sshd = getSshdInfo(config);
     info.suggest = getSuggestInfo(config);
     info.user = getUserInfo(anonymousCowardName);
+    info.receive = getReceiveInfo(config);
     return info;
   }
 
@@ -266,6 +268,12 @@
     return info;
   }
 
+  private ReceiveInfo getReceiveInfo(Config cfg) {
+    ReceiveInfo info = new ReceiveInfo();
+    info.enableSignedPush = SignedPushModule.isEnabled(cfg);
+    return info;
+  }
+
   private static Boolean toBoolean(boolean v) {
     return v ? v : null;
   }
@@ -280,6 +288,7 @@
     public SshdInfo sshd;
     public SuggestInfo suggest;
     public UserConfigInfo user;
+    public ReceiveInfo receive;
   }
 
   public static class AuthInfo {
@@ -343,4 +352,8 @@
   public static class UserConfigInfo {
     public String anonymousCowardName;
   }
+
+  public static class ReceiveInfo {
+    public Boolean enableSignedPush;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
index f6e08b8..2d0f4f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreModule.java
@@ -18,23 +18,19 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.BouncyCastleUtil;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.ProvisionException;
 
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.openpgp.PGPPublicKey;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.StringUtils;
 
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.security.Security;
 
 /** Creates the {@link ContactStore} based on the configuration. */
 public class ContactStoreModule extends AbstractModule {
@@ -52,7 +48,7 @@
       return new NoContactStore();
     }
 
-    if (!havePGP()) {
+    if (!BouncyCastleUtil.havePGP()) {
       throw new ProvisionException("BouncyCastle PGP not installed; "
           + " needed to encrypt contact information");
     }
@@ -73,25 +69,4 @@
     return new EncryptedContactStore(storeUrl, storeAPPSEC, pubkey, schema,
         connFactory);
   }
-
-  private 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());
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index b25c024..b0107cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -118,6 +118,7 @@
   private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
       "requireContributorAgreement";
   private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
+  private static final String KEY_ENABLE_SIGNED_PUSH = "enableSignedPush";
 
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
@@ -418,6 +419,8 @@
     p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, InheritableBoolean.INHERIT));
     p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, InheritableBoolean.INHERIT));
     p.setCreateNewChangeForAllNotInTarget(getEnum(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, InheritableBoolean.INHERIT));
+    p.setEnableSignedPush(getEnum(rc, RECEIVE, null,
+          KEY_ENABLE_SIGNED_PUSH, InheritableBoolean.INHERIT));
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
@@ -815,6 +818,8 @@
     set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, p.getCreateNewChangeForAllNotInTarget(), InheritableBoolean.INHERIT);
     set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
+    set(rc, RECEIVE, null, KEY_ENABLE_SIGNED_PUSH,
+        p.getEnableSignedPush(), InheritableBoolean.INHERIT);
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushModule.java
new file mode 100644
index 0000000..88a918d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushModule.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.BouncyCastleUtil;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+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;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+
+public class SignedPushModule extends AbstractModule {
+  private static final Logger log =
+      LoggerFactory.getLogger(SignedPushModule.class);
+
+  public static boolean isEnabled(Config cfg) {
+    return cfg.getBoolean("receive", null, "enableSignedPush", false);
+  }
+
+  @Override
+  protected void configure() {
+    if (BouncyCastleUtil.havePGP()) {
+      DynamicSet.bind(binder(), ReceivePackInitializer.class)
+          .to(Initializer.class);
+    } else {
+      log.info("BouncyCastle PGP not installed; signed push verification is"
+          + " disabled");
+    }
+  }
+
+  @Singleton
+  private static class Initializer implements ReceivePackInitializer {
+    private final SignedPushConfig signedPushConfig;
+    private final SignedPushPreReceiveHook hook;
+    private final ProjectCache projectCache;
+
+    @Inject
+    Initializer(@GerritServerConfig Config cfg,
+        SignedPushPreReceiveHook hook,
+        ProjectCache projectCache) {
+      this.hook = hook;
+      this.projectCache = projectCache;
+
+      if (isEnabled(cfg)) {
+        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;
+      }
+      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(signedPushConfig);
+      rp.setPreReceiveHook(PreReceiveHookChain.newChain(Lists.newArrayList(
+          hook, rp.getPreReceiveHook())));
+    }
+  }
+
+  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();
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushPreReceiveHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushPreReceiveHook.java
new file mode 100644
index 0000000..671c109
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SignedPushPreReceiveHook.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static org.bouncycastle.openpgp.PGPSignature.CERTIFICATION_REVOCATION;
+import static org.bouncycastle.openpgp.PGPSignature.DEFAULT_CERTIFICATION;
+import static org.bouncycastle.openpgp.PGPSignature.POSITIVE_CERTIFICATION;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+/**
+ * Pre-receive hook to validate signed pushes.
+ * <p>
+ * If configured, prior to processing any push using {@link ReceiveCommits},
+ * requires that any push certificate present must be valid.
+ */
+@Singleton
+public class SignedPushPreReceiveHook implements PreReceiveHook {
+  private static final Logger log =
+      LoggerFactory.getLogger(SignedPushPreReceiveHook.class);
+
+  private final GitRepositoryManager repoManager;
+  private final AllUsersName allUsers;
+
+  @Inject
+  public SignedPushPreReceiveHook(
+      GitRepositoryManager repoManager,
+      AllUsersName allUsers) {
+    this.repoManager = repoManager;
+    this.allUsers = allUsers;
+  }
+
+  @Override
+  public void onPreReceive(ReceivePack rp,
+      Collection<ReceiveCommand> commands) {
+    try (Writer msgOut = new OutputStreamWriter(rp.getMessageOutputStream())) {
+      PushCertificate cert = rp.getPushCertificate();
+      if (cert == null) {
+        return;
+      }
+      if (cert.getNonceStatus() != NonceStatus.OK) {
+        rejectInvalid(commands);
+        return;
+      }
+      verifySignature(cert, commands, msgOut);
+    } catch (IOException e) {
+      log.error("Error verifying push certificate", e);
+      reject(commands, "push cert error");
+    }
+  }
+
+  private void verifySignature(PushCertificate cert,
+      Collection<ReceiveCommand> commands, Writer msgOut) throws IOException {
+    PGPSignature sig = readSignature(cert);
+    if (sig == null) {
+      msgOut.write("Invalid signature format\n");
+      rejectInvalid(commands);
+      return;
+    }
+    PGPPublicKey key = readPublicKey(sig.getKeyID(), cert.getPusherIdent());
+    if (key == null) {
+      msgOut.write("No valid public key found for ID "
+          + keyIdToString(sig.getKeyID()) + "\n");
+      rejectInvalid(commands);
+      return;
+    }
+    try {
+      sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+      sig.update(Constants.encode(cert.toText()));
+      if (!sig.verify()) {
+        msgOut.write("Push certificate signature does not match\n");
+        rejectInvalid(commands);
+      }
+      return;
+    } catch (PGPException e) {
+      msgOut.write(
+          "Push certificate verification error: " + e.getMessage() + "\n");
+      rejectInvalid(commands);
+      return;
+    }
+  }
+
+  private PGPSignature readSignature(PushCertificate cert) throws IOException {
+    ArmoredInputStream in = new ArmoredInputStream(
+        new ByteArrayInputStream(Constants.encode(cert.getSignature())));
+    PGPObjectFactory factory = new BcPGPObjectFactory(in);
+    PGPSignature sig = null;
+
+    Object obj;
+    while ((obj = factory.nextObject()) != null) {
+      if (!(obj instanceof PGPSignatureList)) {
+        log.error("Unexpected packet in push cert: {}",
+            obj.getClass().getSimpleName());
+        return null;
+      }
+      if (sig != null) {
+        log.error("Multiple signature packets found in push cert");
+        return null;
+      }
+      PGPSignatureList sigs = (PGPSignatureList) obj;
+      if (sigs.size() != 1) {
+        log.error("Expected 1 signature in push cert, found {}", sigs.size());
+        return null;
+      }
+      sig = sigs.get(0);
+    }
+    return sig;
+  }
+
+  private PGPPublicKey readPublicKey(long keyId,
+      PushCertificateIdent expectedIdent) throws IOException {
+    try (Repository repo = repoManager.openRepository(allUsers);
+        RevWalk rw = new RevWalk(repo)) {
+      Ref ref = repo.getRefDatabase().exactRef(RefNames.REFS_GPG_KEYS);
+      if (ref == null) {
+        return null;
+      }
+      NoteMap notes = NoteMap.read(
+          rw.getObjectReader(), rw.parseCommit(ref.getObjectId()));
+      Note note = notes.getNote(keyObjectId(keyId));
+      if (note == null) {
+        return null;
+      }
+
+      try (InputStream objIn =
+              rw.getObjectReader().open(note.getData(), OBJ_BLOB).openStream();
+          ArmoredInputStream in = new ArmoredInputStream(objIn)) {
+        PGPObjectFactory factory = new BcPGPObjectFactory(in);
+        PGPPublicKey matched = null;
+        Object obj;
+        while ((obj = factory.nextObject()) != null) {
+          if (!(obj instanceof PGPPublicKeyRing)) {
+            // TODO(dborowitz): Support assertions signed by a trusted key.
+            log.info("Ignoring {} packet in {}",
+                obj.getClass().getSimpleName(), note.getName());
+            continue;
+          }
+          PGPPublicKeyRing keyRing = (PGPPublicKeyRing) obj;
+          PGPPublicKey key = keyRing.getPublicKey(keyId);
+          if (key == null) {
+            log.warn("Public key ring in {} does not contain key ID {}",
+                note.getName(), keyObjectId(keyId));
+            continue;
+          }
+          if (matched != null) {
+            // TODO(dborowitz): Try all keys.
+            log.warn("Ignoring key with duplicate ID: {}", toString(key));
+            continue;
+          }
+          if (!verifyPublicKey(key, expectedIdent)) {
+            continue;
+          }
+          matched = key;
+        }
+        return matched;
+      }
+    }
+  }
+
+  private boolean verifyPublicKey(PGPPublicKey key,
+      PushCertificateIdent ident) {
+    if (key.isRevoked()) {
+      // TODO(dborowitz): isRevoked is overeager:
+      // http://www.bouncycastle.org/jira/browse/BJB-45
+      log.warn("Key is revoked: {}", toString(key));
+      return false;
+    } else if (key.getValidSeconds() == 0) {
+      log.warn("Key is expired: {}", toString(key));
+      return false;
+    }
+    return verifyPublicKeyCertifications(key, ident);
+  }
+
+  private boolean verifyPublicKeyCertifications(PGPPublicKey key,
+      PushCertificateIdent ident) {
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> sigs = key.getSignaturesForID(ident.getUserId());
+    if (sigs == null) {
+      sigs = Collections.emptyIterator();
+    }
+    boolean valid = false;
+    boolean revoked = false;
+    try {
+      while (sigs.hasNext()) {
+        PGPSignature sig = sigs.next();
+        if (sig.getKeyID() != key.getKeyID()) {
+          // TODO(dborowitz): Support certifications by other trusted keys?
+          continue;
+        } else if (sig.getSignatureType() != DEFAULT_CERTIFICATION
+            && sig.getSignatureType() != POSITIVE_CERTIFICATION
+            && sig.getSignatureType() != CERTIFICATION_REVOCATION) {
+          continue;
+        }
+        sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+        if (sig.verifyCertification(ident.getUserId(), key)) {
+          if (sig.getSignatureType() == CERTIFICATION_REVOCATION) {
+            revoked = true;
+          } else {
+            valid = true;
+          }
+        } else {
+          log.warn("Invalid signature for pusher identity {} in key: {}",
+              ident.getUserId(), toString(key));
+        }
+      }
+    } catch (PGPException e) {
+      log.warn("Error in signature verification for public key", e);
+    }
+
+    if (revoked) {
+      log.warn("Pusher identity {} is revoked in key {}",
+          ident.getUserId(), toString(key));
+      return false;
+    } else if (!valid) {
+      log.warn(
+          "Key does not contain valid certification for pusher identity {}: {}",
+          ident.getUserId(), toString(key));
+      return false;
+    }
+    return true;
+  }
+
+  static ObjectId keyObjectId(long keyId) {
+    // Right-pad key IDs in network byte order to ObjectId length. This allows
+    // us to reuse the fanout code in NoteMap for free. (If we ever fix the
+    // fanout code to work with variable-length byte strings, we will need to
+    // fall back to this key format during a transition period.)
+    ByteBuffer buf = ByteBuffer.wrap(new byte[Constants.OBJECT_ID_LENGTH]);
+    buf.putLong(keyId);
+    return ObjectId.fromRaw(buf.array());
+  }
+
+  static String toString(PGPPublicKey key) {
+    @SuppressWarnings("unchecked")
+    Iterator<String> it = key.getUserIDs();
+    ByteBuffer buf = ByteBuffer.wrap(key.getFingerprint());
+    return String.format(
+        "%s %s(%04X %04X %04X %04X %04X  %04X %04X %04X %04X %04X)",
+        keyIdToString(key.getKeyID()),
+        it.hasNext() ? it.next() + " " : "",
+        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
+        buf.getShort(), buf.getShort(), buf.getShort(), buf.getShort(),
+        buf.getShort(), buf.getShort());
+  }
+
+  private static void reject(Collection<ReceiveCommand> commands,
+      String reason) {
+    for (ReceiveCommand cmd : commands) {
+      if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+        cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
+      }
+    }
+  }
+
+  static String keyIdToString(long keyId) {
+    // Match key ID format from gpg --list-keys.
+    return String.format("%08X", (int) keyId);
+  }
+
+  private static void rejectInvalid(Collection<ReceiveCommand> commands) {
+    reject(commands, "invalid push cert");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
index 7e44877..31135d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -21,12 +21,14 @@
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
-class EmailSettings {
-  final boolean includeDiff;
-  final int maximumDiffSize;
+public class EmailSettings {
+  public final boolean allowRegisterNewEmail;
+  public final boolean includeDiff;
+  public final int maximumDiffSize;
 
   @Inject
   EmailSettings(@GerritServerConfig Config cfg) {
+    allowRegisterNewEmail = cfg.getBoolean("sendemail", "allowRegisterNewEmail", true);
     includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
     maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 1c6782c..28700b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -30,9 +30,12 @@
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.extensions.webui.UiActions;
+import com.google.gerrit.server.git.SignedPushModule;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.inject.util.Providers;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -45,6 +48,7 @@
   public InheritedBooleanInfo useSignedOffBy;
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
+  public InheritedBooleanInfo enableSignedPush;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
   public com.google.gerrit.extensions.client.ProjectState state;
@@ -54,7 +58,8 @@
   public Map<String, CommentLinkInfo> commentlinks;
   public ThemeInfo theme;
 
-  public ConfigInfo(ProjectControl control,
+  public ConfigInfo(Config gerritConfig,
+      ProjectControl control,
       TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
@@ -71,6 +76,7 @@
     InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
     InheritedBooleanInfo createNewChangeForAllNotInTarget =
         new InheritedBooleanInfo();
+    InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
 
     useContributorAgreements.value = projectState.isUseContributorAgreements();
     useSignedOffBy.value = projectState.isUseSignedOffBy();
@@ -86,6 +92,7 @@
     requireChangeId.configuredValue = p.getRequireChangeID();
     createNewChangeForAllNotInTarget.configuredValue =
         p.getCreateNewChangeForAllNotInTarget();
+    enableSignedPush.configuredValue = p.getEnableSignedPush();
 
     ProjectState parentState = Iterables.getFirst(projectState
         .parents(), null);
@@ -97,6 +104,7 @@
       requireChangeId.inheritedValue = parentState.isRequireChangeID();
       createNewChangeForAllNotInTarget.inheritedValue =
           parentState.isCreateNewChangeForAllNotInTarget();
+      enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
     }
 
     this.useContributorAgreements = useContributorAgreements;
@@ -104,6 +112,9 @@
     this.useContentMerge = useContentMerge;
     this.requireChangeId = requireChangeId;
     this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
+    if (SignedPushModule.isEnabled(gerritConfig)) {
+      this.enableSignedPush = enableSignedPush;
+    }
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
     maxObjectSizeLimit.value =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index bb91097..2ab10c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -18,15 +18,18 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
+
 @Singleton
 public class GetConfig implements RestReadView<ProjectResource> {
-
+  private final Config gerritConfig;
   private final TransferConfig config;
   private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
   private final PluginConfigFactory cfgFactory;
@@ -34,11 +37,13 @@
   private final DynamicMap<RestView<ProjectResource>> views;
 
   @Inject
-  public GetConfig(TransferConfig config,
+  public GetConfig(@GerritServerConfig Config gerritConfig,
+      TransferConfig config,
       DynamicMap<ProjectConfigEntry> pluginConfigEntries,
       PluginConfigFactory cfgFactory,
       AllProjectsNameProvider allProjects,
       DynamicMap<RestView<ProjectResource>> views) {
+    this.gerritConfig = gerritConfig;
     this.config = config;
     this.pluginConfigEntries = pluginConfigEntries;
     this.allProjects = allProjects;
@@ -48,7 +53,7 @@
 
   @Override
   public ConfigInfo apply(ProjectResource resource) {
-    return new ConfigInfo(resource.getControl(), config,
+    return new ConfigInfo(gerritConfig, resource.getControl(), config,
         pluginConfigEntries, cfgFactory, allProjects, views);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 2f031a0..1dae042 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -406,6 +406,15 @@
     });
   }
 
+  public boolean isEnableSignedPush() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getEnableSignedPush();
+      }
+    });
+  }
+
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
     for (ProjectState s : treeInOrder()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index f212f67..da7df8c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.config.ProjectConfigEntry;
@@ -47,6 +48,7 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,6 +63,7 @@
 @Singleton
 public class PutConfig implements RestModifyView<ProjectResource, Input> {
   private static final Logger log = LoggerFactory.getLogger(PutConfig.class);
+
   public static class Input {
     public String description;
     public InheritableBoolean useContributorAgreements;
@@ -68,12 +71,14 @@
     public InheritableBoolean useSignedOffBy;
     public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
+    public InheritableBoolean enableSignedPush;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
     public com.google.gerrit.extensions.client.ProjectState state;
     public Map<String, Map<String, ConfigValue>> pluginConfigValues;
   }
 
+  private final Config gerritConfig;
   private final MetaDataUpdate.User metaDataUpdateFactory;
   private final ProjectCache projectCache;
   private final GitRepositoryManager gitMgr;
@@ -87,7 +92,8 @@
   private final ChangeHooks hooks;
 
   @Inject
-  PutConfig(MetaDataUpdate.User metaDataUpdateFactory,
+  PutConfig(@GerritServerConfig Config gerritConfig,
+      MetaDataUpdate.User metaDataUpdateFactory,
       ProjectCache projectCache,
       GitRepositoryManager gitMgr,
       ProjectState.Factory projectStateFactory,
@@ -98,6 +104,7 @@
       DynamicMap<RestView<ProjectResource>> views,
       ChangeHooks hooks,
       Provider<CurrentUser> currentUser) {
+    this.gerritConfig = gerritConfig;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.projectCache = projectCache;
     this.gitMgr = gitMgr;
@@ -161,6 +168,10 @@
         p.setRequireChangeID(input.requireChangeId);
       }
 
+      if (input.enableSignedPush != null) {
+        p.setEnableSignedPush(input.enableSignedPush);
+      }
+
       if (input.maxObjectSizeLimit != null) {
         p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
       }
@@ -203,8 +214,8 @@
       }
 
       ProjectState state = projectStateFactory.create(projectConfig);
-      return new ConfigInfo(state.controlFor(currentUser.get()), config,
-          pluginConfigEntries, cfgFactory, allProjects, views);
+      return new ConfigInfo(gerritConfig, state.controlFor(currentUser.get()),
+          config, pluginConfigEntries, cfgFactory, allProjects, views);
     } catch (ConfigInvalidException err) {
       throw new ResourceConflictException("Cannot read project " + projectName, err);
     } catch (IOException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 1198176..b142bb0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -139,6 +139,7 @@
     p.setUseContentMerge(InheritableBoolean.TRUE);
     p.setUseContributorAgreements(InheritableBoolean.FALSE);
     p.setUseSignedOffBy(InheritableBoolean.FALSE);
+    p.setEnableSignedPush(InheritableBoolean.FALSE);
 
     AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
     AccessSection all = config.getAccessSection(AccessSection.ALL, true);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/BouncyCastleUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/BouncyCastleUtil.java
new file mode 100644
index 0000000..ba87d58
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/BouncyCastleUtil.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPPublicKey;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.security.Security;
+
+/** 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() {
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SignedPushPreReceiveHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SignedPushPreReceiveHookTest.java
new file mode 100644
index 0000000..f0cfe18
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SignedPushPreReceiveHookTest.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.git.SignedPushPreReceiveHook.keyIdToString;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+
+public class SignedPushPreReceiveHookTest {
+  // ./pubring.gpg
+  // -------------
+  // pub   1024R/30A5A053 2015-06-16 [expires: 2015-06-17]
+  //       Key fingerprint = 96D6 DE78 E6D8 DA49 9387  1F31 FA09 A0C4 30A5 A053
+  // uid                  A U. Thor <a_u_thor@example.com>
+  // sub   1024R/D6831DC8 2015-06-16 [expires: 2015-06-17]
+  private static final String PUBKEY =
+      "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+      + "Version: GnuPG v1\n"
+      + "\n"
+      + "mI0EVYCBUQEEALCKzuY6M68RRRm6PS1F322lpHSHTdW9PIURm5B//tbfS32EN6lM\n"
+      + "ISwJxhanpZanv2o4mbV3V8oLT3jMVDPJ3dqmOZJdJs37l+dxCVJ3ycFe1LHtT2oT\n"
+      + "eRyC5PxD7UY5PdDe97mjp7yrp/bx1hE6XqGV0nDGrkJXc8A35u3WzIF5ABEBAAG0\n"
+      + "IEEgVS4gVGhvciA8YV91X3Rob3JAZXhhbXBsZS5jb20+iL4EEwECACgFAlWAgVEC\n"
+      + "GwMFCQABUYAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPoJoMQwpaBTjhoD\n"
+      + "/0MRCX1zBjEKIfzFYeSEg/OcSLbAkUD7un5YTfpgds3oUNIKlIgovWO24TQxrCCu\n"
+      + "5pSzN/WfRSzPFhj9HahY/5yh+EGd6HmIU2v/k5I3LwTPEOcZUi1SzOScSv6JOO9Q\n"
+      + "3srVilCu3h6TNW1UGBNjfOr1NdmkWfsUZcjsEc/XrfBGuI0EVYCBUQEEAL0UP9jJ\n"
+      + "eLj3klCCa2tmwdgyFiSf9T+Yoed4I3v3ag2F0/CWrCJr3e1ogSs4Bdts0WptI+Nu\n"
+      + "QIq40AYszewq55dTcB4lbNAYE4svVYQ5AGz78iKzljaBFhyT6ePdZ5wfb+8Jqu1l\n"
+      + "7wRwzRI5Jn3OXCmdGm/dmoUNG136EA9A4ZLLABEBAAGIpQQYAQIADwUCVYCBUQIb\n"
+      + "DAUJAAFRgAAKCRD6CaDEMKWgU5JTA/9XjwPFZ5NseNROMhYZMmje1/ixISb2jaVc\n"
+      + "9m9RLCl8Y3RCY9NNdU5FinTIX9LsRTrJlW6FSG5sin8mwx9jq0eGE1TBEKND5klT\n"
+      + "TmsG0jx1dZG9kWDy6lPnIWw2/4W+N0fK/Cw6WEL1Xg7RLi4NQ9Bi2WoxJii9bWMv\n"
+      + "yy35U6UfPQ==\n"
+      + "=0GL9\n"
+      + "-----END PGP PUBLIC KEY BLOCK-----\n";
+
+  private PGPPublicKey key;
+
+  @Before
+  public void setUp() throws Exception {
+    ArmoredInputStream in = new ArmoredInputStream(
+        new ByteArrayInputStream(Constants.encode(PUBKEY)));
+    PGPPublicKeyRing keyRing =
+        new PGPPublicKeyRing(in, new BcKeyFingerprintCalculator());
+    key = keyRing.getPublicKey();
+  }
+
+  @Test
+  public void testKeyIdToString() throws Exception {
+    assertThat(keyIdToString(key.getKeyID()))
+        .isEqualTo("30A5A053");
+  }
+
+  @Test
+  public void testKeyToString() throws Exception {
+    assertThat(SignedPushPreReceiveHook.toString(key))
+        .isEqualTo("30A5A053 A U. Thor <a_u_thor@example.com>"
+          + " (96D6 DE78 E6D8 DA49 9387  1F31 FA09 A0C4 30A5 A053)");
+  }
+
+  @Test
+  public void testKeyObjectId() throws Exception {
+    String objId = SignedPushPreReceiveHook.keyObjectId(key.getKeyID()).name();
+    assertThat(objId).isEqualTo("fa09a0c430a5a053000000000000000000000000");
+    assertThat(objId.substring(8, 16))
+        .isEqualTo(keyIdToString(key.getKeyID()).toLowerCase());
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index dc43537..807f78d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -94,6 +94,8 @@
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
     cfg.setInt("sendemail", null, "threadPoolSize", 0);
+    cfg.setBoolean("receive", null, "enableSignedPush", false);
+    cfg.setString("receive", null, "certNonceSeed", "sekret");
   }
 
   private final Config cfg;
diff --git a/tools/download_all.py b/tools/download_all.py
index 241d20b..3b21882 100755
--- a/tools/download_all.py
+++ b/tools/download_all.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/download_file.py b/tools/download_file.py
index 88ab41a..061d67c 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index dd6f248..752b63a 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index cc10816..7017406 100644
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/pack_war.py b/tools/pack_war.py
index 7e7d895..cfa7e36 100755
--- a/tools/pack_war.py
+++ b/tools/pack_war.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/util_test.py b/tools/util_test.py
index f116171..30647ba 100644
--- a/tools/util_test.py
+++ b/tools/util_test.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2013 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/version.py b/tools/version.py
index 28f6b65..eb1e076 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # Copyright (C) 2014 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");