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:
Dave Borowitz 2015-06-24 15:06:10 +00:00 committed by Gerrit Code Review
commit c2b94eb946
27 changed files with 779 additions and 36 deletions

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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[

View File

@ -42,6 +42,7 @@ public interface AdminConstants extends Constants {
String useContributorAgreements();
String useSignedOffBy();
String createNewChangeForAllNotInTarget();
String enableSignedPush();
String requireChangeID();
String headingMaxObjectSizeLimit();
String headingGroupOptions();

View File

@ -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

View File

@ -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())),

View File

@ -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() {
}
}
}

View File

@ -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());
}

View File

@ -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; }-*/;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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',

View File

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

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -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 =

View File

@ -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);
}
}

View File

@ -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()) {

View File

@ -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) {

View File

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

View File

@ -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() {
}
}

View File

@ -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());
}
}

View File

@ -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;