mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 16:05:44 +01:00
refactor
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
"signedBy": "Signiert von",
|
||||
"signatureStatus": "Status",
|
||||
"keyId": "Schlüssel-ID",
|
||||
"keyContacts": "Kontakte",
|
||||
"signatureVerified": "Verifiziert",
|
||||
"signatureNotVerified": "Nicht verifiziert",
|
||||
"signatureInvalid": "Ungültig",
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"tags": "Tags",
|
||||
"signedBy": "Signed by",
|
||||
"keyId": "Key ID",
|
||||
"keyContacts": "Contacts",
|
||||
"signatureStatus": "Status",
|
||||
"signatureVerified": "verified",
|
||||
"signatureNotVerified": "not verified",
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=6sgk
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
Reference in New Issue
Block a user