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