Merge changes from topic 'signed-push'
* changes: Configure signed push verification on a per-project basis Configure signed push globally in Gerrit config Verify pusher identity against public key Add basic signed push support Extract havePGP() to its own utility class
This commit is contained in:
commit
c2b94eb946
@ -2771,6 +2771,40 @@ behavior of Gerrit's 'receive-pack' mechanism.
|
||||
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
|
||||
@ -3650,7 +3684,7 @@ notifications if the full name of the user is not set.
|
||||
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
|
||||
|
@ -154,6 +154,16 @@ up to Gerrit then the JGit checks need to be disabled.
|
||||
+
|
||||
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
|
||||
|
||||
|
@ -1277,6 +1277,20 @@ The maximal memory size. The value is returned with a unit abbreviation
|
||||
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 @@ GerritInfo] entity.
|
||||
|`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]
|
||||
|
@ -731,6 +731,7 @@ link:#config-input[ConfigInput] entity.
|
||||
"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 @@ ConfigInfo] entity.
|
||||
"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 @@ link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
|
||||
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[
|
||||
|
@ -42,6 +42,7 @@ public interface AdminConstants extends Constants {
|
||||
String useContributorAgreements();
|
||||
String useSignedOffBy();
|
||||
String createNewChangeForAllNotInTarget();
|
||||
String enableSignedPush();
|
||||
String requireChangeID();
|
||||
String headingMaxObjectSizeLimit();
|
||||
String headingGroupOptions();
|
||||
|
@ -24,6 +24,7 @@ useContentMerge = Allow content merges
|
||||
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
|
||||
|
@ -82,6 +82,7 @@ public class ProjectInfoScreen extends ProjectScreen {
|
||||
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 @@ public class ProjectInfoScreen extends ProjectScreen {
|
||||
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 @@ public class ProjectInfoScreen extends ProjectScreen {
|
||||
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 @@ public class ProjectInfoScreen extends ProjectScreen {
|
||||
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 @@ public class ProjectInfoScreen extends ProjectScreen {
|
||||
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())),
|
||||
|
@ -26,6 +26,7 @@ public class ServerInfo extends JavaScriptObject {
|
||||
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 @@ public class ServerInfo extends JavaScriptObject {
|
||||
protected UserConfigInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReceiveInfo extends JavaScriptObject {
|
||||
public final native boolean enableSignedPush()
|
||||
/*-{ return this.enable_signed_push || false; }-*/;
|
||||
|
||||
protected ReceiveInfo() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ public class ConfigInfo extends JavaScriptObject {
|
||||
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());
|
||||
}
|
||||
|
@ -99,7 +99,9 @@ public class ProjectApi {
|
||||
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 @@ public class ProjectApi {
|
||||
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 @@ public class ProjectApi {
|
||||
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; }-*/;
|
||||
|
||||
|
@ -96,6 +96,8 @@ public final class Project {
|
||||
|
||||
protected InheritableBoolean createNewChangeForAllNotInTarget;
|
||||
|
||||
protected InheritableBoolean enableSignedPush;
|
||||
|
||||
protected Project() {
|
||||
}
|
||||
|
||||
@ -108,6 +110,7 @@ public final class Project {
|
||||
requireChangeID = InheritableBoolean.INHERIT;
|
||||
useContentMerge = InheritableBoolean.INHERIT;
|
||||
createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
|
||||
enableSignedPush = InheritableBoolean.INHERIT;
|
||||
}
|
||||
|
||||
public Project.NameKey getNameKey() {
|
||||
@ -171,6 +174,14 @@ public final class Project {
|
||||
this.createNewChangeForAllNotInTarget = useAllNotInTarget;
|
||||
}
|
||||
|
||||
public InheritableBoolean getEnableSignedPush() {
|
||||
return enableSignedPush;
|
||||
}
|
||||
|
||||
public void setEnableSignedPush(InheritableBoolean enable) {
|
||||
enableSignedPush = enable;
|
||||
}
|
||||
|
||||
public void setMaxObjectSizeLimit(final String limit) {
|
||||
maxObjectSizeLimit = limit;
|
||||
}
|
||||
|
@ -57,6 +57,12 @@ public class RefNames {
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -213,6 +213,9 @@ java_test(
|
||||
'//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',
|
||||
|
@ -79,6 +79,7 @@ import com.google.gerrit.server.git.MergeQueue;
|
||||
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 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
install(new NoteDbModule());
|
||||
install(new PrologModule());
|
||||
install(new SshAddressesModule());
|
||||
install(new SignedPushModule());
|
||||
install(ThreadLocalRequestContext.module());
|
||||
|
||||
bind(AccountResolver.class);
|
||||
|
@ -29,6 +29,7 @@ import com.google.gerrit.reviewdb.client.AuthType;
|
||||
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 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
|
||||
info.sshd = getSshdInfo(config);
|
||||
info.suggest = getSuggestInfo(config);
|
||||
info.user = getUserInfo(anonymousCowardName);
|
||||
info.receive = getReceiveInfo(config);
|
||||
return info;
|
||||
}
|
||||
|
||||
@ -266,6 +268,12 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
|
||||
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 class GetServerInfo implements RestReadView<ConfigResource> {
|
||||
public SshdInfo sshd;
|
||||
public SuggestInfo suggest;
|
||||
public UserConfigInfo user;
|
||||
public ReceiveInfo receive;
|
||||
}
|
||||
|
||||
public static class AuthInfo {
|
||||
@ -343,4 +352,8 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
|
||||
public static class UserConfigInfo {
|
||||
public String anonymousCowardName;
|
||||
}
|
||||
|
||||
public static class ReceiveInfo {
|
||||
public Boolean enableSignedPush;
|
||||
}
|
||||
}
|
||||
|
@ -18,23 +18,19 @@ import com.google.gerrit.common.Nullable;
|
||||
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 @@ public class ContactStoreModule extends AbstractModule {
|
||||
return new NoContactStore();
|
||||
}
|
||||
|
||||
if (!havePGP()) {
|
||||
if (!BouncyCastleUtil.havePGP()) {
|
||||
throw new ProvisionException("BouncyCastle PGP not installed; "
|
||||
+ " needed to encrypt contact information");
|
||||
}
|
||||
@ -73,25 +69,4 @@ public class ContactStoreModule extends AbstractModule {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +118,7 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
|
||||
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 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
|
||||
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 @@ public class ProjectConfig extends VersionedMetaData implements ValidationError.
|
||||
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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -30,9 +30,12 @@ import com.google.gerrit.server.config.PluginConfig;
|
||||
import com.google.gerrit.server.config.PluginConfigFactory;
|
||||
import com.google.gerrit.server.config.ProjectConfigEntry;
|
||||
import com.google.gerrit.server.extensions.webui.UiActions;
|
||||
import com.google.gerrit.server.git.SignedPushModule;
|
||||
import com.google.gerrit.server.git.TransferConfig;
|
||||
import com.google.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 class ConfigInfo {
|
||||
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 class ConfigInfo {
|
||||
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 @@ public class ConfigInfo {
|
||||
InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
|
||||
InheritedBooleanInfo createNewChangeForAllNotInTarget =
|
||||
new InheritedBooleanInfo();
|
||||
InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
|
||||
|
||||
useContributorAgreements.value = projectState.isUseContributorAgreements();
|
||||
useSignedOffBy.value = projectState.isUseSignedOffBy();
|
||||
@ -86,6 +92,7 @@ public class ConfigInfo {
|
||||
requireChangeId.configuredValue = p.getRequireChangeID();
|
||||
createNewChangeForAllNotInTarget.configuredValue =
|
||||
p.getCreateNewChangeForAllNotInTarget();
|
||||
enableSignedPush.configuredValue = p.getEnableSignedPush();
|
||||
|
||||
ProjectState parentState = Iterables.getFirst(projectState
|
||||
.parents(), null);
|
||||
@ -97,6 +104,7 @@ public class ConfigInfo {
|
||||
requireChangeId.inheritedValue = parentState.isRequireChangeID();
|
||||
createNewChangeForAllNotInTarget.inheritedValue =
|
||||
parentState.isCreateNewChangeForAllNotInTarget();
|
||||
enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
|
||||
}
|
||||
|
||||
this.useContributorAgreements = useContributorAgreements;
|
||||
@ -104,6 +112,9 @@ public class ConfigInfo {
|
||||
this.useContentMerge = useContentMerge;
|
||||
this.requireChangeId = requireChangeId;
|
||||
this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
|
||||
if (SignedPushModule.isEnabled(gerritConfig)) {
|
||||
this.enableSignedPush = enableSignedPush;
|
||||
}
|
||||
|
||||
MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
|
||||
maxObjectSizeLimit.value =
|
||||
|
@ -18,15 +18,18 @@ import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
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 @@ public class GetConfig implements RestReadView<ProjectResource> {
|
||||
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 @@ public class GetConfig implements RestReadView<ProjectResource> {
|
||||
|
||||
@Override
|
||||
public ConfigInfo apply(ProjectResource resource) {
|
||||
return new ConfigInfo(resource.getControl(), config,
|
||||
return new ConfigInfo(gerritConfig, resource.getControl(), config,
|
||||
pluginConfigEntries, cfgFactory, allProjects, views);
|
||||
}
|
||||
}
|
||||
|
@ -406,6 +406,15 @@ public class ProjectState {
|
||||
});
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
@ -33,6 +33,7 @@ import com.google.gerrit.reviewdb.client.RefNames;
|
||||
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 com.google.inject.Singleton;
|
||||
|
||||
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 @@ import java.util.Objects;
|
||||
@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 class PutConfig implements RestModifyView<ProjectResource, Input> {
|
||||
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 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
|
||||
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 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
|
||||
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 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
|
||||
p.setRequireChangeID(input.requireChangeId);
|
||||
}
|
||||
|
||||
if (input.enableSignedPush != null) {
|
||||
p.setEnableSignedPush(input.enableSignedPush);
|
||||
}
|
||||
|
||||
if (input.maxObjectSizeLimit != null) {
|
||||
p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
|
||||
}
|
||||
@ -203,8 +214,8 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -139,6 +139,7 @@ public class AllProjectsCreator {
|
||||
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);
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -94,6 +94,8 @@ public class InMemoryModule extends FactoryModule {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user