diff --git a/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java b/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java index d8784c7a71..73961353ea 100644 --- a/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java +++ b/scm-core/src/main/java/sonia/scm/repository/SignatureStatus.java @@ -24,6 +24,9 @@ package sonia.scm.repository; +/** + * @since 2.4.0 + */ public enum SignatureStatus { VERIFIED, NOT_FOUND, INVALID; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java index d4dfaef0a3..55e1f44f3c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/MergeCommandBuilder.java @@ -138,12 +138,12 @@ public class MergeCommandBuilder { } /** - * Disables adding a verifiable signature to the merge. + * Disables adding a verifiable signature to the merge commit. * @return This builder instance. * @since 2.4.0 */ public MergeCommandBuilder disableSigning() { - request.setSigningDisabled(true); + request.setSign(false); return this; } 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 c9620db7f7..aeccb24b99 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 @@ -165,12 +165,12 @@ public class ModifyCommandBuilder { } /** - * Disables adding a verifiable signature to the modification. + * Disables adding a verifiable signature to the modification commit. * @return This builder instance. * @since 2.4.0 */ public ModifyCommandBuilder disableSigning() { - request.setSigningDisabled(true); + request.setSign(false); return this; } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java index 773263057d..935fc1cfba 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/MergeCommandRequest.java @@ -43,7 +43,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl private Person author; private String messageTemplate; private MergeStrategy mergeStrategy; - private boolean signingDisabled; + private boolean sign = true; public String getBranchToMerge() { return branchToMerge; @@ -85,12 +85,12 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl this.mergeStrategy = mergeStrategy; } - public boolean isSigningDisabled() { - return signingDisabled; + public boolean isSign() { + return sign; } - public void setSigningDisabled(boolean signingDisabled) { - this.signingDisabled = signingDisabled; + public void setSign(boolean sign) { + this.sign = sign; } public boolean isValid() { @@ -101,7 +101,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl public void reset() { this.setBranchToMerge(null); this.setTargetBranch(null); - this.setSigningDisabled(false); + this.setSign(false); } @Override @@ -120,7 +120,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl && Objects.equal(targetBranch, other.targetBranch) && Objects.equal(author, other.author) && Objects.equal(mergeStrategy, other.mergeStrategy) - && Objects.equal(signingDisabled, other.signingDisabled); + && Objects.equal(sign, other.sign); } @Override @@ -135,7 +135,7 @@ public class MergeCommandRequest implements Validateable, Resetable, Serializabl .add("targetBranch", targetBranch) .add("author", author) .add("mergeStrategy", mergeStrategy) - .add("signatureDisabled", signingDisabled) + .add("signatureDisabled", sign) .toString(); } } 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 8ee00cfeef..a13ee52b31 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,7 +49,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit private String branch; private String expectedRevision; private boolean defaultPath; - private boolean signingDisabled; + private boolean sign = true; @Override public void reset() { @@ -58,7 +58,7 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit commitMessage = null; branch = null; defaultPath = false; - signingDisabled = false; + sign = false; } public void addRequest(PartialRequest request) { @@ -77,8 +77,8 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit this.branch = branch; } - public void setSigningDisabled(boolean signingDisabled) { - this.signingDisabled = signingDisabled; + public void setSign(boolean sign) { + this.sign = sign; } public List getRequests() { @@ -118,8 +118,8 @@ public class ModifyCommandRequest implements Resetable, Validateable, CommandWit this.defaultPath = defaultPath; } - public boolean isSigningDisabled() { - return signingDisabled; + public boolean isSign() { + return sign; } public interface PartialRequest { diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java b/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java index a79aeca9c4..a598533088 100644 --- a/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java +++ b/scm-core/src/main/java/sonia/scm/security/PublicKeyCreatedEvent.java @@ -31,5 +31,14 @@ import sonia.scm.event.Event; * @since 2.4.0 */ @Event -public class PublicKeyCreatedEvent { +public final class PublicKeyCreatedEvent { + private final PublicKey key; + + public PublicKeyCreatedEvent(PublicKey key) { + this.key = key; + } + + public PublicKey getKey() { + return key; + } } diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java b/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java index f3579dc7b1..833af8dee2 100644 --- a/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java +++ b/scm-core/src/main/java/sonia/scm/security/PublicKeyDeletedEvent.java @@ -31,5 +31,14 @@ import sonia.scm.event.Event; * @since 2.4.0 */ @Event -public class PublicKeyDeletedEvent { +public final class PublicKeyDeletedEvent { + private final PublicKey key; + + public PublicKeyDeletedEvent(PublicKey key) { + this.key = key; + } + + public PublicKey getKey() { + return key; + } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java index 48e9a71c86..5cbf94b602 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/AbstractGitCommand.java @@ -219,7 +219,7 @@ class AbstractGitCommand { } } - Optional doCommit(String message, Person author, boolean signingDisabled) { + Optional doCommit(String message, Person author, boolean sign) { Person authorToUse = determineAuthor(author); try { Status status = clone.status().call(); @@ -228,8 +228,8 @@ class AbstractGitCommand { .setAuthor(authorToUse.getName(), authorToUse.getMail()) .setCommitter("SCM-Manager", "noreply@scm-manager.org") .setMessage(message) - .setSign(!signingDisabled) - .setSigningKey(signingDisabled ? null : "SCM-MANAGER-DEFAULT-KEY") + .setSign(sign) + .setSigningKey(sign ? "SCM-MANAGER-DEFAULT-KEY" : null) .call()); } else { return empty(); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java index 0c58cf6aa1..72a3f23b5f 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitMergeStrategy.java @@ -56,7 +56,7 @@ abstract class GitMergeStrategy extends AbstractGitCommand.GitCloneWorker doCommit() { logger.debug("merged branch {} into {}", branchToMerge, targetBranch); - return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author, signingDisabled); + return doCommit(MessageFormat.format(determineMessageTemplate(), branchToMerge, targetBranch), author, sign); } MergeCommandResult createSuccessResult(String newRevision) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index cd5fda2d0e..01271d0fc3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -38,7 +38,6 @@ import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.security.GPG; import sonia.scm.web.lfs.LfsBlobStoreFactory; import javax.inject.Inject; @@ -94,7 +93,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman r.execute(this); } failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())); - Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSigningDisabled()); + Optional revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign()); push(); return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name(); } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java index e9d36609cb..9a3c179f95 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/GitTestHelper.java @@ -24,13 +24,18 @@ 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 sonia.scm.security.PrivateKey; import sonia.scm.security.PublicKey; import java.util.Collections; import java.util.Optional; -import java.util.Set; public final class GitTestHelper { @@ -41,6 +46,25 @@ public final class GitTestHelper { return new GitChangesetConverterFactory(new NoopGPG()); } + public static class SimpleGpgSigner extends GpgSigner { + + public static byte[] getSignature() { + return "SIGNATURE".getBytes(); + } + + @Override + public void sign(CommitBuilder commitBuilder, String s, PersonIdent personIdent, CredentialsProvider + credentialsProvider) throws CanceledException { + commitBuilder.setGpgSignature(new GpgSignature(SimpleGpgSigner.getSignature())); + } + + @Override + public boolean canLocateSigningKey(String s, PersonIdent personIdent, CredentialsProvider credentialsProvider) throws CanceledException { + return true; + } + + } + private static class NoopGPG implements GPG { @Override diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java index 0bf61a739b..716b172981 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitMergeCommandTest.java @@ -29,17 +29,24 @@ import com.github.sdorra.shiro.SubjectAware; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.jupiter.api.Assertions; import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.repository.Added; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.GitWorkingCopyFactory; import sonia.scm.repository.Person; import sonia.scm.repository.api.MergeCommandResult; @@ -68,6 +75,11 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { @Rule public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); + @BeforeClass + public static void setSigner() { + GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + } + @Test public void shouldDetectMergeableBranches() { GitMergeCommand command = createCommand(); @@ -419,6 +431,48 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase { command.dryRun(request); } + @Test + public void shouldSignMergeCommit() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("empty_merge"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getRawGpgSignature()).isNotEmpty(); + assertThat(mergeCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature()); + + } + + @Test + public void shouldNotSignMergeCommitIfSigningIsDisabled() throws IOException, GitAPIException { + GitMergeCommand command = createCommand(); + MergeCommandRequest request = new MergeCommandRequest(); + request.setTargetBranch("master"); + request.setBranchToMerge("empty_merge"); + request.setMergeStrategy(MergeStrategy.MERGE_COMMIT); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + request.setSign(false); + + MergeCommandResult mergeCommandResult = command.merge(request); + + assertThat(mergeCommandResult.isSuccess()).isTrue(); + + Repository repository = createContext().open(); + Iterable commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call(); + RevCommit mergeCommit = commits.iterator().next(); + assertThat(mergeCommit.getRawGpgSignature()).isNullOrEmpty(); + + } + private GitMergeCommand createCommand() { return createCommand(git -> { }); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index e72ad1af7e..473356d8bf 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -27,23 +27,33 @@ package sonia.scm.repository.spi; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.GpgSignature; +import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.rules.TemporaryFolder; import sonia.scm.AlreadyExistsException; import sonia.scm.BadRequestException; import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Person; import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; +import sonia.scm.security.PublicKey; import sonia.scm.web.lfs.LfsBlobStoreFactory; import java.io.File; @@ -65,6 +75,11 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + @BeforeClass + public static void setSigner() { + GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + } + @Test public void shouldCreateCommit() throws IOException, GitAPIException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); @@ -306,6 +321,48 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { command.execute(request); } + @Test + public void shouldSignCreatedCommit() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + try (Git git = new Git(createContext().open())) { + + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getRawGpgSignature()).isNotEmpty(); + assertThat(lastCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature()); + } + } + + @Test + public void shouldNotSignCreatedCommitIfSigningDisabled() throws IOException, GitAPIException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + GitModifyCommand command = createCommand(); + + ModifyCommandRequest request = new ModifyCommandRequest(); + request.setCommitMessage("test commit"); + request.setSign(false); + request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false)); + request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det")); + + command.execute(request); + + try (Git git = new Git(createContext().open())) { + + RevCommit lastCommit = getLastCommit(git); + assertThat(lastCommit.getRawGpgSignature()).isNullOrEmpty(); + } + } + private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException { try (Git git = new Git(createContext().open())) { RevCommit lastCommit = getLastCommit(git); diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index e496e41fc1..39b044a930 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -2939,6 +2939,381 @@ exports[`Storyshots Changesets With multiple Co-Authors 1`] = ` `; +exports[`Storyshots Changesets With multiple signatures and invalid status 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With multiple signatures and not found status 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + +exports[`Storyshots Changesets With multiple signatures and valid status 1`] = ` +
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+ +
+
+
+
+
+
+
+
+
+
+

+ +

+

+ +

+
+
+
+
+
+`; + exports[`Storyshots Changesets With unknown signature 1`] = `
; + }) + .add("With multiple signatures and invalid status", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x912389FJIQW8W223", + type: "gpg", + status: "INVALID", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x9123891239VFIA33", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With multiple signatures and valid status", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x912389FJIQW8W223", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x247E908C6FD35473", + type: "gpg", + status: "VERIFIED", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x9123891239VFIA33", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; + }) + .add("With multiple signatures and not found status", () => { + const changeset = copy(three); + changeset.signatures = [{ + keyId: "0x912389FJIQW8W223", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }, { + keyId: "0x9123891239VFIA33", + type: "gpg", + status: "NOT_FOUND", + owner: "trillian", + contacts: [{ + name: "Tricia Marie McMilla", + mail: "trillian@hitchhiker.com" + }] + }]; + return ; }); diff --git a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx index 48cf2a3b11..81c2882e10 100644 --- a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx +++ b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx @@ -56,6 +56,18 @@ const SignatureIcon: FC = ({signatures, className}) => { return null; } + const getColor = (signatures: Signature[]) => { + const invalid = signatures.some(sig => sig.status === "INVALID"); + if (invalid) { + return "danger"; + } + const verified = signatures.some(sig => sig.status === "VERIFIED"); + if (verified) { + return "success"; + } + return undefined; + }; + const createSignatureBlock = (signature: Signature) => { let status; if (signature.status === "VERIFIED") { @@ -78,37 +90,26 @@ const SignatureIcon: FC = ({signatures, className}) => {
{t("changeset.keyId")}: { signature._links?.rawKey ? {signature.keyId} : signature.keyId }
-
{t("changeset.signatureStatus")}: {status}
+
{t("changeset.signatureStatus")}: {status}
{signature.contacts && signature.contacts.length > 0 && <>
{t("changeset.keyContacts")}:
- {signature.contacts && signature.contacts.map(contact =>
- {contact.name}{contact.mail && ` <${contact.mail}>`}
)} + {signature.contacts && signature.contacts.map(contact => +
- {contact.name}{contact.mail && ` <${contact.mail}>`}
)} }

; }; const signatureElements = signatures.map(signature => createSignatureBlock(signature)); - const getColor = () => { - const invalid = signatures.some(sig => sig.status === "INVALID"); - if (invalid) { - return "danger"; - } - const verified = signatures.some(sig => sig.status === "VERIFIED"); - if (verified) { - return "success"; - } - return undefined; - }; - return ( <> - + {t("changeset.signatures")}} width={500} {...popoverProps}> {signatureElements}
- +
); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java index 354ade5c86..384331c6ad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DefaultChangesetToChangesetDtoMapper.java @@ -40,10 +40,13 @@ import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.security.gpg.PublicKeyResource; +import sonia.scm.security.gpg.PublicKeyStore; +import sonia.scm.security.gpg.RawGpgKey; import sonia.scm.web.EdisonHalAppender; import javax.inject.Inject; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -72,6 +75,9 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa @Inject private ScmPathInfoStore scmPathInfoStore; + @Inject + private PublicKeyStore publicKeyStore; + abstract ContributorDto map(Contributor contributor); abstract SignatureDto map(Signature signature); @@ -80,7 +86,8 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa @ObjectFactory SignatureDto createDto(Signature signature) { - if (signature.getType().equals("gpg")) { + final Optional key = publicKeyStore.findById(signature.getKeyId()); + if (signature.getType().equals("gpg") && key.isPresent()) { final Links.Builder linkBuilder = linkingTo() .single(link("rawKey", new LinkBuilder(scmPathInfoStore.get(), PublicKeyResource.class) 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 45a2783964..e470636c4a 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,48 +24,25 @@ package sonia.scm.security.gpg; -import org.apache.commons.io.IOUtils; import org.apache.shiro.SecurityUtils; import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.HashAlgorithmTags; -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; -import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.Person; import sonia.scm.security.GPG; import sonia.scm.security.PrivateKey; import sonia.scm.security.PublicKey; -import sonia.scm.user.User; import javax.inject.Inject; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -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; @@ -100,7 +77,7 @@ public class DefaultGPG implements GPG { public Optional findPublicKey(String id) { Optional key = publicKeyStore.findById(id); - return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())); + return key.map(rawGpgKey -> new DefaultPublicKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())); } @Override @@ -110,7 +87,7 @@ public class DefaultGPG implements GPG { if (!keys.isEmpty()) { return keys .stream() - .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())) + .map(rawGpgKey -> new DefaultPublicKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())) .collect(Collectors.toSet()); } @@ -124,98 +101,21 @@ public class DefaultGPG implements GPG { if (!privateRawKey.isPresent()) { try { - final PGPKeyRingGenerator keyPair = generateKeyPair(); + final PGPKeyRingGenerator keyPair = GPGKeyPairGenerator.generateKeyPair(); - final String rawPublicKey = exportKeyRing(keyPair.generatePublicKeyRing()); - final String rawPrivateKey = exportKeyRing(keyPair.generateSecretKeyRing()); + final String rawPublicKey = GPGKeyExporter.exportKeyRing(keyPair.generatePublicKeyRing()); + final String rawPrivateKey = GPGKeyExporter.exportKeyRing(keyPair.generateSecretKeyRing()); privateKeyStore.setForUserId(userId, rawPrivateKey); publicKeyStore.add("Default SCM-Manager Signing Key", userId, rawPublicKey, true); return new DefaultPrivateKey(rawPrivateKey); } catch (PGPException | NoSuchAlgorithmException | NoSuchProviderException | IOException e) { - throw new IllegalStateException("Private key could not be generated", e); + throw new GPGException("Private key could not be generated", e); } } else { return new DefaultPrivateKey(privateRawKey.get()); } } - String exportKeyRing(PGPKeyRing keyRing) throws IOException { - final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - final ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(byteArrayOutputStream); - keyRing.encode(armoredOutputStream); - armoredOutputStream.close(); - return new String(byteArrayOutputStream.toByteArray()); - } - - PGPKeyRingGenerator generateKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); - keyPairGenerator.initialize(2048); - - KeyPair pair = keyPairGenerator.generateKeyPair(); - - PGPKeyPair keyPair = new JcaPGPKeyPair(PublicKeyAlgorithmTags.RSA_GENERAL, pair, new Date()); - final User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); - final Person person = new Person(user.getDisplayName(), user.getMail()); - - return new PGPKeyRingGenerator( - PGPSignature.POSITIVE_CERTIFICATION, - keyPair, - person.toString(), - new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1), - null, - null, - new JcaPGPContentSignerBuilder(PublicKeyAlgorithmTags.RSA_GENERAL, HashAlgorithmTags.SHA1), - new JcePBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256).build(new char[]{}) - ); - } - - static class DefaultPrivateKey implements PrivateKey { - - final Optional privateKey; - - DefaultPrivateKey(String rawPrivateKey) { - privateKey = PgpPrivateKeyExtractor.getFromRawKey(rawPrivateKey); - } - - @Override - public String getId() { - if (privateKey.isPresent()) { - return Keys.createId(privateKey.get()); - } else { - return null; - } - } - - @Override - public byte[] sign(InputStream stream) { - - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - new JcaPGPContentSignerBuilder( - PublicKeyAlgorithmTags.RSA_GENERAL, - HashAlgorithmTags.SHA1).setProvider(BouncyCastleProvider.PROVIDER_NAME) - ); - - if (privateKey.isPresent()) { - try { - signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey.get()); - } catch (PGPException e) { - throw new IllegalStateException("Could not initialize signature generator", e); - } - } else { - throw new IllegalStateException("Missing private key"); - } - - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) { - signatureGenerator.update(IOUtils.toByteArray(stream)); - signatureGenerator.generate().encode(out); - } catch (PGPException | IOException e) { - throw new IllegalStateException("Could not create signature", e); - } - - return buffer.toByteArray(); - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultPrivateKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultPrivateKey.java new file mode 100644 index 0000000000..bee5df16f3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultPrivateKey.java @@ -0,0 +1,87 @@ +/* + * 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 org.apache.commons.io.IOUtils; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import sonia.scm.security.PrivateKey; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +class DefaultPrivateKey implements PrivateKey { + + final Optional privateKey; + + DefaultPrivateKey(String rawPrivateKey) { + privateKey = KeysExtractor.extractPrivateKey(rawPrivateKey); + } + + @Override + public String getId() { + return privateKey.map(Keys::createId).orElse(null); + } + + @Override + public byte[] sign(InputStream stream) { + + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( + new JcaPGPContentSignerBuilder( + PublicKeyAlgorithmTags.RSA_GENERAL, + HashAlgorithmTags.SHA1).setProvider(BouncyCastleProvider.PROVIDER_NAME) + ); + + if (privateKey.isPresent()) { + try { + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey.get()); + } catch (PGPException e) { + throw new GPGException("Could not initialize signature generator", e); + } + } else { + throw new GPGException("Missing private key"); + } + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) { + signatureGenerator.update(IOUtils.toByteArray(stream)); + signatureGenerator.generate().encode(out); + } catch (PGPException | IOException e) { + throw new GPGException("Could not create signature", e); + } + + return buffer.toByteArray(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultPublicKey.java similarity index 95% rename from scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java rename to scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultPublicKey.java index cf1aeb5a3f..f4853c86b7 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DefaultPublicKey.java @@ -46,16 +46,16 @@ import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.Set; -public class GpgKey implements PublicKey { +public class DefaultPublicKey implements PublicKey { - private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class); + private static final Logger LOG = LoggerFactory.getLogger(DefaultPublicKey.class); private final String id; private final String owner; private final String raw; private final Set contacts; - public GpgKey(String id, String owner, String raw, Set contacts) { + public DefaultPublicKey(String id, String owner, String raw, Set contacts) { this.id = id; this.owner = owner; this.raw = raw; diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/DeletingReadonlyKeyNotAllowedException.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/DeletingReadonlyKeyNotAllowedException.java new file mode 100644 index 0000000000..9f52ed20f3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/DeletingReadonlyKeyNotAllowedException.java @@ -0,0 +1,42 @@ +/* + * 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 sonia.scm.BadRequestException; +import sonia.scm.ContextEntry; + +public final class DeletingReadonlyKeyNotAllowedException extends BadRequestException { + + public DeletingReadonlyKeyNotAllowedException(String keyId) { + super(ContextEntry.ContextBuilder.entity(RawGpgKey.class, keyId).build(), "deleting readonly gpg keys is not allowed"); + } + + private static final String CODE = "3US6mweXy1"; + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGKeyExporter.java similarity index 52% rename from scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java rename to scm-webapp/src/main/java/sonia/scm/security/gpg/GPGKeyExporter.java index c63a361295..15a21daf89 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGKeyExporter.java @@ -24,34 +24,20 @@ package sonia.scm.security.gpg; -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPKeyRing; -import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.Optional; -public class PgpPublicKeyExtractor { +class GPGKeyExporter { + private GPGKeyExporter() { } - private PgpPublicKeyExtractor() {} - - private static final Logger LOG = LoggerFactory.getLogger(PgpPublicKeyExtractor.class); - - static Optional getFromRawKey(String rawKey) { - try { - ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(rawKey.getBytes())); - PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator()); - PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey(); - return Optional.of(publicKey); - - } catch (IOException e) { - LOG.error("Invalid PGP key", e); - } - return Optional.empty(); + static String exportKeyRing(PGPKeyRing keyRing) throws IOException { + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + final ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(byteArrayOutputStream); + keyRing.encode(armoredOutputStream); + armoredOutputStream.close(); + return new String(byteArrayOutputStream.toByteArray()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGKeyPairGenerator.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGKeyPairGenerator.java new file mode 100644 index 0000000000..575c12a96e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGKeyPairGenerator.java @@ -0,0 +1,70 @@ +/* + * 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 org.apache.shiro.SecurityUtils; +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPKeyRingGenerator; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; +import sonia.scm.repository.Person; +import sonia.scm.user.User; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.Date; + +class GPGKeyPairGenerator { + static PGPKeyRingGenerator generateKeyPair() throws PGPException, NoSuchProviderException, NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(2048); + + KeyPair pair = keyPairGenerator.generateKeyPair(); + + PGPKeyPair keyPair = new JcaPGPKeyPair(PublicKeyAlgorithmTags.RSA_GENERAL, pair, new Date()); + final User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class); + final Person person = new Person(user.getDisplayName(), user.getMail()); + + return new PGPKeyRingGenerator( + PGPSignature.POSITIVE_CERTIFICATION, + keyPair, + person.toString(), + new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1), + null, + null, + new JcaPGPContentSignerBuilder(PublicKeyAlgorithmTags.RSA_GENERAL, HashAlgorithmTags.SHA1), + new JcePBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256).build(new char[]{}) + ); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/KeysExtractor.java similarity index 66% rename from scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.java rename to scm-webapp/src/main/java/sonia/scm/security/gpg/KeysExtractor.java index bcf10a57f0..0b3c502f79 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/KeysExtractor.java @@ -24,24 +24,30 @@ package sonia.scm.security.gpg; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.jcajce.JcaPGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.util.Optional; -public class PgpPrivateKeyExtractor { +public class KeysExtractor { - private PgpPrivateKeyExtractor() {} + private KeysExtractor() {} - private static final Logger LOG = LoggerFactory.getLogger(PgpPrivateKeyExtractor.class); + private static final Logger LOG = LoggerFactory.getLogger(KeysExtractor.class); - static Optional getFromRawKey(String rawKey) { + static Optional extractPrivateKey(String rawKey) { try (final InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(rawKey.getBytes()))) { JcaPGPSecretKeyRingCollection secretKeyRingCollection = new JcaPGPSecretKeyRingCollection(decoderStream); final PGPPrivateKey privateKey = secretKeyRingCollection.getKeyRings().next().getSecretKey().extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().build(new char[]{})); @@ -51,4 +57,17 @@ public class PgpPrivateKeyExtractor { return Optional.empty(); } } + + static Optional extractPublicKey(String rawKey) { + try { + ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(rawKey.getBytes())); + PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator()); + PGPPublicKey publicKey = ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey(); + return Optional.of(publicKey); + + } catch (IOException e) { + LOG.error("Invalid PGP key", e); + } + return Optional.empty(); + } } 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 c955f63d1c..6af6fbb4e6 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 @@ -65,8 +65,7 @@ public class PublicKeyResource { responseCode = "200", description = "success", content = @Content( - mediaType = "application/pgp-keys", - schema = @Schema(implementation = RawGpgKeyDto.class) + mediaType = "application/pgp-keys" ) ) @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") 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 f853fc4789..1099a9de76 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 @@ -25,7 +25,6 @@ package sonia.scm.security.gpg; import org.bouncycastle.openpgp.PGPPublicKey; -import sonia.scm.BadRequestException; import sonia.scm.ContextEntry; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Person; @@ -45,7 +44,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey; +import static sonia.scm.security.gpg.KeysExtractor.extractPublicKey; @Singleton public class PublicKeyStore { @@ -85,7 +84,7 @@ public class PublicKeyStore { RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, getContactsFromPublicKey(rawKey), Instant.now(), readonly); store.put(master, key); - eventBus.post(new PublicKeyCreatedEvent()); + eventBus.post(new PublicKeyCreatedEvent(new DefaultPublicKey(key.getId(), key.getOwner(), key.getRaw(), key.getContacts()))); return key; @@ -93,7 +92,7 @@ public class PublicKeyStore { private Set getContactsFromPublicKey(String rawKey) { List userIds = new ArrayList<>(); - Optional publicKeyFromRawKey = getFromRawKey(rawKey); + Optional publicKeyFromRawKey = extractPublicKey(rawKey); publicKeyFromRawKey.ifPresent(pgpPublicKey -> pgpPublicKey.getUserIDs().forEachRemaining(userIds::add)); return userIds.stream().map(Person::toPerson).collect(Collectors.toSet()); @@ -105,7 +104,7 @@ public class PublicKeyStore { if (!rawGpgKey.isReadonly()) { UserPermissions.changePublicKeys(rawGpgKey.getOwner()).check(); store.remove(id); - eventBus.post(new PublicKeyDeletedEvent()); + eventBus.post(new PublicKeyDeletedEvent(new DefaultPublicKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts()))); } else { throw new DeletingReadonlyKeyNotAllowedException(id); } @@ -129,20 +128,4 @@ public class PublicKeyStore { .collect(Collectors.toList()); } - @SuppressWarnings("squid:MaximumInheritanceDepth") - // exceptions have a deep inheritance depth themselves; therefore we accept this here - public static class DeletingReadonlyKeyNotAllowedException extends BadRequestException { - - public DeletingReadonlyKeyNotAllowedException(String keyId) { - super(ContextEntry.ContextBuilder.entity(RawGpgKey.class, keyId).build(), "deleting readonly gpg keys is not allowed"); - } - - private static final String CODE = "3US6mweXy1"; - - @Override - public String getCode() { - return CODE; - } - } - } diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index e5972f31c9..ec36d12acc 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -248,6 +248,14 @@ "3tS0mjSoo1": { "displayName": "Fehler bei der Erstellung eines Arbeitsverzeichnisses", "description": "Der Server konnte kein Arbeitsverzeichnis zur Abarbeitung der Anfrage erstellen. Bitte prüfen Sie die Server Logs für genauere Informationen." + }, + "3US6mweXy1": { + "displayName": "Fehler beim Löschen eines schreibgeschützen Schlüssels", + "description": "Vom Server generierte, öffentliche Schlüssel sind schreibgeschützt und können nicht gelöscht werden." + }, + "BxS5wX2v71": { + "displayName": "Inkorrekter Schlüssel", + "description": "Der bereitgestellte Schlüssel ist kein korrekt formartierter öffentlicher Schlüssel." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 8a897f82ff..85f7c0495d 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -248,6 +248,14 @@ "3tS0mjSoo1": { "displayName": "Error creating a new working directory", "description": "The server could not create a new working directory to process the request. Please check the server log for further information." + }, + "3US6mweXy1": { + "displayName": "Error deleting readonly key", + "description": "Public keys generated by the server are readonly and cannot be deleted." + }, + "BxS5wX2v71": { + "displayName": "Invalid key", + "description": "The provided key is not a valid public key." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java index 71e044347d..e44d62af2a 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultGPGTest.java @@ -81,7 +81,7 @@ class DefaultGPGTest { @InjectMocks private DefaultGPG gpg; - Subject subjectUnderTest; + private Subject subjectUnderTest; @AfterEach void unbindThreadContext() { @@ -140,40 +140,11 @@ class DefaultGPGTest { assertThat(key.getOwner().get()).contains("trillian"); } - @Test - void shouldGenerateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException { - final PGPKeyRingGenerator keyRingGenerator = gpg.generateKeyPair(); - assertThat(keyRingGenerator.generatePublicKeyRing().getPublicKey()).isNotNull(); - assertThat(keyRingGenerator.generateSecretKeyRing().getSecretKey()).isNotNull(); - } - - @Test - void shouldExportGeneratedKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException, IOException { - final PGPKeyRingGenerator keyRingGenerator = gpg.generateKeyPair(); - - final String exportedPublicKey = gpg.exportKeyRing(keyRingGenerator.generatePublicKeyRing()); - assertThat(exportedPublicKey).isNotBlank(); - assertThat(exportedPublicKey).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----"); - assertThat(exportedPublicKey).contains("-----END PGP PUBLIC KEY BLOCK-----"); - - final String exportedPrivateKey = gpg.exportKeyRing(keyRingGenerator.generateSecretKeyRing()); - assertThat(exportedPrivateKey).isNotBlank(); - assertThat(exportedPrivateKey).startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----"); - assertThat(exportedPrivateKey).contains("-----END PGP PRIVATE KEY BLOCK-----"); - } - - @Test - void shouldImportKeyPair() throws IOException, PGPException { - String raw = GPGTestHelper.readResourceAsString("private-key.asc"); - final Optional privateKey = PgpPrivateKeyExtractor.getFromRawKey(raw); - assertThat(privateKey).isPresent(); - } - @Test void shouldImportExportedGeneratedPrivateKey() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException, IOException { - final PGPKeyRingGenerator keyRingGenerator = gpg.generateKeyPair(); - final String exportedPrivateKey = gpg.exportKeyRing(keyRingGenerator.generateSecretKeyRing()); - final Optional privateKey = PgpPrivateKeyExtractor.getFromRawKey(exportedPrivateKey); + final PGPKeyRingGenerator keyRingGenerator = GPGKeyPairGenerator.generateKeyPair(); + final String exportedPrivateKey = GPGKeyExporter.exportKeyRing(keyRingGenerator.generateSecretKeyRing()); + final Optional privateKey = KeysExtractor.extractPrivateKey(exportedPrivateKey); assertThat(privateKey).isPresent(); } @@ -184,7 +155,7 @@ class DefaultGPGTest { ThreadContext.bind(subjectUnderTest); String raw = GPGTestHelper.readResourceAsString("private-key.asc"); - final DefaultGPG.DefaultPrivateKey privateKey = new DefaultGPG.DefaultPrivateKey(raw); + final DefaultPrivateKey privateKey = new DefaultPrivateKey(raw); final byte[] signature = privateKey.sign("This is a test commit".getBytes()); final String signatureString = new String(signature); assertThat(signature).isNotEmpty(); diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultPublicKeyTest.java similarity index 92% rename from scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java rename to scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultPublicKeyTest.java index a830784988..39faf3bef2 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/DefaultPublicKeyTest.java @@ -31,12 +31,12 @@ import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; -class GpgKeyTest { +class DefaultPublicKeyTest { @Test void shouldVerifyPublicKey() throws IOException { String rawPublicKey = GPGTestHelper.readResourceAsString("subkeys.asc"); - GpgKey publicKey = new GpgKey("1", "trillian", rawPublicKey, Collections.emptySet()); + DefaultPublicKey publicKey = new DefaultPublicKey("1", "trillian", rawPublicKey, Collections.emptySet()); byte[] content = GPGTestHelper.readResourceAsBytes("slarti.txt"); byte[] signature = GPGTestHelper.readResourceAsBytes("slarti.txt.asc"); diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGKeyExporterTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGKeyExporterTest.java new file mode 100644 index 0000000000..f53c45d10c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGKeyExporterTest.java @@ -0,0 +1,83 @@ +/* + * 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 org.apache.shiro.SecurityUtils; +import org.apache.shiro.mgt.DefaultSecurityManager; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRingGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sonia.scm.util.MockUtil; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; + +import static org.assertj.core.api.Assertions.assertThat; + +class GPGKeyExporterTest { + + private static void registerBouncyCastleProviderIfNecessary() { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @AfterEach + void unbindThreadContext() { + ThreadContext.unbindSubject(); + ThreadContext.unbindSecurityManager(); + } + + @BeforeEach + void bindThreadContext() { + registerBouncyCastleProviderIfNecessary(); + + SecurityUtils.setSecurityManager(new DefaultSecurityManager()); + ThreadContext.bind(MockUtil.createUserSubject(SecurityUtils.getSecurityManager())); + } + + @Test + void shouldExportGeneratedKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException, IOException { + final PGPKeyRingGenerator keyRingGenerator = GPGKeyPairGenerator.generateKeyPair(); + + final String exportedPublicKey = GPGKeyExporter.exportKeyRing(keyRingGenerator.generatePublicKeyRing()); + assertThat(exportedPublicKey).isNotBlank(); + assertThat(exportedPublicKey).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----"); + assertThat(exportedPublicKey).contains("-----END PGP PUBLIC KEY BLOCK-----"); + + final String exportedPrivateKey = GPGKeyExporter.exportKeyRing(keyRingGenerator.generateSecretKeyRing()); + assertThat(exportedPrivateKey).isNotBlank(); + assertThat(exportedPrivateKey).startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----"); + assertThat(exportedPrivateKey).contains("-----END PGP PRIVATE KEY BLOCK-----"); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGKeyPairGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGKeyPairGeneratorTest.java new file mode 100644 index 0000000000..e7485efc19 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGKeyPairGeneratorTest.java @@ -0,0 +1,73 @@ +/* + * 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 org.apache.shiro.SecurityUtils; +import org.apache.shiro.mgt.DefaultSecurityManager; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.assertj.core.api.Assertions; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRingGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sonia.scm.util.MockUtil; + +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; + +public class GPGKeyPairGeneratorTest { + + private static void registerBouncyCastleProviderIfNecessary() { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @AfterEach + void unbindThreadContext() { + ThreadContext.unbindSubject(); + ThreadContext.unbindSecurityManager(); + } + + @BeforeEach + void bindThreadContext() { + registerBouncyCastleProviderIfNecessary(); + + SecurityUtils.setSecurityManager(new DefaultSecurityManager()); + ThreadContext.bind(MockUtil.createUserSubject(SecurityUtils.getSecurityManager())); + } + + @Test + void shouldGenerateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException { + final PGPKeyRingGenerator keyRingGenerator = GPGKeyPairGenerator.generateKeyPair(); + Assertions.assertThat(keyRingGenerator.generatePublicKeyRing().getPublicKey()).isNotNull(); + Assertions.assertThat(keyRingGenerator.generateSecretKeyRing().getSecretKey()).isNotNull(); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysExtractorTest.java similarity index 79% rename from scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java rename to scm-webapp/src/test/java/sonia/scm/security/gpg/KeysExtractorTest.java index ebb328351b..9bf5f4a4de 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysExtractorTest.java @@ -24,6 +24,7 @@ package sonia.scm.security.gpg; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.junit.jupiter.api.Test; @@ -32,16 +33,23 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -class PgpPublicKeyExtractorTest { +class KeysExtractorTest { @Test void shouldExtractPublicKeyFromRawKey() throws IOException { String raw = GPGTestHelper.readResourceAsString("single.asc"); - Optional publicKey = PgpPublicKeyExtractor.getFromRawKey(raw); + Optional publicKey = KeysExtractor.extractPublicKey(raw); assertThat(publicKey).isPresent(); assertThat(Long.toHexString(publicKey.get().getKeyID())).isEqualTo("975922f193b07d6e"); } + @Test + void shouldExtractPrivateKeyFromRawKey() throws IOException { + String raw = GPGTestHelper.readResourceAsString("private-key.asc"); + final Optional privateKey = KeysExtractor.extractPrivateKey(raw); + assertThat(privateKey).isPresent(); + } + } diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java index 85b594297f..bbc2aa82e7 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java @@ -27,12 +27,10 @@ package sonia.scm.security.gpg; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; -import org.junit.Rule; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.rules.ExpectedException; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.event.ScmEventBus; @@ -163,7 +161,7 @@ class PublicKeyStoreTest { assertThat(key).isPresent(); - assertThrows(PublicKeyStore.DeletingReadonlyKeyNotAllowedException.class, () -> keyStore.delete("0x975922F193B07D6E")); + assertThrows(DeletingReadonlyKeyNotAllowedException.class, () -> keyStore.delete("0x975922F193B07D6E")); key = keyStore.findById("0x975922F193B07D6E"); assertThat(key).isPresent();