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");