diff --git a/pom.xml b/pom.xml index 0335226c4a..11b51fb99c 100644 --- a/pom.xml +++ b/pom.xml @@ -525,6 +525,26 @@ 1.14 + + + + org.bouncycastle + bcpg-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + @@ -899,6 +919,7 @@ 4.2.3 2.3.3 6.1.5.Final + 1.65 1.6.2 diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 5daec406b4..eb83eaeb11 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -114,6 +114,23 @@ ${jjwt.version} + + + + org.bouncycastle + bcpg-jdk15on + + + + org.bouncycastle + bcprov-jdk15on + + + + org.bouncycastle + bcpkix-jdk15on + + diff --git a/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGException.java b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGException.java new file mode 100644 index 0000000000..da278f03da --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/GPGException.java @@ -0,0 +1,35 @@ +/* + * 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; + +public class GPGException extends RuntimeException { + GPGException(String message) { + super(message); + } + + GPGException(String message, Throwable cause) { + super(message, cause); + } +} 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 new file mode 100644 index 0000000000..7ecbd1d49d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/Keys.java @@ -0,0 +1,86 @@ +/* + * 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.gpg.keybox.PublicKeyRingBlob; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRing; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import sonia.scm.security.PublicKey; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +final class Keys { + + private static final KeyFingerPrintCalculator calculator = new JcaKeyFingerprintCalculator(); + + private Keys() {} + + static String resolveIdFromKey(String rawKey) throws IOException, PGPException { + List keys = collectKeys(rawKey); + if (keys.size() > 1) { + keys = keys.stream().filter(PGPPublicKey::isMasterKey).collect(Collectors.toList()); + } + if (keys.isEmpty()) { + throw new IllegalArgumentException("found multiple keys, but no master keys"); + } + if (keys.size() > 1) { + throw new IllegalArgumentException("found multiple master keys"); + } + + PGPPublicKey pgpPublicKey = keys.get(0); + return createId(pgpPublicKey); + } + + private static String createId(PGPPublicKey pgpPublicKey) { + return "0x" + Long.toHexString(pgpPublicKey.getKeyID()).toUpperCase(Locale.ENGLISH); + } + + private static List collectKeys(String rawKey) throws IOException, PGPException { + List publicKeys = new ArrayList<>(); + InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(rawKey.getBytes(StandardCharsets.UTF_8))); + PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(decoderStream, calculator); + for (PGPPublicKeyRing pgpPublicKeys : collection) { + for (PGPPublicKey pgpPublicKey : pgpPublicKeys) { + publicKeys.add(pgpPublicKey); + } + } + return publicKeys; + } +} 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 new file mode 100644 index 0000000000..394e657e59 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/PublicKeyStore.java @@ -0,0 +1,86 @@ +/* + * 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 com.google.common.annotations.VisibleForTesting; +import org.apache.shiro.SecurityUtils; +import org.bouncycastle.openpgp.PGPException; +import sonia.scm.security.PublicKey; +import sonia.scm.store.DataStore; +import sonia.scm.store.DataStoreFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +@Singleton +public class PublicKeyStore { + + private static final String STORE_NAME = "gpg_public_keys"; + + private final DataStore store; + private final Supplier currentUserSupplier; + + @Inject + public PublicKeyStore(DataStoreFactory dataStoreFactory) { + this( + dataStoreFactory.withType(RawGpgKey.class).withName(STORE_NAME).build(), + () -> SecurityUtils.getSubject().getPrincipal().toString() + ); + } + + @VisibleForTesting + PublicKeyStore(DataStore store, Supplier currentUserSupplier) { + this.store = store; + this.currentUserSupplier = currentUserSupplier; + } + + public RawGpgKey add(String displayName, String rawKey) { + try { + String id = Keys.resolveIdFromKey(rawKey); + RawGpgKey key = new RawGpgKey(id, displayName, currentUserSupplier.get(), rawKey, Instant.now()); + + store.put(id, key); + + return key; + } catch (IOException | PGPException e) { + throw new GPGException("failed to resolve id from gpg key"); + } + } + + public Optional findById(String id) { + return store.getOptional(id); + } + +} 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 new file mode 100644 index 0000000000..5b098c5af3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/gpg/RawGpgKey.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.security.gpg; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sonia.scm.xml.XmlInstantAdapter; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.time.Instant; +import java.util.Objects; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@XmlAccessorType(XmlAccessType.FIELD) +public class RawGpgKey { + + private String id; + private String displayName; + private String owner; + private String raw; + + @XmlJavaTypeAdapter(XmlInstantAdapter.class) + private Instant created; + + RawGpgKey(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RawGpgKey that = (RawGpgKey) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} 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 new file mode 100644 index 0000000000..4608aace5c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/GPGTestHelper.java @@ -0,0 +1,44 @@ +/* + * 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 com.google.common.io.Resources; + +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +final class GPGTestHelper { + + private GPGTestHelper() { + } + + @SuppressWarnings("UnstableApiUsage") + static String readKey(String key) throws IOException { + URL resource = Resources.getResource("sonia/scm/security/gpg/" + key); + return Resources.toString(resource, StandardCharsets.UTF_8); + } + +} 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 new file mode 100644 index 0000000000..765674d0f2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/KeysTest.java @@ -0,0 +1,49 @@ +/* + * 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.PGPException; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static sonia.scm.security.gpg.GPGTestHelper.readKey; + +class KeysTest { + + @Test + void shouldResolveId() throws IOException, PGPException { + String rawPublicKey = readKey("single.asc"); + assertThat(Keys.resolveIdFromKey(rawPublicKey)).isEqualTo("0x975922F193B07D6E"); + } + + @Test + void shouldResolveIdFromMasterKey() throws IOException, PGPException { + String rawPublicKey = readKey("subkeys.asc"); + assertThat(Keys.resolveIdFromKey(rawPublicKey)).isEqualTo("0x13B13D4C8A9350A1"); + } + +} 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 new file mode 100644 index 0000000000..3c26256247 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/gpg/PublicKeyStoreTest.java @@ -0,0 +1,67 @@ +/* + * 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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sonia.scm.store.InMemoryDataStore; + +import java.io.IOException; +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class PublicKeyStoreTest { + + private PublicKeyStore keyStore; + + @BeforeEach + void setUpKeyStore() { + keyStore = new PublicKeyStore(new InMemoryDataStore<>(), () -> "trillian"); + } + + @Test + void shouldReturnStoredKey() throws IOException { + String rawKey = GPGTestHelper.readKey("single.asc"); + Instant now = Instant.now(); + + RawGpgKey key = keyStore.add("SCM Package Key", rawKey); + assertThat(key.getId()).isEqualTo("0x975922F193B07D6E"); + assertThat(key.getDisplayName()).isEqualTo("SCM Package Key"); + assertThat(key.getOwner()).isEqualTo("trillian"); + assertThat(key.getCreated()).isAfterOrEqualTo(now); + assertThat(key.getRaw()).isEqualTo(rawKey); + } + + @Test + void shouldFindStoredKeyById() throws IOException { + String rawKey = GPGTestHelper.readKey("single.asc"); + keyStore.add("SCM Package Key", rawKey); + Optional key = keyStore.findById("0x975922F193B07D6E"); + assertThat(key).isPresent(); + } + +} diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/single.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/single.asc new file mode 100644 index 0000000000..8c06de15d9 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/single.asc @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBF69VQEBEADcnNUucubBBn0nEubrNV3SWh1CbiUyhLU6TEKSUbyB2gDcOrMR +4wAeqK0ar4cloIpS5YEtjT/qVpERzabbJe0NSfCwYblpdfoA0idcvi7Ssnczfr/i +1cF0gqmAjoDqAaSk7xHa/mxiEwAUAXGDWu4pCksT8gHDDx/7lIkHvPZs+VGyMM/O +NVc3QET0JMuKhBYJafSJkdUl3qjX9N6ykETQIbxSv0YLjZuAzQJggLoiMWZtDkrU +KuXh/bJja2xnzj9XtMmpkrIRAX1VYLlI7sJ17b1Tv/yIvaZ1akccRSFtx/kqcM86 +LSO77E82EfLhuVeVCvzxzgfrYHVVX6oFhAEzKUI0TRT453yGZuC5j91HXP3VuXjH +opAODeQLbDfcWPH8joegDZBuK1dQRvVcyh5CMSAWHw9vgXKergrMHVB4EVIqMMir +Q29KfbSuhA5D+xLxPjphbDsMIcLMy0ADd7N0ydmpt2x7ES3Sx3iTssibBYQB731Z +DQCgxy8mdL4MSCNUkDziGyqK+cI9/jRehiOimFsnQIDqQ1hOQBw7M21lvGlbn1IA +iKosi9rb+tUtnNT2d3byjvjZzMj1xoJeOs9i+xEDu1224xEEfJIixfSLLW2T8Qdb +6/a5XGENQB9ZfcW0CrK+V+bHLKXkY2MG9mAL3KEgmDDqydQTwOGNFzJHpwARAQAB +tE9TQ00gUGFja2FnZXMgKHNpZ25pbmcga2V5IGZvciBwYWNrYWdlcy5zY20tbWFu +YWdlci5vcmcpIDxzY20tdGVhbUBjbG91ZG9ndS5jb20+iQJOBBMBCgA4FiEEI9Ji +WyNeJaRxmHWil1ki8ZOwfW4FAl69VQECGy8FCwkIBwMFFQoJCAsFFgIDAQACHgEC +F4AACgkQl1ki8ZOwfW49vg/9EYZSEejdfuzLWcC1M8C9lyausvB+SAI7fEcnD4do +w6WEdnPTus5aAnr1qOncH3aJpjwqfIpuCMdS94i9jgLJTLaQ8S2WegLFVhDQvC7v +Q6ZieOUAYVWJx2Klq2OT1MVJPEzskV3QtFBTaHmuseJrGvH0Waw26MGw8MiAPyES +oZGdcULZBwpr8nazqcFXFuDxMFr1Y28sEzW/ntfScLnIVIVXAWaAXq/4dtB1cIIc +KKsszkM018HdEPSf99ry/nqiwGkOBqMUiEM9+VIMuJRs+BSvT9ETM0yx21fYV2Jj +YG20ahsd1tRFwYLLyzwukT9KUBydZ2RZP+L0gkC+WrfMxvreQxP7d8PH8aF2Ii1e +SpLs91h97tXq+ucp6YyGTEsVnajQeGSA0mX/AhOe3swBNZ08vuhSWkKjKnOXqR4h +IyJaJGAuo6vd+GzdAu/9MxWZQZTWERauofxLTzESwJl7WfTgEFvF+7hNCkQmUA7r +oGc8ahEvuGCZG2MtfBPSPL51FifDlO+G0rifWqHuocZWdBX6fcmt+SYb8SHaG0cu +JP35uWVGuva+Bw73+S21xU3yIjt8bTkZpIuHO9xhivXIOVR3jVhB1V6KrI+jlZl/ +DmQc8MI3+Ez6Hh3kVQjokL1W/u/gg8XumCmc+4hq2QIOSF/ODUMg0b1nne4msLc/ +H1Q= +=5/Nh +-----END PGP PUBLIC KEY BLOCK----- diff --git a/scm-webapp/src/test/resources/sonia/scm/security/gpg/subkeys.asc b/scm-webapp/src/test/resources/sonia/scm/security/gpg/subkeys.asc new file mode 100644 index 0000000000..eaacc24062 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/security/gpg/subkeys.asc @@ -0,0 +1,122 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=dKpI +-----END PGP PUBLIC KEY BLOCK-----