mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
fixes gpg verification
This commit is contained in:
@@ -24,29 +24,27 @@
|
|||||||
|
|
||||||
package sonia.scm.security.gpg;
|
package sonia.scm.security.gpg;
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
import org.bouncycastle.openpgp.PGPCompressedData;
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
import org.bouncycastle.openpgp.PGPObjectFactory;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
import org.bouncycastle.openpgp.PGPSignature;
|
||||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
import org.bouncycastle.openpgp.PGPSignatureList;
|
||||||
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
|
import org.bouncycastle.openpgp.PGPUtil;
|
||||||
|
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
|
||||||
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.security.PublicKey;
|
import sonia.scm.security.PublicKey;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
|
|
||||||
|
|
||||||
public class GpgKey implements PublicKey {
|
public class GpgKey implements PublicKey {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class);
|
private static final Logger LOG = LoggerFactory.getLogger(GpgKey.class);
|
||||||
@@ -90,35 +88,59 @@ public class GpgKey implements PublicKey {
|
|||||||
public boolean verify(InputStream stream, byte[] signature) {
|
public boolean verify(InputStream stream, byte[] signature) {
|
||||||
boolean verified = false;
|
boolean verified = false;
|
||||||
try {
|
try {
|
||||||
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
|
verified = verify(stream, asDecodedStream(signature));
|
||||||
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, null);
|
|
||||||
PGPSignature pgpSignature = ((PGPSignatureList) pgpObjectFactory.nextObject()).get(0);
|
|
||||||
|
|
||||||
PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
|
|
||||||
|
|
||||||
Optional<PGPPublicKey> pgpPublicKey = getFromRawKey(raw);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException | PGPException e) {
|
} catch (IOException | PGPException e) {
|
||||||
LOG.error("Could not verify GPG key", e);
|
LOG.error("Could not verify GPG key", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return verified;
|
return verified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean verify(InputStream stream, InputStream signature) throws IOException, PGPException {
|
||||||
|
PGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(signature);
|
||||||
|
Object o = pgpObjectFactory.nextObject();
|
||||||
|
if (o instanceof PGPSignatureList) {
|
||||||
|
return verify(stream, ((PGPSignatureList) o).get(0));
|
||||||
|
} else if (o instanceof PGPCompressedData) {
|
||||||
|
return verify(stream, ((PGPCompressedData) o).getDataStream());
|
||||||
|
} else {
|
||||||
|
LOG.warn("could not find valid signature, only found {}", o);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean verify(InputStream stream, PGPSignature signature) throws IOException, PGPException {
|
||||||
|
PGPPublicKey publicKey = findKey(signature);
|
||||||
|
if (publicKey != null) {
|
||||||
|
JcaPGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
|
||||||
|
signature.init(provider, publicKey);
|
||||||
|
|
||||||
|
int bytesRead;
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
while ((bytesRead = stream.read(buffer, 0, buffer.length)) != -1) {
|
||||||
|
signature.update(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature.verify();
|
||||||
|
} else {
|
||||||
|
LOG.warn("failed to parse public gpg key");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PGPPublicKey findKey(PGPSignature signature) throws IOException {
|
||||||
|
PGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(asDecodedStream(raw));
|
||||||
|
PGPPublicKeyRing keyRing = (PGPPublicKeyRing) pgpObjectFactory.nextObject();
|
||||||
|
return keyRing.getPublicKey(signature.getKeyID());
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream asDecodedStream(String content) throws IOException {
|
||||||
|
return asDecodedStream(content.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputStream asDecodedStream(byte[] bytes) throws IOException {
|
||||||
|
return PGPUtil.getDecoderStream(new ByteArrayInputStream(bytes));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ import static sonia.scm.security.gpg.PgpPublicKeyExtractor.getFromRawKey;
|
|||||||
@Singleton
|
@Singleton
|
||||||
public class PublicKeyStore {
|
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 STORE_NAME = "gpg_public_keys";
|
||||||
private static final String SUBKEY_STORE_NAME = "gpg_public_sub_keys";
|
private static final String SUBKEY_STORE_NAME = "gpg_public_sub_keys";
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class DefaultGPGTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindIdInSignature() throws IOException {
|
void shouldFindIdInSignature() throws IOException {
|
||||||
String raw = GPGTestHelper.readResource("signature.asc");
|
String raw = GPGTestHelper.readResourceAsString("signature.asc");
|
||||||
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
|
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
|
||||||
|
|
||||||
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
|
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
|
||||||
@@ -60,7 +60,7 @@ class DefaultGPGTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindPublicKey() throws IOException {
|
void shouldFindPublicKey() throws IOException {
|
||||||
String raw = GPGTestHelper.readResource("subkeys.asc");
|
String raw = GPGTestHelper.readResourceAsString("subkeys.asc");
|
||||||
RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, ImmutableSet.of("trillian", "zaphod"), Instant.now());
|
RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, ImmutableSet.of("trillian", "zaphod"), Instant.now());
|
||||||
|
|
||||||
when(store.findById("42")).thenReturn(Optional.of(key1));
|
when(store.findById("42")).thenReturn(Optional.of(key1));
|
||||||
@@ -76,8 +76,8 @@ class DefaultGPGTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindKeysForUsername() throws IOException {
|
void shouldFindKeysForUsername() throws IOException {
|
||||||
String raw = GPGTestHelper.readResource("single.asc");
|
String raw = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
String raw2= GPGTestHelper.readResource("subkeys.asc");
|
String raw2= GPGTestHelper.readResourceAsString("subkeys.asc");
|
||||||
|
|
||||||
RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Collections.emptySet(), 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());
|
RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Collections.emptySet(), Instant.now());
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ final class GPGTestHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("UnstableApiUsage")
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
static String readResource(String fileName) throws IOException {
|
static byte[] readResourceAsBytes(String fileName) throws IOException {
|
||||||
|
URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
|
||||||
|
return Resources.toByteArray(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
|
static String readResourceAsString(String fileName) throws IOException {
|
||||||
URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
|
URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
|
||||||
return Resources.toString(resource, StandardCharsets.UTF_8);
|
return Resources.toString(resource, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,19 +35,14 @@ class GpgKeyTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldVerifyPublicKey() throws IOException {
|
void shouldVerifyPublicKey() throws IOException {
|
||||||
StringBuilder longContent = new StringBuilder();
|
String rawPublicKey = GPGTestHelper.readResourceAsString("subkeys.asc");
|
||||||
for (int i = 1; i < 10000; i++) {
|
GpgKey publicKey = new GpgKey("1", "trillian", rawPublicKey, Collections.emptySet());
|
||||||
longContent.append(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
String raw = GPGTestHelper.readResource("pubKeyEH.asc");
|
byte[] content = GPGTestHelper.readResourceAsBytes("slarti.txt");
|
||||||
String signature = GPGTestHelper.readResource("signature.asc");
|
byte[] signature = GPGTestHelper.readResourceAsBytes("slarti.txt.asc");
|
||||||
|
|
||||||
GpgKey key = new GpgKey("1", "trillian", raw, Collections.emptySet());
|
boolean verified = publicKey.verify(content, signature);
|
||||||
|
assertThat(verified).isTrue();
|
||||||
boolean verified = key.verify(longContent.toString().getBytes(), signature.getBytes());
|
|
||||||
|
|
||||||
//assertThat(verified).isTrue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,21 +38,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static sonia.scm.security.gpg.GPGTestHelper.readResource;
|
import static sonia.scm.security.gpg.GPGTestHelper.readResourceAsString;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class KeysTest {
|
class KeysTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveSingleId() throws IOException {
|
void shouldResolveSingleId() throws IOException {
|
||||||
String rawPublicKey = readResource("single.asc");
|
String rawPublicKey = readResourceAsString("single.asc");
|
||||||
Keys keys = Keys.resolve(rawPublicKey);
|
Keys keys = Keys.resolve(rawPublicKey);
|
||||||
assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E");
|
assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveIdsFromSubkeys() throws IOException {
|
void shouldResolveIdsFromSubkeys() throws IOException {
|
||||||
String rawPublicKey = readResource("subkeys.asc");
|
String rawPublicKey = readResourceAsString("subkeys.asc");
|
||||||
Keys keys = Keys.resolve(rawPublicKey);
|
Keys keys = Keys.resolve(rawPublicKey);
|
||||||
assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1");
|
assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1");
|
||||||
assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60");
|
assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60");
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class PgpPublicKeyExtractorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExtractPublicKeyFromRawKey() throws IOException {
|
void shouldExtractPublicKeyFromRawKey() throws IOException {
|
||||||
String raw = GPGTestHelper.readResource("pubKeyEH.asc");
|
String raw = GPGTestHelper.readResourceAsString("pubKeyEH.asc");
|
||||||
|
|
||||||
Optional<PGPPublicKey> publicKey = PgpPublicKeyExtractor.getFromRawKey(raw);
|
Optional<PGPPublicKey> publicKey = PgpPublicKeyExtractor.getFromRawKey(raw);
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class PublicKeyCollectionMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private RawGpgKey createPublicKey(String displayName) throws IOException {
|
private RawGpgKey createPublicKey(String displayName) throws IOException {
|
||||||
String raw = GPGTestHelper.readResource("single.asc");
|
String raw = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
return new RawGpgKey(displayName, displayName, "trillian", raw, Collections.emptySet(), Instant.now());
|
return new RawGpgKey(displayName, displayName, "trillian", raw, Collections.emptySet(), Instant.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class PublicKeyMapperTest {
|
|||||||
void shouldMapKeyToDto() throws IOException {
|
void shouldMapKeyToDto() throws IOException {
|
||||||
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
|
when(subject.isPermitted("user:changePublicKeys:trillian")).thenReturn(true);
|
||||||
|
|
||||||
String raw = GPGTestHelper.readResource("single.asc");
|
String raw = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
|
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
|
||||||
|
|
||||||
RawGpgKeyDto dto = mapper.map(key);
|
RawGpgKeyDto dto = mapper.map(key);
|
||||||
@@ -83,7 +83,7 @@ class PublicKeyMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotAppendDeleteLink() throws IOException {
|
void shouldNotAppendDeleteLink() throws IOException {
|
||||||
String raw = GPGTestHelper.readResource("single.asc");
|
String raw = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
|
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, Collections.emptySet(), Instant.now());
|
||||||
|
|
||||||
RawGpgKeyDto dto = mapper.map(key);
|
RawGpgKeyDto dto = mapper.map(key);
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class PublicKeyResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAddToStore() throws URISyntaxException, IOException {
|
void shouldAddToStore() throws URISyntaxException, IOException {
|
||||||
String raw = GPGTestHelper.readResource("single.asc");
|
String raw = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
|
|
||||||
UriInfo uriInfo = mock(UriInfo.class);
|
UriInfo uriInfo = mock(UriInfo.class);
|
||||||
UriBuilder builder = mock(UriBuilder.class);
|
UriBuilder builder = mock(UriBuilder.class);
|
||||||
|
|||||||
@@ -80,21 +80,21 @@ class PublicKeyStoreTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
|
void shouldThrowAuthorizationExceptionOnAdd() throws IOException {
|
||||||
doThrow(AuthorizationException.class).when(subject).checkPermission("user:changePublicKeys:zaphod");
|
doThrow(AuthorizationException.class).when(subject).checkPermission("user:changePublicKeys:zaphod");
|
||||||
String rawKey = GPGTestHelper.readResource("single.asc");
|
String rawKey = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
|
|
||||||
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
|
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldOnlyStorePublicKeys() throws IOException {
|
void shouldOnlyStorePublicKeys() throws IOException {
|
||||||
String rawKey = GPGTestHelper.readResource("single.asc").replace("PUBLIC", "PRIVATE");
|
String rawKey = GPGTestHelper.readResourceAsString("single.asc").replace("PUBLIC", "PRIVATE");
|
||||||
|
|
||||||
assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
|
assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnStoredKey() throws IOException {
|
void shouldReturnStoredKey() throws IOException {
|
||||||
String rawKey = GPGTestHelper.readResource("single.asc");
|
String rawKey = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
|
|
||||||
RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey);
|
RawGpgKey key = keyStore.add("SCM Package Key", "trillian", rawKey);
|
||||||
@@ -107,7 +107,7 @@ class PublicKeyStoreTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindStoredKeyById() throws IOException {
|
void shouldFindStoredKeyById() throws IOException {
|
||||||
String rawKey = GPGTestHelper.readResource("single.asc");
|
String rawKey = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
keyStore.add("SCM Package Key", "trillian", rawKey);
|
keyStore.add("SCM Package Key", "trillian", rawKey);
|
||||||
Optional<RawGpgKey> key = keyStore.findById("0x975922F193B07D6E");
|
Optional<RawGpgKey> key = keyStore.findById("0x975922F193B07D6E");
|
||||||
assertThat(key).isPresent();
|
assertThat(key).isPresent();
|
||||||
@@ -115,7 +115,7 @@ class PublicKeyStoreTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteKey() throws IOException {
|
void shouldDeleteKey() throws IOException {
|
||||||
String rawKey = GPGTestHelper.readResource("single.asc");
|
String rawKey = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
keyStore.add("SCM Package Key", "trillian", rawKey);
|
keyStore.add("SCM Package Key", "trillian", rawKey);
|
||||||
Optional<RawGpgKey> key = keyStore.findById("0x975922F193B07D6E");
|
Optional<RawGpgKey> key = keyStore.findById("0x975922F193B07D6E");
|
||||||
|
|
||||||
@@ -139,10 +139,10 @@ class PublicKeyStoreTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindAllKeysForUser() throws IOException {
|
void shouldFindAllKeysForUser() throws IOException {
|
||||||
String singleKey = GPGTestHelper.readResource("single.asc");
|
String singleKey = GPGTestHelper.readResourceAsString("single.asc");
|
||||||
keyStore.add("SCM Single Key", "trillian", singleKey);
|
keyStore.add("SCM Single Key", "trillian", singleKey);
|
||||||
|
|
||||||
String multiKey = GPGTestHelper.readResource("subkeys.asc");
|
String multiKey = GPGTestHelper.readResourceAsString("subkeys.asc");
|
||||||
keyStore.add("SCM Multi Key", "trillian", multiKey);
|
keyStore.add("SCM Multi Key", "trillian", multiKey);
|
||||||
|
|
||||||
List<RawGpgKey> keys = keyStore.findByUsername("trillian");
|
List<RawGpgKey> keys = keyStore.findByUsername("trillian");
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
=6sgk
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQIzBAABCgAdFiEEAibrQDifYryAFPjQHxe3mgna1bkFAl8gSFMACgkQHxe3mgna
|
|
||||||
1bk0Hg/9HaN7aRkRo1FH5xPnswGFAidHG+XQBFYTK3EhR2g6m4iRCij58qIEMQVh
|
|
||||||
gV6FTtz6xGB/Oq32e5+gp9dYTp6lTrfm35hjg7uzwiC7OQx3QswZn7GUX/ZmuLk0
|
|
||||||
nqz4ryiVxeMWst47JkKAm9PY6GC+UITaL3tptNF//MBPAwEfNnDP7O667BTvnROh
|
|
||||||
9XaSlIdYlbGxs5YzQK8BMB56YdutTDMtHl92oOjDA7r238dScmlNUSw3IQIsNPkz
|
|
||||||
4WZwGM7HVxw7PjbRoMbwXbNEJ87F4SuBqhWM7BjHEUFS21wH8APtVZrNzd2znveq
|
|
||||||
oemn/+9pn0LG3Mg/2FASqHA6X+HS79YT9fb0O3HUvHzlaJmev88h4JSGcTJZG5+o
|
|
||||||
a9LEPW56clJYmCq/ghKAyV+bJfUkIAP9i75p4zi8Il4ACJnf9oVRg6RuOTXK5cnf
|
|
||||||
bvSzEtBWlXT1ELV52uo9gwcQMqgkQ89p8pTHcYHD3UhfdsuCfzlTGP/aQ/6OUBGg
|
|
||||||
k6AFS1kAwalAp2VEOBIJUXylM60VfdLaLfWpgg4T8mq4WIV3ROXTV0o2XciuF4r0
|
|
||||||
2oXtyc84J7nnGJlJ4HqHBqzMNHF4giqVNhKQzBAQ1OEjePBfkW6RMDcYxkLilUrU
|
|
||||||
LzguRD1ybuS9xYQK60s2S68sNoAFkY8NTn9z8je1BO7ajzQn/fw=
|
|
||||||
=smHD
|
|
||||||
-----END PGP SIGNATURE-----
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Slartibartfast is a Magrathean, and a designer of planets.[2] His favourite part of the job is creating coastlines, the most notable of which are the fjords found on the coast of Norway on planet Earth,[3] for which he won an award. While trapped on prehistoric Earth, Arthur Dent and Ford Prefect see Slartibartfast's signature deep inside a glacier in ancient Norway.
|
||||||
|
|
||||||
|
When Earth Mk. II is being made, Slartibartfast is assigned to the continent of Africa. He is unhappy about this because he has begun "doing it with fjords again" (arguing that they give a continent a lovely baroque feel), but has been told by his superiors that they are "not equatorial enough". In relation to this, he expresses the view that he would "far rather be happy than right any day."
|
||||||
|
|
||||||
|
In any event, the new Earth is not required and, much to Slartibartfast's disgust, its owners suggested that he take a quick skiing holiday on his glaciers before dismantling them.
|
||||||
|
|
||||||
|
Slartibartfast's aircar is later found near the place where Zaphod Beeblebrox, Ford Prefect, Trillian and Arthur Dent are attacked by cops, who are suddenly killed in a way similar to how the cleaning staff in Slartibartfast's study have perished. There is a note pointing to one of the controls in the aircar saying "This is the probably the best button to press."
|
||||||
|
|
||||||
|
In Life, the Universe and Everything Slartibartfast has joined the Campaign for Real Time (or "CamTim" as the volunteers casually refer to it, a reference to CAMRA) which tries to preserve events as they happened before time travelling was invented. He picks up Arthur and Ford from Lord's Cricket Ground with his Starship Bistromath, after which they head out to stop the robots of Krikkit from bringing together the pieces of the Wikkit Gate.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQIzBAABCgAdFiEE5uxwNFYM4xFJsy3eJH6QjG/TVHMFAl8ii3QACgkQJH6QjG/T
|
||||||
|
VHPIWQ//fz+n5HLeIDWMeMhvkNes8dwGzdfHme/Yyb1vocqGj3VK+xr3YVjum09h
|
||||||
|
NjKJvumazdALTUXnXNW9T57LVD3kAJpAnwCHFtIQvPmg0EVn1oz7WDh+YVVA2Ko4
|
||||||
|
fGgH0dB64N2FUEmCYU8aV8wKUOgQ8Fh5FcSggzC5UegU9yZou+B38AfI55od1Ay/
|
||||||
|
jk5tEExEwsErjjhDZFho/D/Ybp43otj4WtVy+fPHaZYW7TzKRVBi7ngqAlyCFGwO
|
||||||
|
W/xEy11nv1apXV+l3iGxJkU2jlCi7ORbxH2ooSyhrC33rWxAtdYxgMElF7lRbnoc
|
||||||
|
Pg8EQXZ8zmEwgm9u6+Ng0/qsu/wajV+QKSDMRJMhmFN0zpdvyscvaFcowcu6jW25
|
||||||
|
Smz/Gs5B2oASDh/L/sLxUdSfCHVM7gk6HYHWNZgSajtpgLeJy8/wxOSYmB2TD72A
|
||||||
|
ktZN2v5adkaHM8rEXLPdD0BtCMGs82pxgHEK42ncW6RFFdiOkgb6KPhkmhlxl0XU
|
||||||
|
r64mfHj3n/dNBR5LoSbDFtHD2LakN8CPcubURneA/psfUiUdfktl6KcDYsuS1fJk
|
||||||
|
+XdxAdVUIqf3MwQU3od1nklu5Sybv5+Q2MZOstGn7opGuQXndKFtnC4WOMfo0w+X
|
||||||
|
HTZilw/HDYN0wgzLl5YpHWmZ5MQl5/aN1nn5js3vOhgEF3+qhvQ=
|
||||||
|
=3ZJK
|
||||||
|
-----END PGP SIGNATURE-----
|
||||||
Reference in New Issue
Block a user