From 08a025ba81e31a281eb4380f01516f963290e87e Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Mon, 3 Aug 2020 16:09:37 +0200 Subject: [PATCH] wip --- .../repository/api/ModifyCommandBuilder.java | 9 +++ .../spi/ModificationsCommandRequest.java | 2 + .../repository/spi/ModifyCommandRequest.java | 10 +++ .../sonia/scm/repository/ScmGpgSigner.java | 62 +++++++++++++++ .../repository/ScmGpgSignerInitializer.java | 54 +++++++++++++ .../sonia/scm/security/gpg/DefaultGPG.java | 60 ++++++++++++-- .../scm/security/gpg/PrivateKeyStore.java | 78 +++++++++++++++++++ .../scm/security/gpg/PublicKeyMapper.java | 2 +- .../scm/security/gpg/PublicKeyResource.java | 5 +- .../scm/security/gpg/PublicKeyStore.java | 2 +- .../sonia/scm/security/gpg/RawGpgKey.java | 1 + .../security/gpg/PublicKeyResourceTest.java | 4 +- 12 files changed, 278 insertions(+), 11 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java create mode 100644 scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PrivateKeyStore.java diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 3067d7a054..56f2835ade 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -164,6 +164,15 @@ public class ModifyCommandBuilder { return this; } + /** + * Set the branch the changes should be made upon. + * @return This builder instance. + */ + public ModifyCommandBuilder disableSignature(String branch) { + request.setBranch(branch); + return this; + } + /** * Set the expected revision of the branch, before the changes are applied. If the branch does not have the * expected revision, a concurrent modification exception will be thrown when the command is executed and no diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java index d0c05fd99d..fb305e6289 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java @@ -40,9 +40,11 @@ import lombok.ToString; @NoArgsConstructor public class ModificationsCommandRequest implements Resetable { private String revision; + private boolean sign = true; @Override public void reset() { revision = null; + sign = true; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index 8f077430df..89e9b3844d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -49,6 +49,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit private String branch; private String expectedRevision; private boolean defaultPath; + private boolean disableSigning; @Override public void reset() { @@ -57,6 +58,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit commitMessage = null; branch = null; defaultPath = false; + disableSigning = false; } public void addRequest(PartialRequest request) { @@ -112,6 +114,14 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit this.defaultPath = defaultPath; } + public boolean isDisableSigning() { + return disableSigning; + } + + public void setDisableSigning(boolean disableSigning) { + this.disableSigning = disableSigning; + } + public interface PartialRequest { void execute(ModifyCommand.Worker worker) throws IOException; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java new file mode 100644 index 0000000000..ebfa5d1a9f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSigner.java @@ -0,0 +1,62 @@ +/* + * + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository; + +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.transport.CredentialsProvider; +import sonia.scm.security.GPG; + +import javax.inject.Inject; +import java.io.UnsupportedEncodingException; + +public class ScmGpgSigner extends GpgSigner { + + private final GPG gpg; + + @Inject + public ScmGpgSigner(GPG gpg) { + this.gpg = gpg; + } + + @Override + public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + try { + final byte[] signature = this.gpg.getPrivateKey().sign(commitBuilder.build()); + commitBuilder.setGpgSignature(new GpgSignature(signature)); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + + @Override + public boolean canLocateSigningKey(String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + return true; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java new file mode 100644 index 0000000000..4526615f39 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/ScmGpgSignerInitializer.java @@ -0,0 +1,54 @@ +/* + * + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.repository; + +import org.eclipse.jgit.lib.GpgSigner; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +@Extension +public class ScmGpgSignerInitializer implements ServletContextListener { + + private final ScmGpgSigner scmGpgSigner; + + @Inject + public ScmGpgSignerInitializer(ScmGpgSigner scmGpgSigner) { + this.scmGpgSigner = scmGpgSigner; + } + + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + GpgSigner.setDefault(scmGpgSigner); + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + // Do nothing + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java index 008c7f4b5e..8f592efb7f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultGPG.java @@ -24,20 +24,36 @@ package sonia.scm.security.gpg; +import com.sun.tools.javac.util.Pair; +import org.apache.shiro.SecurityUtils; import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.security.GPG; import sonia.scm.security.PrivateKey; import sonia.scm.security.PublicKey; +import sonia.scm.security.SessionId; +import sonia.scm.user.User; import javax.inject.Inject; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -45,11 +61,13 @@ import java.util.stream.Collectors; public class DefaultGPG implements GPG { private static final Logger LOG = LoggerFactory.getLogger(DefaultGPG.class); - private final PublicKeyStore store; + private final PublicKeyStore publicKeyStore; + private final PrivateKeyStore privateKeyStore; @Inject - public DefaultGPG(PublicKeyStore store) { - this.store = store; + public DefaultGPG(PublicKeyStore publicKeyStore, PrivateKeyStore privateKeyStore) { + this.publicKeyStore = publicKeyStore; + this.privateKeyStore = privateKeyStore; } @Override @@ -67,14 +85,14 @@ public class DefaultGPG implements GPG { @Override public Optional findPublicKey(String id) { - Optional key = store.findById(id); + Optional key = publicKeyStore.findById(id); return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())); } @Override public Iterable findPublicKeysByUsername(String username) { - List keys = store.findByUsername(username); + List keys = publicKeyStore.findByUsername(username); if (!keys.isEmpty()) { return keys @@ -88,6 +106,36 @@ public class DefaultGPG implements GPG { @Override public PrivateKey getPrivateKey() { - throw new UnsupportedOperationException("getPrivateKey is not yet implemented"); + final String userId = SecurityUtils.getSubject().getPrincipal().toString(); + final Optional privateRawKey = privateKeyStore.getForUserId(userId); + + if (!privateRawKey.isPresent()) { + try { + // Generate key pair + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(2048); + + KeyPair pair = keyPairGenerator.generateKeyPair(); + + String identity = "0xAWESOMExBOB"; + PGPKeyPair keyPair = new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date()); + + new PGPKeyRingGenerator().generateSecretKeyRing().; + } catch (PGPException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + } + + // + privateKeyStore.setForUserId(user.getId(), privateKeyGpgKeyPair.fst); + publicKeyStore.add(user.getDisplayName(), user.getName(), privateKeyGpgKeyPair.snd.getRaw()); + return privateKeyGpgKeyPair.fst; + } else { +// PGPUtil.getDecoderStream(); + return privateRawKey.get(); + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PrivateKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PrivateKeyStore.java new file mode 100644 index 0000000000..1684ea7a2c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PrivateKeyStore.java @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + + +package sonia.scm.security.gpg; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import sonia.scm.security.CipherUtil; +import sonia.scm.security.PrivateKey; +import sonia.scm.store.DataStore; +import sonia.scm.store.DataStoreFactory; +import sonia.scm.xml.XmlInstantAdapter; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.time.Instant; +import java.util.Optional; + +@Singleton +class PrivateKeyStore { + + private static final String STORE_NAME = "gpg_private_keys"; + + private final DataStore store; + + @Inject + PrivateKeyStore(DataStoreFactory dataStoreFactory) { + this.store = dataStoreFactory.withType(RawPrivateKey.class).withName(STORE_NAME).build(); + } + + Optional getForUserId(String userId) { + return store.getOptional(userId).map(rawPrivateKey -> CipherUtil.getInstance().decode(rawPrivateKey.key)); + } + + void setForUserId(String userId, String rawKey) { + final String encodedRawKey = CipherUtil.getInstance().encode(rawKey); + store.put(userId, new RawPrivateKey(encodedRawKey, Instant.now())); + } + + @XmlRootElement + @XmlAccessorType(XmlAccessType.FIELD) + @AllArgsConstructor + @NoArgsConstructor + private class RawPrivateKey { + private String key; + + @XmlJavaTypeAdapter(XmlInstantAdapter.class) + private Instant date; + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java index 7e5ef8ce8c..ede00b9f9d 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyMapper.java @@ -58,7 +58,7 @@ public abstract class PublicKeyMapper { RawGpgKeyDto createDto(RawGpgKey rawGpgKey) { Links.Builder linksBuilder = linkingTo(); linksBuilder.self(createSelfLink(rawGpgKey)); - if (UserPermissions.changePublicKeys(rawGpgKey.getOwner()).isPermitted()) { + if (UserPermissions.changePublicKeys(rawGpgKey.getOwner()).isPermitted() && !rawGpgKey.isReadonly()) { linksBuilder.single(Link.link("delete", createDeleteLink(rawGpgKey))); } return new RawGpgKeyDto(linksBuilder.build()); diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java index de927f134b..d2338f4ecf 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyResource.java @@ -30,12 +30,14 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.api.v2.resources.ErrorDto; +import sonia.scm.security.AllowAnonymousAccess; import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -97,6 +99,7 @@ public class PublicKeyResource { @GET @Path("{id}") @Produces(MEDIA_TYPE) + @AllowAnonymousAccess @Operation( summary = "Get single key for user", description = "Returns a single public key for username by id.", @@ -129,7 +132,7 @@ public class PublicKeyResource { schema = @Schema(implementation = ErrorDto.class) ) ) - public Response findById(@PathParam("id") String id) { + public Response findByIdJson(@PathParam("id") String id) { Optional byId = store.findById(id); if (byId.isPresent()) { return Response.ok(mapper.map(byId.get())).build(); diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java index 31586850b8..9ecf75d31e 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java @@ -64,7 +64,7 @@ public class PublicKeyStore { this.eventBus = eventBus; } - public RawGpgKey add(String displayName, String username, String rawKey) { + public RawGpgKey add(String displayName, String username, String rawKey, ) { UserPermissions.changePublicKeys(username).check(); if (!rawKey.contains("PUBLIC KEY")) { diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java index eaf099e68f..420683efd4 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java @@ -50,6 +50,7 @@ public class RawGpgKey { private String displayName; private String owner; private String raw; + private boolean readonly = false; private Set contacts; @XmlJavaTypeAdapter(XmlInstantAdapter.class) diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java index ff11515014..68d3387f04 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java @@ -97,7 +97,7 @@ class PublicKeyResourceTest { RawGpgKeyDto dto = new RawGpgKeyDto(); when(mapper.map(key)).thenReturn(dto); - Response response = resource.findById("42"); + Response response = resource.findByIdJson("42"); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getEntity()).isSameAs(dto); } @@ -106,7 +106,7 @@ class PublicKeyResourceTest { void shouldReturn404IfIdDoesNotExists() { when(store.findById("42")).thenReturn(Optional.empty()); - Response response = resource.findById("42"); + Response response = resource.findByIdJson("42"); assertThat(response.getStatus()).isEqualTo(404); }