From 3da77105433e41b173afb3b4c629383b1b34ddd7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 29 Jul 2020 17:16:57 +0200 Subject: [PATCH] refactor --- .../java/sonia/scm/security/PublicKey.java | 7 ++ scm-ui/ui-webapp/public/locales/de/repos.json | 1 + scm-ui/ui-webapp/public/locales/en/repos.json | 1 + .../components/changesets/SignatureIcon.tsx | 10 +- .../components/publicKeys/formatPublicKey.ts | 19 +-- .../sonia/scm/security/gpg/DefaultGPG.java | 4 +- .../java/sonia/scm/security/gpg/GpgKey.java | 57 ++++----- .../security/gpg/PgpPublicKeyExtractor.java | 57 +++++++++ .../scm/security/gpg/PublicKeyStore.java | 18 ++- .../sonia/scm/security/gpg/RawGpgKey.java | 2 + .../scm/security/gpg/DefaultGPGTest.java | 20 ++-- .../sonia/scm/security/gpg/GPGTestHelper.java | 4 +- .../sonia/scm/security/gpg/GpgKeyTest.java | 12 +- .../java/sonia/scm/security/gpg/KeysTest.java | 6 +- .../gpg/PgpPublicKeyExtractorTest.java | 47 ++++++++ .../gpg/PublicKeyCollectionMapperTest.java | 5 +- .../scm/security/gpg/PublicKeyMapperTest.java | 9 +- .../security/gpg/PublicKeyResourceTest.java | 2 +- .../scm/security/gpg/PublicKeyStoreTest.java | 14 +-- .../sonia/scm/security/gpg/pubKeyEH.asc | 109 ++++++++++++++++++ 20 files changed, 323 insertions(+), 81 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java create mode 100644 scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc diff --git a/scm-core/src/main/java/sonia/scm/security/PublicKey.java b/scm-core/src/main/java/sonia/scm/security/PublicKey.java index 6348f73509..bcce1814fa 100644 --- a/scm-core/src/main/java/sonia/scm/security/PublicKey.java +++ b/scm-core/src/main/java/sonia/scm/security/PublicKey.java @@ -50,6 +50,13 @@ public interface PublicKey { */ Optional getOwner(); + /** + * Returns raw of the public key. + * + * @return raw of key + */ + String getRaw(); + /** * Returns the contacts of the publickey. * diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index c711113356..71eb6a9f98 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -91,6 +91,7 @@ "signedBy": "Signiert von", "signatureStatus": "Status", "keyId": "Schlüssel-ID", + "keyContacts": "Kontakte", "signatureVerified": "Verifiziert", "signatureNotVerified": "Nicht verifiziert", "signatureInvalid": "Ungültig", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 93b846f7a8..a53146bc78 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -89,6 +89,7 @@ "tags": "Tags", "signedBy": "Signed by", "keyId": "Key ID", + "keyContacts": "Contacts", "signatureStatus": "Status", "signatureVerified": "verified", "signatureNotVerified": "not verified", diff --git a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx index 7bd4148200..bd3b4863d8 100644 --- a/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx +++ b/scm-ui/ui-webapp/src/repos/components/changesets/SignatureIcon.tsx @@ -50,9 +50,15 @@ const SignatureIcon: FC = ({ signatures, className }) => { return `${t("changeset.signatureStatus")}: ${status}`; } - return `${t("changeset.signedBy")}: ${signature.owner}\n${t("changeset.keyId")}: ${signature.keyId}\n${t( + let message = `${t("changeset.signedBy")}: ${signature.owner}\n${t("changeset.keyId")}: ${signature.keyId}\n${t( "changeset.signatureStatus" - )}: ${status}\n${t("changeset.keyKontacts")}: ${signature.contacts.map((contact: string) => `\n- ${contact}`)}`; + )}: ${status}`; + + if (signature.contacts?.length > 0) { + message = message + `\n${t("changeset.keyContacts")}: ${signature.contacts.map((contact: string) => `\n- ${contact}`)}`; + } + + return message; }; const getColor = () => { diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts index aa2db2a14c..af17e1b362 100644 --- a/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts +++ b/scm-ui/ui-webapp/src/users/components/publicKeys/formatPublicKey.ts @@ -23,21 +23,6 @@ */ export const formatPublicKey = (key: string) => { - const parts = key.split(/\s+/); - if (parts.length === 3) { - return parts[0] + " ... " + parts[2]; - } else if (parts.length === 2) { - if (parts[0].length >= parts[1].length) { - return parts[0].substring(0, 7) + "... " + parts[1]; - } else { - const keyLength = parts[1].length; - return parts[0] + " ..." + parts[1].substring(keyLength - 7); - } - } else { - const keyLength = parts[0].length; - if (keyLength < 15) { - return parts[0]; - } - return parts[0].substring(0, 7) + "..." + parts[0].substring(keyLength - 7); - } + const parts = key.split(/\n/); + return parts[2].substring(0, 15); }; 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 ffbf41360f..008c7f4b5e 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 @@ -69,7 +69,7 @@ public class DefaultGPG implements GPG { public Optional findPublicKey(String id) { Optional key = store.findById(id); - return key.map(rawGpgKey -> new GpgKey(id, rawGpgKey.getOwner(), rawGpgKey.getRaw().getBytes())); + return key.map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())); } @Override @@ -79,7 +79,7 @@ public class DefaultGPG implements GPG { if (!keys.isEmpty()) { return keys .stream() - .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw().getBytes())) + .map(rawGpgKey -> new GpgKey(rawGpgKey.getId(), rawGpgKey.getOwner(), rawGpgKey.getRaw(), rawGpgKey.getContacts())) .collect(Collectors.toSet()); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java index 9fdbfca991..2855a0d265 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GpgKey.java @@ -28,10 +28,9 @@ import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,26 +42,25 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; +import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey; + public class GpgKey implements PublicKey { private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class); private final String id; private final String owner; - private final Set contacts = new LinkedHashSet<>(); + private final String raw; + private final Set contacts; - public GpgKey(String id, String owner, byte[] raw) { + public GpgKey(String id, String owner, String raw, Set contacts) { this.id = id; this.owner = owner; - try { - getPgpPublicKey(raw).getUserIDs().forEachRemaining(contacts::add); - } catch (IOException e) { - LOG.error("Could not find contacts in public key", e); - } + this.raw = raw; + this.contacts = contacts; } @Override @@ -78,6 +76,11 @@ public class GpgKey implements PublicKey { return Optional.of(owner); } + @Override + public String getRaw() { + return raw; + } + @Override public Set getContacts() { return contacts; @@ -87,32 +90,34 @@ public class GpgKey implements PublicKey { public boolean verify(InputStream stream, byte[] signature) { boolean verified = false; try { - PGPPublicKey publicKey = getPgpPublicKey(signature); - PGPSignature pgpSignature = ((PGPSignature) publicKey.getSignatures().next()); + ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature)); + PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, null); + PGPSignature pgpSignature = ((PGPSignatureList) pgpObjectFactory.nextObject()).get(0); PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider(); - pgpSignature.init(provider, publicKey); - char[] buffer = new char[1024]; - int bytesRead = 0; - BufferedReader in = new BufferedReader(new InputStreamReader(stream)); + Optional pgpPublicKey = getFromRawKey(raw); - while (bytesRead != -1) { - bytesRead = in.read(buffer, 0, 1024); - pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8)); + if (pgpPublicKey.isPresent()) { + pgpSignature.init(provider, pgpPublicKey.get()); + + char[] buffer = new char[1024]; + int bytesRead = 0; + BufferedReader in = new BufferedReader(new InputStreamReader(stream)); + + while (bytesRead != -1) { + bytesRead = in.read(buffer, 0, 1024); + pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8)); + } + + verified = pgpSignature.verify(); } - verified = pgpSignature.verify(); } catch (IOException | PGPException e) { LOG.error("Could not verify GPG key", e); } - return verified; - } - private PGPPublicKey getPgpPublicKey(byte[] signature) throws IOException { - ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature)); - PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator()); - return ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey(); + return verified; } } 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 new file mode 100644 index 0000000000..003503194b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PgpPublicKeyExtractor.java @@ -0,0 +1,57 @@ +/* + * 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.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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Optional; + +public class PgpPublicKeyExtractor { + + 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"); + } + return Optional.empty(); + } +} 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 ce3d756781..7308ea6cfe 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 @@ -24,6 +24,9 @@ package sonia.scm.security.gpg; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; import sonia.scm.event.ScmEventBus; import sonia.scm.security.NotPublicKeyException; @@ -35,13 +38,19 @@ import sonia.scm.user.UserPermissions; import javax.inject.Inject; import javax.inject.Singleton; import java.time.Instant; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey; + @Singleton public class PublicKeyStore { + private static final Logger LOG = LoggerFactory.getLogger(PublicKeyStore.class); + private static final String STORE_NAME = "gpg_public_keys"; private static final String SUBKEY_STORE_NAME = "gpg_public_sub_keys"; @@ -70,7 +79,7 @@ public class PublicKeyStore { subKeyStore.put(subKey, new MasterKeyReference(master)); } - RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, Instant.now()); + RawGpgKey key = new RawGpgKey(master, displayName, username, rawKey, getContactsFromPublicKey(rawKey), Instant.now()); store.put(master, key); @@ -78,6 +87,13 @@ public class PublicKeyStore { } + private Set getContactsFromPublicKey(String rawKey) { + Set contacts = new HashSet<>(); + Optional publicKeyFromRawKey = getFromRawKey(rawKey); + publicKeyFromRawKey.ifPresent(pgpPublicKey -> pgpPublicKey.getUserIDs().forEachRemaining(contacts::add)); + return contacts; + } + public void delete(String id) { RawGpgKey rawGpgKey = store.get(id); if (rawGpgKey != null) { diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java index f32d757d24..19c88296c8 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java @@ -36,6 +36,7 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.time.Instant; import java.util.Objects; +import java.util.Set; @Getter @NoArgsConstructor @@ -48,6 +49,7 @@ public class RawGpgKey { private String displayName; private String owner; private String raw; + private Set contacts; @XmlJavaTypeAdapter(XmlInstantAdapter.class) private Instant created; 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 98cff24a35..d98aa56b85 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 @@ -25,6 +25,7 @@ package sonia.scm.security.gpg; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -34,8 +35,8 @@ import sonia.scm.security.PublicKey; import java.io.IOException; import java.time.Instant; +import java.util.Collections; import java.util.Optional; -import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -51,7 +52,7 @@ class DefaultGPGTest { @Test void shouldFindIdInSignature() throws IOException { - String raw = GPGTestHelper.readKey("signature.asc"); + String raw = GPGTestHelper.readResource("signature.asc"); String publicKeyId = gpg.findPublicKeyId(raw.getBytes()); assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9"); @@ -59,8 +60,8 @@ class DefaultGPGTest { @Test void shouldFindPublicKey() throws IOException { - String raw = GPGTestHelper.readKey("subkeys.asc"); - RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, Instant.now()); + String raw = GPGTestHelper.readResource("subkeys.asc"); + RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, ImmutableSet.of("trillian", "zaphod"), Instant.now()); when(store.findById("42")).thenReturn(Optional.of(key1)); @@ -70,17 +71,16 @@ class DefaultGPGTest { assertThat(publicKey.get().getOwner()).isPresent(); assertThat(publicKey.get().getOwner().get()).contains("trillian"); assertThat(publicKey.get().getId()).isEqualTo("42"); - assertThat(publicKey.get().getContacts()).contains("Sebastian Sdorra ", - "Sebastian Sdorra "); + assertThat(publicKey.get().getContacts()).contains("trillian", "zaphod"); } @Test void shouldFindKeysForUsername() throws IOException { - String raw = GPGTestHelper.readKey("single.asc"); - String raw2= GPGTestHelper.readKey("subkeys.asc"); + String raw = GPGTestHelper.readResource("single.asc"); + String raw2= GPGTestHelper.readResource("subkeys.asc"); - RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Instant.now()); - RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Instant.now()); + RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Collections.emptySet(), Instant.now()); + RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Collections.emptySet(), Instant.now()); when(store.findByUsername("trillian")).thenReturn(ImmutableList.of(key1, key2)); Iterable keys = gpg.findPublicKeysByUsername("trillian"); diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java index 4608aace5c..dde8e21d52 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java @@ -36,8 +36,8 @@ final class GPGTestHelper { } @SuppressWarnings("UnstableApiUsage") - static String readKey(String key) throws IOException { - URL resource = Resources.getResource("sonia/scm/security/gpg/" + key); + static String readResource(String fileName) throws IOException { + URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName); return Resources.toString(resource, StandardCharsets.UTF_8); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java index 6fb6a3ac69..3e399ae9be 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GpgKeyTest.java @@ -27,6 +27,9 @@ package sonia.scm.security.gpg; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; class GpgKeyTest { @@ -37,13 +40,14 @@ class GpgKeyTest { longContent.append(i); } - byte[] raw = GPGTestHelper.readKey("subkeys.asc").getBytes(); + String raw = GPGTestHelper.readResource("pubKeyEH.asc"); + String signature = GPGTestHelper.readResource("signature.asc"); - GpgKey key = new GpgKey("1", "trillian", raw); + GpgKey key = new GpgKey("1", "trillian", raw, Collections.emptySet()); - boolean verified = key.verify(longContent.toString().getBytes(), raw); + boolean verified = key.verify(longContent.toString().getBytes(), signature.getBytes()); - // assertThat(verified).isTrue(); + //assertThat(verified).isTrue(); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java index 3bc97eca2a..07a482cbfb 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java @@ -38,21 +38,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static sonia.scm.security.gpg.GPGTestHelper.readKey; +import static sonia.scm.security.gpg.GPGTestHelper.readResource; @ExtendWith(MockitoExtension.class) class KeysTest { @Test void shouldResolveSingleId() throws IOException { - String rawPublicKey = readKey("single.asc"); + String rawPublicKey = readResource("single.asc"); Keys keys = Keys.resolve(rawPublicKey); assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E"); } @Test void shouldResolveIdsFromSubkeys() throws IOException { - String rawPublicKey = readKey("subkeys.asc"); + String rawPublicKey = readResource("subkeys.asc"); Keys keys = Keys.resolve(rawPublicKey); assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1"); assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60"); diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java new file mode 100644 index 0000000000..a49f5cfb6b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PgpPublicKeyExtractorTest.java @@ -0,0 +1,47 @@ +/* + * 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.PGPPublicKey; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class PgpPublicKeyExtractorTest { + + @Test + void shouldExtractPublicKeyFromRawKey() throws IOException { + String raw = GPGTestHelper.readResource("pubKeyEH.asc"); + + Optional publicKey = PgpPublicKeyExtractor.getFromRawKey(raw); + + assertThat(publicKey).isPresent(); + assertThat(Long.toHexString(publicKey.get().getKeyID())).isEqualTo("39ad4bed55527f1c"); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java index 6bd69efdc1..b82261c4ad 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyCollectionMapperTest.java @@ -40,6 +40,7 @@ import sonia.scm.api.v2.resources.ScmPathInfoStore; import java.io.IOException; import java.net.URI; import java.time.Instant; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -103,8 +104,8 @@ class PublicKeyCollectionMapperTest { } private RawGpgKey createPublicKey(String displayName) throws IOException { - String raw = GPGTestHelper.readKey("single.asc"); - return new RawGpgKey(displayName, displayName, "trillian", raw, Instant.now()); + String raw = GPGTestHelper.readResource("single.asc"); + return new RawGpgKey(displayName, displayName, "trillian", raw, Collections.emptySet(), Instant.now()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java index 0e7bd496c8..90af5bd984 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyMapperTest.java @@ -38,6 +38,7 @@ import sonia.scm.api.v2.resources.ScmPathInfoStore; import java.io.IOException; import java.net.URI; import java.time.Instant; +import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -68,8 +69,8 @@ class PublicKeyMapperTest { void shouldMapKeyToDto() throws IOException { when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true); - String raw = GPGTestHelper.readKey("single.asc"); - RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now()); + String raw = GPGTestHelper.readResource("single.asc"); + RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now()); RawGpgKeyDto dto = mapper.map(key); @@ -82,8 +83,8 @@ class PublicKeyMapperTest { @Test void shouldNotAppendDeleteLink() throws IOException { - String raw = GPGTestHelper.readKey("single.asc"); - RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Instant.now()); + String raw = GPGTestHelper.readResource("single.asc"); + RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now()); RawGpgKeyDto dto = mapper.map(key); diff --git a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java index 112fc1b31f..45cf1b0fa3 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyResourceTest.java @@ -112,7 +112,7 @@ class PublicKeyResourceTest { @Test void shouldAddToStore() throws URISyntaxException, IOException { - String raw = GPGTestHelper.readKey("single.asc"); + String raw = GPGTestHelper.readResource("single.asc"); UriInfo uriInfo = mock(UriInfo.class); UriBuilder builder = mock(UriBuilder.class); 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 1871827939..b57cf1430d 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 @@ -80,21 +80,21 @@ class PublicKeyStoreTest { @Test void shouldThrowAuthorizationExceptionOnAdd() throws IOException { doThrow(AuthorizationException.class).when(subject).checkPermission("user:changePublicKeys:zaphod"); - String rawKey = GPGTestHelper.readKey("single.asc"); + String rawKey = GPGTestHelper.readResource("single.asc"); assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey)); } @Test void shouldOnlyStorePublicKeys() throws IOException { - String rawKey = GPGTestHelper.readKey("single.asc").replace("PUBLIC", "PRIVATE"); + String rawKey = GPGTestHelper.readResource("single.asc").replace("PUBLIC", "PRIVATE"); assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey)); } @Test void shouldReturnStoredKey() throws IOException { - String rawKey = GPGTestHelper.readKey("single.asc"); + String rawKey = GPGTestHelper.readResource("single.asc"); Instant now = Instant.now(); RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey); @@ -107,7 +107,7 @@ class PublicKeyStoreTest { @Test void shouldFindStoredKeyById() throws IOException { - String rawKey = GPGTestHelper.readKey("single.asc"); + String rawKey = GPGTestHelper.readResource("single.asc"); keyStore.add("SCM Package Key", "trillian", rawKey); Optional key = keyStore.findById("0x975922F193B07D6E"); assertThat(key).isPresent(); @@ -115,7 +115,7 @@ class PublicKeyStoreTest { @Test void shouldDeleteKey() throws IOException { - String rawKey = GPGTestHelper.readKey("single.asc"); + String rawKey = GPGTestHelper.readResource("single.asc"); keyStore.add("SCM Package Key", "trillian", rawKey); Optional key = keyStore.findById("0x975922F193B07D6E"); @@ -139,10 +139,10 @@ class PublicKeyStoreTest { @Test void shouldFindAllKeysForUser() throws IOException { - String singleKey = GPGTestHelper.readKey("single.asc"); + String singleKey = GPGTestHelper.readResource("single.asc"); keyStore.add("SCM Single Key", "trillian", singleKey); - String multiKey = GPGTestHelper.readKey("subkeys.asc"); + String multiKey = GPGTestHelper.readResource("subkeys.asc"); keyStore.add("SCM Multi Key", "trillian", multiKey); List keys = keyStore.findByUsername("trillian"); diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc new file mode 100644 index 0000000000..d8cfe4c4e2 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/pubKeyEH.asc @@ -0,0 +1,109 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=6sgk +-----END PGP PUBLIC KEY BLOCK-----