From 143d4273b866352d4adc85cb924cb674840267f8 Mon Sep 17 00:00:00 2001
From: Konstantin Schaper
Date: Thu, 6 Aug 2020 21:57:31 +0200
Subject: [PATCH] allow key download from signature in changeset view
---
.../scm/api/v2/resources/ChangesetDto.java | 2 +-
.../scm/api/v2/resources/SignatureDto.java | 53 ++++++++++++++
.../src/repos/changesets/SignatureIcon.tsx | 72 ++++++++++++-------
scm-ui/ui-types/src/Changesets.ts | 5 +-
scm-ui/ui-webapp/public/locales/de/repos.json | 1 +
scm-ui/ui-webapp/public/locales/en/repos.json | 1 +
.../components/publicKeys/PublicKeyEntry.tsx | 5 +-
.../DefaultChangesetToChangesetDtoMapper.java | 24 ++++++-
.../sonia/scm/security/gpg/DefaultGPG.java | 44 ++++++------
.../java/sonia/scm/security/gpg/Keys.java | 13 +++-
.../security/gpg/PgpPrivateKeyExtractor.java | 54 ++++++++++++++
.../security/gpg/PgpPublicKeyExtractor.java | 2 +-
.../scm/security/gpg/PublicKeyMapper.java | 2 +-
.../scm/security/gpg/DefaultGPGTest.java | 42 ++++++-----
14 files changed, 244 insertions(+), 76 deletions(-)
create mode 100644 scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java
create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.java
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java
index 2a3db03b67..6721aa215c 100644
--- a/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ChangesetDto.java
@@ -62,7 +62,7 @@ public class ChangesetDto extends HalRepresentation {
private List contributors;
- private List signatures;
+ private List signatures;
public ChangesetDto(Links links, Embedded embedded) {
super(links, embedded);
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java
new file mode 100644
index 0000000000..1ba2fdc1f0
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/SignatureDto.java
@@ -0,0 +1,53 @@
+/*
+ * 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.api.v2.resources;
+
+import de.otto.edison.hal.HalRepresentation;
+import de.otto.edison.hal.Links;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import sonia.scm.repository.Person;
+import sonia.scm.repository.SignatureStatus;
+
+import java.util.Optional;
+import java.util.Set;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SignatureDto extends HalRepresentation {
+
+ private String keyId;
+ private String type;
+ private SignatureStatus status;
+ private Optional owner;
+ private Set contacts;
+
+ public SignatureDto(Links links) {
+ super(links);
+ }
+
+}
diff --git a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
index 65ad8453a0..f79e6dd413 100644
--- a/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
+++ b/scm-ui/ui-components/src/repos/changesets/SignatureIcon.tsx
@@ -21,12 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-import React, { FC } from "react";
-import { useTranslation } from "react-i18next";
-import { Signature } from "@scm-manager/ui-types";
+import React, {FC} from "react";
+import {useTranslation} from "react-i18next";
+import {Signature} from "@scm-manager/ui-types";
import styled from "styled-components";
import Icon from "../../Icon";
-import Tooltip from "../../Tooltip";
+import {usePopover} from "../../popover";
+import Popover from "../../popover/Popover";
type Props = {
signatures: Signature[];
@@ -41,17 +42,21 @@ const StyledIcon = styled(Icon)`
margin-bottom: 0.2em;
`;
+const StyledDiv = styled.div`
+ > *:not(:last-child) {
+ margin-bottom: 24px;
+ }
+`;
-const SignatureIcon: FC = ({ signatures, className }) => {
+const SignatureIcon: FC = ({signatures, className}) => {
const [t] = useTranslation("repos");
+ const {popoverProps, triggerProps} = usePopover();
- const signature = signatures?.length > 0 ? signatures[0] : undefined;
-
- if (!signature) {
+ if (!signatures.length) {
return null;
}
- const createTooltipMessage = () => {
+ const createSignatureBlock = (signature: Signature) => {
let status;
if (signature.status === "VERIFIED") {
status = t("changeset.signatureVerified");
@@ -62,37 +67,50 @@ const SignatureIcon: FC = ({ signatures, className }) => {
}
if (signature.status === "NOT_FOUND") {
- return `${t("changeset.signatureStatus")}: ${status}\n${t("changeset.keyId")}: ${signature.keyId}`;
+ return
+
{t("changeset.signatureStatus")}: {status}
+ {t("changeset.keyId")}: {signature.keyId}
+
;
}
- let message = `${t("changeset.keyOwner")}: ${signature.owner ? signature.owner : t("changeset.noOwner")}\n${t("changeset.keyId")}: ${signature.keyId}\n${t(
- "changeset.signatureStatus"
- )}: ${status}`;
-
- if (signature.contacts?.length > 0) {
- message += `\n${t("changeset.keyContacts")}:`;
- signature.contacts.forEach((contact) => {
- message += `\n- ${contact.name} <${contact.mail}>`;
- });
- }
- return message;
+ return
+
{t("changeset.keyOwner")}: {signature.owner || t("changeset.noOwner")}
+ {t("changeset.keyId")}: {
+ signature._links?.rawKey ?
{signature.keyId} : signature.keyId
+ }
+ {t("changeset.signatureStatus")}: {status}
+ {signature.contacts?.length > 0 && <>
+ {t("changeset.keyContacts")}:
+ {signature.contacts.map(contact => - {contact.name}{contact.mail && ` <${contact.mail}>`}
)}
+ >}
+ ;
};
+ const signatureElements = signatures.map(signature => createSignatureBlock(signature));
+
const getColor = () => {
- if (signature.status === "VERIFIED") {
+ const verified = signatures.some(sig => sig.status === "VERIFIED");
+ if (verified) {
return "success";
}
- if (signature.status === "INVALID") {
+ const invalid = signatures.some(sig => sig.status === "INVALID");
+ if (invalid) {
return "danger";
}
return undefined;
};
-
return (
-
-
-
+ <>
+
+
+ {signatureElements}
+
+
+
+
+
+ >
);
};
diff --git a/scm-ui/ui-types/src/Changesets.ts b/scm-ui/ui-types/src/Changesets.ts
index 04d2913647..60f1bddea7 100644
--- a/scm-ui/ui-types/src/Changesets.ts
+++ b/scm-ui/ui-types/src/Changesets.ts
@@ -22,7 +22,7 @@
* SOFTWARE.
*/
-import { Collection, Links } from "./hal";
+import {Collection, Link, Links} from "./hal";
import { Tag } from "./Tags";
import { Branch } from "./Branches";
import { Person } from "./Person";
@@ -48,6 +48,9 @@ export type Signature = {
status: "VERIFIED" | "NOT_FOUND" | "INVALID";
owner: string;
contacts: Person[];
+ _links?: {
+ rawKey?: Link;
+ };
}
export type Contributor = {
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index ad35ed7371..2098d38587 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -96,6 +96,7 @@
"signatureVerified": "Verifiziert",
"signatureNotVerified": "Nicht verifiziert",
"signatureInvalid": "Ungültig",
+ "signatures": "Signaturen",
"shortlink": {
"title": "Changeset {{id}} aus {{namespace}}/{{name}}"
},
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index daa5bc0caa..b5beb1bf55 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -95,6 +95,7 @@
"signatureVerified": "verified",
"signatureNotVerified": "not verified",
"signatureInvalid": "invalid",
+ "signatures": "Signatures",
"shortlink": {
"title": "Changeset {{id}} of {{namespace}}/{{name}}"
},
diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
index 108f53873d..8ef703752f 100644
--- a/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
+++ b/scm-ui/ui-webapp/src/users/components/publicKeys/PublicKeyEntry.tsx
@@ -27,6 +27,7 @@ import {DateFromNow, DeleteButton, DownloadButton} from "@scm-manager/ui-compone
import { PublicKey } from "./SetPublicKeys";
import { useTranslation } from "react-i18next";
import { Link } from "@scm-manager/ui-types";
+import styled from "styled-components";
type Props = {
publicKey: PublicKey;
@@ -53,12 +54,12 @@ export const PublicKeyEntry: FC = ({ publicKey, onDelete }) => {
<>
| {publicKey.displayName} |
-
+ |
|
{publicKey.id} |
{deleteButton} |
- {downloadButton} |
+ {downloadButton} |
>
);
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 2314cd0aa3..354ade5c86 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
@@ -31,13 +31,15 @@ import org.mapstruct.Mapper;
import org.mapstruct.ObjectFactory;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Changeset;
+import sonia.scm.repository.Contributor;
import sonia.scm.repository.Person;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.Signature;
import sonia.scm.repository.Tag;
-import sonia.scm.repository.Contributor;
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.web.EdisonHalAppender;
import javax.inject.Inject;
@@ -67,10 +69,30 @@ public abstract class DefaultChangesetToChangesetDtoMapper extends HalAppenderMa
@Inject
private TagCollectionToDtoMapper tagCollectionToDtoMapper;
+ @Inject
+ private ScmPathInfoStore scmPathInfoStore;
+
abstract ContributorDto map(Contributor contributor);
+ abstract SignatureDto map(Signature signature);
+
abstract PersonDto map(Person person);
+ @ObjectFactory
+ SignatureDto createDto(Signature signature) {
+ if (signature.getType().equals("gpg")) {
+ final Links.Builder linkBuilder =
+ linkingTo()
+ .single(link("rawKey", new LinkBuilder(scmPathInfoStore.get(), PublicKeyResource.class)
+ .method("findByIdGpg")
+ .parameters(signature.getKeyId())
+ .href()));
+
+ return new SignatureDto(linkBuilder.build());
+ }
+ return new SignatureDto();
+ }
+
@ObjectFactory
ChangesetDto createDto(@Context Repository repository, Changeset source) {
String namespace = repository.getNamespace();
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 4742fd7990..6bfe085a52 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
@@ -42,19 +42,18 @@ import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureList;
-import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.jcajce.JcaPGPSecretKeyRingCollection;
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.JcePBESecretKeyDecryptorBuilder;
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;
@@ -74,7 +73,6 @@ import java.util.stream.Collectors;
public class DefaultGPG implements GPG {
private static final Logger LOG = LoggerFactory.getLogger(DefaultGPG.class);
- static final String PRIVATE_KEY_ID = "SCM-KEY-ID";
private final PublicKeyStore publicKeyStore;
private final PrivateKeyStore privateKeyStore;
@@ -144,14 +142,6 @@ public class DefaultGPG implements GPG {
}
}
- static PGPPrivateKey importPrivateKey(String rawKey) throws IOException, PGPException {
- 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[]{}));
- return privateKey;
- }
- }
-
String exportKeyRing(PGPKeyRing keyRing) throws IOException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(byteArrayOutputStream);
@@ -167,11 +157,13 @@ public class DefaultGPG implements GPG {
KeyPair pair = keyPairGenerator.generateKeyPair();
PGPKeyPair keyPair = new JcaPGPKeyPair(PGPPublicKey.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,
- PRIVATE_KEY_ID,
+ person.toString(),
new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1),
null,
null,
@@ -182,19 +174,19 @@ public class DefaultGPG implements GPG {
static class DefaultPrivateKey implements PrivateKey {
- final PGPPrivateKey privateKey;
+ final Optional privateKey;
DefaultPrivateKey(String rawPrivateKey) {
- try {
- privateKey = importPrivateKey(rawPrivateKey);
- } catch (IOException | PGPException e) {
- throw new IllegalStateException("Could not read private key", e);
- }
+ privateKey = PgpPrivateKeyExtractor.getFromRawKey(rawPrivateKey);
}
@Override
public String getId() {
- return PRIVATE_KEY_ID;
+ if (privateKey.isPresent()) {
+ return Keys.createId(privateKey.get());
+ } else {
+ return null;
+ }
}
@Override
@@ -206,10 +198,14 @@ public class DefaultGPG implements GPG {
HashAlgorithmTags.SHA1).setProvider(BouncyCastleProvider.PROVIDER_NAME)
);
- try {
- signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
- } catch (PGPException e) {
- throw new IllegalStateException("Could not initialize signature generator", e);
+ 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();
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
index 50075c217f..a03819b923 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java
@@ -26,6 +26,7 @@ package sonia.scm.security.gpg;
import lombok.Value;
import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
@@ -86,8 +87,16 @@ final class Keys {
return new Keys(master, Collections.unmodifiableSet(subs));
}
- private static String createId(PGPPublicKey pgpPublicKey) {
- return "0x" + Long.toHexString(pgpPublicKey.getKeyID()).toUpperCase(Locale.ENGLISH);
+ static String createId(PGPPublicKey pgpPublicKey) {
+ return formatKey(pgpPublicKey.getKeyID());
+ }
+
+ static String createId(PGPPrivateKey pgpPrivateKey) {
+ return formatKey(pgpPrivateKey.getKeyID());
+ }
+
+ static String formatKey(long keyId) {
+ return "0x" + Long.toHexString(keyId).toUpperCase(Locale.ENGLISH);
}
private static List collectKeys(String rawKey) {
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.java
new file mode 100644
index 0000000000..bcf10a57f0
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPrivateKeyExtractor.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.security.gpg;
+
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPSecretKeyRingCollection;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Optional;
+
+public class PgpPrivateKeyExtractor {
+
+ private PgpPrivateKeyExtractor() {}
+
+ private static final Logger LOG = LoggerFactory.getLogger(PgpPrivateKeyExtractor.class);
+
+ static Optional getFromRawKey(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[]{}));
+ return Optional.of(privateKey);
+ } catch (Exception e) {
+ LOG.error("Invalid PGP key", e);
+ return Optional.empty();
+ }
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java
index 003503194b..c63a361295 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java
@@ -50,7 +50,7 @@ public class PgpPublicKeyExtractor {
return Optional.of(publicKey);
} catch (IOException e) {
- LOG.error("Invalid PGP key");
+ LOG.error("Invalid PGP key", e);
}
return Optional.empty();
}
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 5b3097a785..81604153f6 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
@@ -51,7 +51,7 @@ public abstract class PublicKeyMapper {
}
@Mapping(target = "attributes", ignore = true)
-// @Mapping(target = "raw", ignore = true) // TODO: Why is there ?
+ @Mapping(target = "raw", ignore = true)
abstract RawGpgKeyDto map(RawGpgKey rawGpgKey);
@ObjectFactory
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 c67e8cbbbf..71e044347d 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
@@ -34,6 +34,8 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
import org.bouncycastle.openpgp.PGPPrivateKey;
+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.mockito.InjectMocks;
@@ -79,6 +81,23 @@ class DefaultGPGTest {
@InjectMocks
private DefaultGPG gpg;
+ Subject subjectUnderTest;
+
+ @AfterEach
+ void unbindThreadContext() {
+ ThreadContext.unbindSubject();
+ ThreadContext.unbindSecurityManager();
+ }
+
+ @BeforeEach
+ void bindThreadContext() {
+ registerBouncyCastleProviderIfNecessary();
+
+ SecurityUtils.setSecurityManager(new DefaultSecurityManager());
+ subjectUnderTest = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
+ ThreadContext.bind(subjectUnderTest);
+ }
+
@Test
void shouldFindIdInSignature() throws IOException {
String raw = GPGTestHelper.readResourceAsString("slarti.txt.asc");
@@ -123,8 +142,6 @@ class DefaultGPGTest {
@Test
void shouldGenerateKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException {
- registerBouncyCastleProviderIfNecessary();
-
final PGPKeyRingGenerator keyRingGenerator = gpg.generateKeyPair();
assertThat(keyRingGenerator.generatePublicKeyRing().getPublicKey()).isNotNull();
assertThat(keyRingGenerator.generateSecretKeyRing().getSecretKey()).isNotNull();
@@ -132,8 +149,6 @@ class DefaultGPGTest {
@Test
void shouldExportGeneratedKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException, IOException {
- registerBouncyCastleProviderIfNecessary();
-
final PGPKeyRingGenerator keyRingGenerator = gpg.generateKeyPair();
final String exportedPublicKey = gpg.exportKeyRing(keyRingGenerator.generatePublicKeyRing());
@@ -150,27 +165,26 @@ class DefaultGPGTest {
@Test
void shouldImportKeyPair() throws IOException, PGPException {
String raw = GPGTestHelper.readResourceAsString("private-key.asc");
- final PGPPrivateKey privateKey = DefaultGPG.importPrivateKey(raw);
- assertThat(privateKey).isNotNull();
+ final Optional privateKey = PgpPrivateKeyExtractor.getFromRawKey(raw);
+ assertThat(privateKey).isPresent();
}
@Test
void shouldImportExportedGeneratedPrivateKey() throws NoSuchProviderException, NoSuchAlgorithmException, PGPException, IOException {
- registerBouncyCastleProviderIfNecessary();
-
final PGPKeyRingGenerator keyRingGenerator = gpg.generateKeyPair();
final String exportedPrivateKey = gpg.exportKeyRing(keyRingGenerator.generateSecretKeyRing());
- final PGPPrivateKey privateKey = DefaultGPG.importPrivateKey(exportedPrivateKey);
- assertThat(privateKey).isNotNull();
+ final Optional privateKey = PgpPrivateKeyExtractor.getFromRawKey(exportedPrivateKey);
+ assertThat(privateKey).isPresent();
}
@Test
void shouldCreateSignature() throws IOException {
- registerBouncyCastleProviderIfNecessary();
+ SecurityUtils.setSecurityManager(new DefaultSecurityManager());
+ Subject subjectUnderTest = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
+ ThreadContext.bind(subjectUnderTest);
String raw = GPGTestHelper.readResourceAsString("private-key.asc");
final DefaultGPG.DefaultPrivateKey privateKey = new DefaultGPG.DefaultPrivateKey(raw);
- assertThat(privateKey.getId()).contains(DefaultGPG.PRIVATE_KEY_ID);
final byte[] signature = privateKey.sign("This is a test commit".getBytes());
final String signatureString = new String(signature);
assertThat(signature).isNotEmpty();
@@ -180,8 +194,6 @@ class DefaultGPGTest {
@Test
void shouldReturnGeneratedPrivateKeyIfNoneStored() {
- registerBouncyCastleProviderIfNecessary();
-
SecurityUtils.setSecurityManager(new DefaultSecurityManager());
Subject subjectUnderTest = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
ThreadContext.bind(subjectUnderTest);
@@ -195,8 +207,6 @@ class DefaultGPGTest {
@Test
void shouldReturnStoredPrivateKey() throws IOException {
- registerBouncyCastleProviderIfNecessary();
-
SecurityUtils.setSecurityManager(new DefaultSecurityManager());
Subject subjectUnderTest = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
ThreadContext.bind(subjectUnderTest);