Fix issue #747 - Support verify signing with sub key

This commit is contained in:
Robin Shen 2022-05-28 21:22:59 +08:00
parent cbd5656b5b
commit 18b0b23b8a
13 changed files with 209 additions and 113 deletions

View File

@ -2,12 +2,13 @@ package io.onedev.server.entitymanager;
import javax.annotation.Nullable;
import io.onedev.server.git.signature.SignatureVerificationKey;
import io.onedev.server.model.GpgKey;
import io.onedev.server.persistence.dao.EntityManager;
public interface GpgKeyManager extends EntityManager<GpgKey> {
@Nullable
GpgKey findByKeyId(long keyId);
SignatureVerificationKey findSignatureVerificationKey(long keyId);
}

View File

@ -2,18 +2,21 @@ package io.onedev.server.entitymanager.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.bouncycastle.openpgp.PGPPublicKey;
import io.onedev.commons.loader.Listen;
import io.onedev.server.entitymanager.GpgKeyManager;
import io.onedev.server.event.entity.EntityPersisted;
import io.onedev.server.event.entity.EntityRemoved;
import io.onedev.server.event.system.SystemStarted;
import io.onedev.server.git.signature.SignatureVerificationKey;
import io.onedev.server.model.EmailAddress;
import io.onedev.server.model.GpgKey;
import io.onedev.server.model.User;
@ -22,13 +25,14 @@ import io.onedev.server.persistence.annotation.Sessional;
import io.onedev.server.persistence.annotation.Transactional;
import io.onedev.server.persistence.dao.BaseEntityManager;
import io.onedev.server.persistence.dao.Dao;
import io.onedev.server.util.GpgUtils;
@Singleton
public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements GpgKeyManager {
private final TransactionManager transactionManager;
private final Map<Long, Long> idCache = new ConcurrentHashMap<>();
private final Map<Long, Long> entityIdCache = new ConcurrentHashMap<>();
@Inject
public DefaultGpgKeyManager(Dao dao, TransactionManager transactionManager) {
@ -36,43 +40,38 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
this.transactionManager = transactionManager;
}
@Sessional
@Override
public GpgKey findByKeyId(long keyId) {
Long id = idCache.get(keyId);
return id != null? load(id): null;
}
@Listen
@Sessional
public void on(SystemStarted event) {
for (GpgKey key: query())
idCache.put(key.getKeyId(), key.getId());
for (GpgKey key: query()) {
for (Long keyId: key.getKeyIds())
entityIdCache.put(keyId, key.getId());
}
}
@Transactional
@Listen
public void on(EntityRemoved event) {
if (event.getEntity() instanceof GpgKey) {
long keyId = ((GpgKey)event.getEntity()).getKeyId();
List<Long> keyIds = ((GpgKey)event.getEntity()).getKeyIds();
transactionManager.runAfterCommit(new Runnable() {
@Override
public void run() {
idCache.remove(keyId);
entityIdCache.keySet().removeAll(keyIds);
}
});
} else if (event.getEntity() instanceof EmailAddress) {
EmailAddress emailAddress = (EmailAddress) event.getEntity();
Collection<Long> keyIds = emailAddress.getGpgKeys().stream()
.map(it->it.getKeyId())
.collect(Collectors.toList());
Collection<Long> keyIds = new ArrayList<>();
for (GpgKey key: emailAddress.getGpgKeys())
keyIds.addAll(key.getKeyIds());
transactionManager.runAfterCommit(new Runnable() {
@Override
public void run() {
idCache.keySet().removeAll(keyIds);
entityIdCache.keySet().removeAll(keyIds);
}
});
@ -80,15 +79,14 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
User user = (User) event.getEntity();
Collection<Long> keyIds = new ArrayList<>();
for (EmailAddress emailAddress: user.getEmailAddresses()) {
keyIds.addAll(emailAddress.getGpgKeys().stream()
.map(it->it.getKeyId())
.collect(Collectors.toList()));
for (GpgKey key: emailAddress.getGpgKeys())
keyIds.addAll(key.getKeyIds());
}
transactionManager.runAfterCommit(new Runnable() {
@Override
public void run() {
idCache.keySet().removeAll(keyIds);
entityIdCache.keySet().removeAll(keyIds);
}
});
@ -100,17 +98,50 @@ public class DefaultGpgKeyManager extends BaseEntityManager<GpgKey> implements G
public void on(EntityPersisted event) {
if (event.getEntity() instanceof GpgKey) {
GpgKey gpgKey = (GpgKey) event.getEntity();
long keyId = gpgKey.getKeyId();
Long id = gpgKey.getId();
List<Long> keyIds = gpgKey.getKeyIds();
Long entityId = gpgKey.getId();
transactionManager.runAfterCommit(new Runnable() {
@Override
public void run() {
idCache.put(keyId, id);
for (Long keyId: keyIds)
entityIdCache.put(keyId, entityId);
}
});
}
}
@Sessional
@Override
public SignatureVerificationKey findSignatureVerificationKey(long keyId) {
Long entityId = entityIdCache.get(keyId);
if (entityId != null) {
return new SignatureVerificationKey() {
@Override
public boolean shouldVerifyDataWriter() {
return true;
}
@Override
public PGPPublicKey getPublicKey() {
for (PGPPublicKey publicKey: load(entityId).getPublicKeys()) {
if (keyId == publicKey.getKeyID())
return publicKey;
}
throw new IllegalStateException();
}
@Override
public String getEmailAddress() {
return GpgUtils.getEmailAddress(load(entityId).getPublicKeys().get(0));
}
};
} else {
return null;
}
}
}

View File

@ -683,8 +683,7 @@ public class GitUtils {
key.getPublicKey());
signature.update(data);
if (signature.verify()) {
String signer = GpgUtils.getEmailAddress(key.getPublicKey());
if (!key.shouldVerifyDataWriter() || signer.equals(dataWriter))
if (!key.shouldVerifyDataWriter() || key.getEmailAddress().equals(dataWriter))
return new SignatureVerified(key);
else
return new SignatureUnverified(key, "Email address of signing key and committer is different");
@ -696,7 +695,7 @@ public class GitUtils {
return new SignatureUnverified(key, "Signature verification failed");
}
} else {
return new SignatureUnverified(null, "Signature is signed with an unkown key "
return new SignatureUnverified(null, "Signature is signed with an unknown key "
+ "(key ID: " + GpgUtils.getKeyIDString(signature.getKeyID()) + ")");
}
} else {

View File

@ -1,25 +1,31 @@
package io.onedev.server.git.signature;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.GpgKeyManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.GpgKey;
import io.onedev.server.model.support.administration.GpgSetting;
@Singleton
public class DefaultSignatureVerificationKeyLoader implements SignatureVerificationKeyLoader {
private final SettingManager settingManager;
private final GpgKeyManager gpgKeyManager;
@Inject
public DefaultSignatureVerificationKeyLoader(SettingManager settingManager, GpgKeyManager gpgKeyManager) {
this.settingManager = settingManager;
this.gpgKeyManager = gpgKeyManager;
}
@Override
public SignatureVerificationKey getSignatureVerificationKey(long keyId) {
GpgSetting gpgSetting = OneDev.getInstance(SettingManager.class).getGpgSetting();
GpgSetting gpgSetting = settingManager.getGpgSetting();
SignatureVerificationKey verificationKey = gpgSetting.findSignatureVerificationKey(keyId);
if (verificationKey == null) {
GpgKey gpgKey = OneDev.getInstance(GpgKeyManager.class).findByKeyId(keyId);
if (gpgKey != null)
verificationKey = gpgKey.getSignatureVerificationKey();
}
if (verificationKey == null)
verificationKey = gpgKeyManager.findSignatureVerificationKey(keyId);
return verificationKey;
}

View File

@ -8,4 +8,6 @@ public interface SignatureVerificationKey {
PGPPublicKey getPublicKey();
String getEmailAddress();
}

View File

@ -10,13 +10,11 @@ import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.onedev.server.git.signature.SignatureVerificationKey;
import io.onedev.server.model.support.BaseGpgKey;
import io.onedev.server.web.editable.annotation.Editable;
@ -66,19 +64,4 @@ public class GpgKey extends BaseGpgKey {
this.createdAt = createdAt;
}
public SignatureVerificationKey getSignatureVerificationKey() {
return new SignatureVerificationKey() {
@Override
public boolean shouldVerifyDataWriter() {
return true;
}
@Override
public PGPPublicKey getPublicKey() {
return GpgKey.this.getPublicKey();
}
};
}
}

View File

@ -1,5 +1,8 @@
package io.onedev.server.model.support;
import java.util.List;
import java.util.stream.Collectors;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.validation.ConstraintValidatorContext;
@ -27,7 +30,7 @@ public class BaseGpgKey extends AbstractEntity implements Validatable {
@Column(nullable=false, length=5000)
private String content;
private transient PGPPublicKey publicKey;
private transient List<PGPPublicKey> publicKeys;
@Editable(name="GPG Public Key", placeholder="GPG public key begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'")
@NotEmpty
@ -41,10 +44,14 @@ public class BaseGpgKey extends AbstractEntity implements Validatable {
this.content = content;
}
public PGPPublicKey getPublicKey() {
if (publicKey == null)
publicKey = GpgUtils.parse(content);
return publicKey;
public List<PGPPublicKey> getPublicKeys() {
if (publicKeys == null)
publicKeys = GpgUtils.parse(content);
return publicKeys;
}
public List<Long> getKeyIds() {
return getPublicKeys().stream().map(it->it.getKeyID()).collect(Collectors.toList());
}
@Override

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@ -26,7 +27,9 @@ public class GpgSetting implements Serializable {
private Map<Long, String> encodedTrustedKeys = new LinkedHashMap<>();
private transient Map<Long, PGPPublicKey> trustedKeys;
private transient Map<Long, SignatureVerificationKey> trustedSignatureVerificationKeys;
private transient Map<Long, List<PGPPublicKey>> trustedKeys;
public GpgSetting() {
// Add GitHub public GPG key
@ -74,20 +77,53 @@ public class GpgSetting implements Serializable {
public Map<Long, String> getEncodedTrustedKeys() {
return encodedTrustedKeys;
}
public void encodedTrustedKeysUpdated() {
trustedKeys = null;
trustedSignatureVerificationKeys = null;
}
public Map<Long, List<PGPPublicKey>> getTrustedKeys() {
if (trustedKeys == null) {
trustedKeys = new HashMap<>();
for (Map.Entry<Long, String> entry: encodedTrustedKeys.entrySet())
trustedKeys.put(entry.getKey(), GpgUtils.parse(entry.getValue()));
}
return trustedKeys;
}
@Nullable
public PGPPublicKey getTrustedKey(long keyId) {
if (trustedKeys == null)
trustedKeys = new HashMap<>();
PGPPublicKey trustedKey = trustedKeys.get(keyId);
if (trustedKey == null) {
String encodedTrustedKey = encodedTrustedKeys.get(keyId);
if (encodedTrustedKey != null) {
trustedKey = GpgUtils.parse(encodedTrustedKey);
trustedKeys.put(keyId, trustedKey);
public SignatureVerificationKey getTrustedSignatureVerificationKey(long keyId) {
return getTrustedSignatureVerificationKeys().get(keyId);
}
private Map<Long, SignatureVerificationKey> getTrustedSignatureVerificationKeys() {
if (trustedSignatureVerificationKeys == null) {
trustedSignatureVerificationKeys = new HashMap<>();
for (List<PGPPublicKey> publicKeys: getTrustedKeys().values()) {
for (PGPPublicKey publicKey: publicKeys) {
trustedSignatureVerificationKeys.put(publicKey.getKeyID(), new SignatureVerificationKey() {
@Override
public boolean shouldVerifyDataWriter() {
return false;
}
@Override
public PGPPublicKey getPublicKey() {
return publicKey;
}
@Override
public String getEmailAddress() {
return GpgUtils.getEmailAddress(publicKeys.get(0));
}
});
}
}
}
return trustedKey;
return trustedSignatureVerificationKeys;
}
@Nullable
@ -104,27 +140,15 @@ public class GpgSetting implements Serializable {
public PGPPublicKey getPublicKey() {
return getSigningKey().getPublicKey();
}
@Override
public String getEmailAddress() {
return GpgUtils.getEmailAddress(getPublicKey());
}
};
} else {
PGPPublicKey trustedKey = getTrustedKey(keyId);
if (trustedKey != null) {
return new SignatureVerificationKey() {
@Override
public boolean shouldVerifyDataWriter() {
return false;
}
@Override
public PGPPublicKey getPublicKey() {
return trustedKey;
}
};
} else {
return null;
}
return getTrustedSignatureVerificationKey(keyId);
}
}

View File

@ -6,8 +6,10 @@ import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
@ -37,18 +39,20 @@ import io.onedev.commons.utils.StringUtils;
public class GpgUtils {
public static PGPPublicKey parse(String publicKeyString) {
public static List<PGPPublicKey> parse(String publicKeyString) {
try (InputStream in = PGPUtil
.getDecoderStream(new ByteArrayInputStream(publicKeyString.getBytes(StandardCharsets.UTF_8)))) {
List<PGPPublicKey> publicKeys = new ArrayList<>();
JcaPGPPublicKeyRingCollection ringCollection = new JcaPGPPublicKeyRingCollection(in);
Iterator<PGPPublicKeyRing> itRing = ringCollection.getKeyRings();
if (!itRing.hasNext())
throw new ExplicitException("No key ring found");
PGPPublicKeyRing ring = itRing.next();
Iterator<PGPPublicKey> itKey = ring.getPublicKeys();
if (!itKey.hasNext())
while (itRing.hasNext()) {
Iterator<PGPPublicKey> itKey = itRing.next().getPublicKeys();
while (itKey.hasNext())
publicKeys.add(itKey.next());
}
if (publicKeys.isEmpty())
throw new ExplicitException("No public key found");
return itKey.next();
return publicKeys;
} catch (IOException | PGPException e) {
throw new RuntimeException(e);
}

View File

@ -35,7 +35,7 @@ abstract class GitSignatureDetailPanel extends Panel {
message = "Signature verified successfully with committer's GPG key";
else
message = "Signature verified successfully with tagger's GPG key";
} else if (getGpgSetting().getTrustedKey(key.getPublicKey().getKeyID()) != null) {
} else if (getGpgSetting().getTrustedSignatureVerificationKey(key.getPublicKey().getKeyID()) != null) {
message = "Signature verified successfully with trusted GPG key";
} else {
message = "Signature verified successfully with OneDev GPG key";
@ -54,7 +54,7 @@ abstract class GitSignatureDetailPanel extends Panel {
if (key != null) {
add(new Label("keyId", GpgUtils.getKeyIDString(key.getPublicKey().getKeyID())));
add(new Label("emailAddress", GpgUtils.getEmailAddress(key.getPublicKey())));
add(new Label("emailAddress", key.getEmailAddress()));
} else {
add(new WebMarkupContainer("keyId").setVisible(false));
add(new WebMarkupContainer("emailAddress").setVisible(false));

View File

@ -3,6 +3,7 @@ package io.onedev.server.web.component.user.gpgkey;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.wicket.Session;
import org.apache.wicket.ajax.AjaxRequestTarget;
@ -26,7 +27,6 @@ import org.apache.wicket.model.Model;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.GpgKeyManager;
import io.onedev.server.model.GpgKey;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.GpgUtils;
import io.onedev.server.web.ajaxlistener.ConfirmClickListener;
import io.onedev.server.web.component.datatable.DefaultDataTable;
@ -67,12 +67,12 @@ public class GpgKeyListPanel extends GenericPanel<List<GpgKey>> {
public void populateItem(Item<ICellPopulator<GpgKey>> cellItem, String componentId,
IModel<GpgKey> rowModel) {
cellItem.add(new Label(componentId,
GpgUtils.getKeyIDString(rowModel.getObject().getPublicKey().getKeyID())));
GpgUtils.getKeyIDString(rowModel.getObject().getKeyId())));
}
});
columns.add(new AbstractColumn<GpgKey, Void>(Model.of("Created At")) {
columns.add(new AbstractColumn<GpgKey, Void>(Model.of("Sub Keys")) {
@Override
public String getCssClass() {
@ -82,7 +82,15 @@ public class GpgKeyListPanel extends GenericPanel<List<GpgKey>> {
@Override
public void populateItem(Item<ICellPopulator<GpgKey>> cellItem, String componentId,
IModel<GpgKey> rowModel) {
cellItem.add(new Label(componentId, DateUtils.formatDateTime(rowModel.getObject().getCreatedAt())));
GpgKey key = rowModel.getObject();
String subKeyIds = key.getKeyIds().stream()
.filter(it->it!=key.getKeyId())
.map(it->GpgUtils.getKeyIDString(it))
.collect(Collectors.joining(", "));
if (subKeyIds.length() != 0)
cellItem.add(new Label(componentId, subKeyIds));
else
cellItem.add(new Label(componentId, "<i>None</i>").setEscapeModelStrings(false));
}
});

View File

@ -56,13 +56,13 @@ public abstract class InsertGpgKeyPanel extends Panel {
GpgKeyManager gpgKeyManager = OneDev.getInstance(GpgKeyManager.class);
GpgKey gpgKey = (GpgKey) editor.getModelObject();
gpgKey.setKeyId(gpgKey.getPublicKey().getKeyID());
gpgKey.setKeyId(gpgKey.getKeyIds().get(0));
if (gpgKeyManager.findByKeyId(gpgKey.getKeyId()) != null) {
editor.error(new Path(new PathNode.Named("content")), "This key is already in use");
if (gpgKey.getKeyIds().stream().anyMatch(it->gpgKeyManager.findSignatureVerificationKey(it)!=null)) {
editor.error(new Path(new PathNode.Named("content")), "This key or one of its subkey is already in use");
target.add(form);
} else {
String emailAddressValue = GpgUtils.getEmailAddress(gpgKey.getPublicKey());
String emailAddressValue = GpgUtils.getEmailAddress(gpgKey.getPublicKeys().get(0));
EmailAddress emailAddress = OneDev.getInstance(EmailAddressManager.class).findByValue(emailAddressValue);
if (emailAddress != null && emailAddress.isVerified() && emailAddress.getOwner().equals(getUser())) {
gpgKey.setEmailAddress(emailAddress);

View File

@ -3,6 +3,7 @@ package io.onedev.server.web.page.admin.gpgtrustedkeys;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
@ -89,10 +90,14 @@ public class GpgTrustedKeysPage extends AdministrationPage {
BaseGpgKey bean = (BaseGpgKey) editor.getModelObject();
GpgSetting setting = getSettingManager().getGpgSetting();
if (setting.getEncodedTrustedKeys().put(bean.getPublicKey().getKeyID(), bean.getContent()) != null) {
editor.error(new Path(new PathNode.Named(BaseGpgKey.PROP_CONTENT)), "This key is already added");
if (bean.getKeyIds().stream().anyMatch(it-> setting.getTrustedSignatureVerificationKey(it) != null)) {
editor.error(
new Path(new PathNode.Named(BaseGpgKey.PROP_CONTENT)),
"This key or one of its sub key is already added");
target.add(form);
} else {
setting.getEncodedTrustedKeys().put(bean.getKeyIds().get(0), bean.getContent());
setting.encodedTrustedKeysUpdated();
getSettingManager().saveGpgSetting(setting);
target.add(trustedKeysTable);
modal.close();
@ -130,8 +135,8 @@ public class GpgTrustedKeysPage extends AdministrationPage {
@Override
public void populateItem(Item<ICellPopulator<Long>> cellItem, String componentId,
IModel<Long> rowModel) {
cellItem.add(new Label(componentId,
GpgUtils.getEmailAddress(getPublicKey(rowModel.getObject()))));
List<PGPPublicKey> trustedKey = getTrustedKey(rowModel.getObject());
cellItem.add(new Label(componentId, GpgUtils.getEmailAddress(trustedKey.get(0))));
}
});
@ -141,8 +146,33 @@ public class GpgTrustedKeysPage extends AdministrationPage {
@Override
public void populateItem(Item<ICellPopulator<Long>> cellItem, String componentId,
IModel<Long> rowModel) {
cellItem.add(new Label(componentId,
GpgUtils.getKeyIDString(getPublicKey(rowModel.getObject()).getKeyID())));
List<PGPPublicKey> trustedKey = getTrustedKey(rowModel.getObject());
cellItem.add(new Label(componentId, GpgUtils.getKeyIDString(trustedKey.get(0).getKeyID())));
}
});
columns.add(new AbstractColumn<Long, Void>(Model.of("Sub Keys")) {
@Override
public void populateItem(Item<ICellPopulator<Long>> cellItem, String componentId,
IModel<Long> rowModel) {
List<PGPPublicKey> trustedKey = getTrustedKey(rowModel.getObject());
long keyId = trustedKey.get(0).getKeyID();
String subKeyIds = trustedKey.stream()
.map(it->it.getKeyID())
.filter(it->it!=keyId)
.map(it->GpgUtils.getKeyIDString(it))
.collect(Collectors.joining(", "));
if (subKeyIds.length() != 0)
cellItem.add(new Label(componentId, subKeyIds));
else
cellItem.add(new Label(componentId, "<i>None</i>").setEscapeModelStrings(false));
}
@Override
public String getCssClass() {
return "expanded";
}
});
@ -160,6 +190,7 @@ public class GpgTrustedKeysPage extends AdministrationPage {
public void onClick(AjaxRequestTarget target) {
GpgSetting setting = getSettingManager().getGpgSetting();
setting.getEncodedTrustedKeys().remove(rowModel.getObject());
setting.encodedTrustedKeysUpdated();
getSettingManager().saveGpgSetting(setting);
Session.get().success("GPG key deleted");
target.add(trustedKeysTable);
@ -206,8 +237,8 @@ public class GpgTrustedKeysPage extends AdministrationPage {
Integer.MAX_VALUE, null));
}
private PGPPublicKey getPublicKey(long keyId) {
return getSettingManager().getGpgSetting().getTrustedKey(keyId);
private List<PGPPublicKey> getTrustedKey(long keyId) {
return getSettingManager().getGpgSetting().getTrustedKeys().get(keyId);
}
private SettingManager getSettingManager() {