fixes gpg verification

This commit is contained in:
Sebastian Sdorra
2020-07-30 11:58:11 +02:00
parent a1153df50a
commit 274ce561fe
15 changed files with 108 additions and 187 deletions

View File

@@ -24,29 +24,27 @@
package sonia.scm.security.gpg;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPCompressedData;
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.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.security.PublicKey;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
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);
@@ -90,35 +88,59 @@ public class GpgKey implements PublicKey {
public boolean verify(InputStream stream, byte[] signature) {
boolean verified = false;
try {
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(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();
}
verified = verify(stream, asDecodedStream(signature));
} catch (IOException | PGPException e) {
LOG.error("Could not verify GPG key", e);
}
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));
}
}

View File

@@ -49,8 +49,6 @@ 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";

View File

@@ -52,7 +52,7 @@ class DefaultGPGTest {
@Test
void shouldFindIdInSignature() throws IOException {
String raw = GPGTestHelper.readResource("signature.asc");
String raw = GPGTestHelper.readResourceAsString("signature.asc");
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
@@ -60,7 +60,7 @@ class DefaultGPGTest {
@Test
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());
when(store.findById("42")).thenReturn(Optional.of(key1));
@@ -76,8 +76,8 @@ class DefaultGPGTest {
@Test
void shouldFindKeysForUsername() throws IOException {
String raw = GPGTestHelper.readResource("single.asc");
String raw2= GPGTestHelper.readResource("subkeys.asc");
String raw = GPGTestHelper.readResourceAsString("single.asc");
String raw2= GPGTestHelper.readResourceAsString("subkeys.asc");
RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Collections.emptySet(), Instant.now());
RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Collections.emptySet(), Instant.now());

View File

@@ -36,7 +36,13 @@ final class GPGTestHelper {
}
@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);
return Resources.toString(resource, StandardCharsets.UTF_8);
}

View File

@@ -35,19 +35,14 @@ class GpgKeyTest {
@Test
void shouldVerifyPublicKey() throws IOException {
StringBuilder longContent = new StringBuilder();
for (int i = 1; i < 10000; i++) {
longContent.append(i);
}
String rawPublicKey = GPGTestHelper.readResourceAsString("subkeys.asc");
GpgKey publicKey = new GpgKey("1", "trillian", rawPublicKey, Collections.emptySet());
String raw = GPGTestHelper.readResource("pubKeyEH.asc");
String signature = GPGTestHelper.readResource("signature.asc");
byte[] content = GPGTestHelper.readResourceAsBytes("slarti.txt");
byte[] signature = GPGTestHelper.readResourceAsBytes("slarti.txt.asc");
GpgKey key = new GpgKey("1", "trillian", raw, Collections.emptySet());
boolean verified = key.verify(longContent.toString().getBytes(), signature.getBytes());
//assertThat(verified).isTrue();
boolean verified = publicKey.verify(content, signature);
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.readResource;
import static sonia.scm.security.gpg.GPGTestHelper.readResourceAsString;
@ExtendWith(MockitoExtension.class)
class KeysTest {
@Test
void shouldResolveSingleId() throws IOException {
String rawPublicKey = readResource("single.asc");
String rawPublicKey = readResourceAsString("single.asc");
Keys keys = Keys.resolve(rawPublicKey);
assertThat(keys.getMaster()).isEqualTo("0x975922F193B07D6E");
}
@Test
void shouldResolveIdsFromSubkeys() throws IOException {
String rawPublicKey = readResource("subkeys.asc");
String rawPublicKey = readResourceAsString("subkeys.asc");
Keys keys = Keys.resolve(rawPublicKey);
assertThat(keys.getMaster()).isEqualTo("0x13B13D4C8A9350A1");
assertThat(keys.getSubs()).containsOnly("0x247E908C6FD35473", "0xE50E1DD8B90D3A6B", "0xBF49759E43DD0E60");

View File

@@ -36,7 +36,7 @@ class PgpPublicKeyExtractorTest {
@Test
void shouldExtractPublicKeyFromRawKey() throws IOException {
String raw = GPGTestHelper.readResource("pubKeyEH.asc");
String raw = GPGTestHelper.readResourceAsString("pubKeyEH.asc");
Optional<PGPPublicKey> publicKey = PgpPublicKeyExtractor.getFromRawKey(raw);

View File

@@ -104,7 +104,7 @@ class PublicKeyCollectionMapperTest {
}
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());
}

View File

@@ -69,7 +69,7 @@ class PublicKeyMapperTest {
void shouldMapKeyToDto() throws IOException {
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());
RawGpgKeyDto dto = mapper.map(key);
@@ -83,7 +83,7 @@ class PublicKeyMapperTest {
@Test
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());
RawGpgKeyDto dto = mapper.map(key);

View File

@@ -112,7 +112,7 @@ class PublicKeyResourceTest {
@Test
void shouldAddToStore() throws URISyntaxException, IOException {
String raw = GPGTestHelper.readResource("single.asc");
String raw = GPGTestHelper.readResourceAsString("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.readResource("single.asc");
String rawKey = GPGTestHelper.readResourceAsString("single.asc");
assertThrows(AuthorizationException.class, () -> keyStore.add("zaphods key", "zaphod", rawKey));
}
@Test
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));
}
@Test
void shouldReturnStoredKey() throws IOException {
String rawKey = GPGTestHelper.readResource("single.asc");
String rawKey = GPGTestHelper.readResourceAsString("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.readResource("single.asc");
String rawKey = GPGTestHelper.readResourceAsString("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.readResource("single.asc");
String rawKey = GPGTestHelper.readResourceAsString("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.readResource("single.asc");
String singleKey = GPGTestHelper.readResourceAsString("single.asc");
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);
List<RawGpgKey> keys = keyStore.findByUsername("trillian");

View File

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

View File

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

View File

@@ -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.

View File

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