This commit is contained in:
Eduard Heimbuch
2020-07-29 17:16:57 +02:00
parent b22ead23de
commit 3da7710543
20 changed files with 323 additions and 81 deletions

View File

@@ -50,6 +50,13 @@ public interface PublicKey {
*/
Optional<String> getOwner();
/**
* Returns raw of the public key.
*
* @return raw of key
*/
String getRaw();
/**
* Returns the contacts of the publickey.
*

View File

@@ -91,6 +91,7 @@
"signedBy": "Signiert von",
"signatureStatus": "Status",
"keyId": "Schlüssel-ID",
"keyContacts": "Kontakte",
"signatureVerified": "Verifiziert",
"signatureNotVerified": "Nicht verifiziert",
"signatureInvalid": "Ungültig",

View File

@@ -89,6 +89,7 @@
"tags": "Tags",
"signedBy": "Signed by",
"keyId": "Key ID",
"keyContacts": "Contacts",
"signatureStatus": "Status",
"signatureVerified": "verified",
"signatureNotVerified": "not verified",

View File

@@ -50,9 +50,15 @@ const SignatureIcon: FC<Props> = ({ 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 = () => {

View File

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

View File

@@ -69,7 +69,7 @@ public class DefaultGPG implements GPG {
public Optional<PublicKey> findPublicKey(String id) {
Optional<RawGpgKey> 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());
}

View File

@@ -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<String> contacts = new LinkedHashSet<>();
private final String raw;
private final Set<String> contacts;
public GpgKey(String id, String owner, byte[] raw) {
public GpgKey(String id, String owner, String raw, Set<String> 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<String> getContacts() {
return contacts;
@@ -87,11 +90,16 @@ 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);
Optional<PGPPublicKey> pgpPublicKey = getFromRawKey(raw);
if (pgpPublicKey.isPresent()) {
pgpSignature.init(provider, pgpPublicKey.get());
char[] buffer = new char[1024];
int bytesRead = 0;
@@ -103,16 +111,13 @@ public class GpgKey implements PublicKey {
}
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;
}
}

View File

@@ -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<PGPPublicKey> 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();
}
}

View File

@@ -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<String> getContactsFromPublicKey(String rawKey) {
Set<String> contacts = new HashSet<>();
Optional<PGPPublicKey> 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) {

View File

@@ -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<String> contacts;
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
private Instant created;

View File

@@ -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 <s.sdorra@gmail.com>",
"Sebastian Sdorra <sebastian.sdorra@cloudogu.com>");
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<PublicKey> keys = gpg.findPublicKeysByUsername("trillian");

View File

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

View File

@@ -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();
}
}

View File

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

View File

@@ -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<PGPPublicKey> publicKey = PgpPublicKeyExtractor.getFromRawKey(raw);
assertThat(publicKey).isPresent();
assertThat(Long.toHexString(publicKey.get().getKeyID())).isEqualTo("39ad4bed55527f1c");
}
}

View File

@@ -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());
}
}

View File

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

View File

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

View File

@@ -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<RawGpgKey> 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<RawGpgKey> 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<RawGpgKey> keys = keyStore.findByUsername("trillian");

View File

@@ -0,0 +1,109 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=6sgk
-----END PGP PUBLIC KEY BLOCK-----