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();
|
Optional<String> getOwner();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns raw of the public key.
|
||||||
|
*
|
||||||
|
* @return raw of key
|
||||||
|
*/
|
||||||
|
String getRaw();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the contacts of the publickey.
|
* Returns the contacts of the publickey.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
"signedBy": "Signiert von",
|
"signedBy": "Signiert von",
|
||||||
"signatureStatus": "Status",
|
"signatureStatus": "Status",
|
||||||
"keyId": "Schlüssel-ID",
|
"keyId": "Schlüssel-ID",
|
||||||
|
"keyContacts": "Kontakte",
|
||||||
"signatureVerified": "Verifiziert",
|
"signatureVerified": "Verifiziert",
|
||||||
"signatureNotVerified": "Nicht verifiziert",
|
"signatureNotVerified": "Nicht verifiziert",
|
||||||
"signatureInvalid": "Ungültig",
|
"signatureInvalid": "Ungültig",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"signedBy": "Signed by",
|
"signedBy": "Signed by",
|
||||||
"keyId": "Key ID",
|
"keyId": "Key ID",
|
||||||
|
"keyContacts": "Contacts",
|
||||||
"signatureStatus": "Status",
|
"signatureStatus": "Status",
|
||||||
"signatureVerified": "verified",
|
"signatureVerified": "verified",
|
||||||
"signatureNotVerified": "not verified",
|
"signatureNotVerified": "not verified",
|
||||||
|
|||||||
@@ -50,9 +50,15 @@ const SignatureIcon: FC<Props> = ({ signatures, className }) => {
|
|||||||
return `${t("changeset.signatureStatus")}: ${status}`;
|
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"
|
"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 = () => {
|
const getColor = () => {
|
||||||
|
|||||||
@@ -23,21 +23,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const formatPublicKey = (key: string) => {
|
export const formatPublicKey = (key: string) => {
|
||||||
const parts = key.split(/\s+/);
|
const parts = key.split(/\n/);
|
||||||
if (parts.length === 3) {
|
return parts[2].substring(0, 15);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class DefaultGPG implements GPG {
|
|||||||
public Optional<PublicKey> findPublicKey(String id) {
|
public Optional<PublicKey> findPublicKey(String id) {
|
||||||
Optional<RawGpgKey> key = store.findById(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
|
@Override
|
||||||
@@ -79,7 +79,7 @@ public class DefaultGPG implements GPG {
|
|||||||
if (!keys.isEmpty()) {
|
if (!keys.isEmpty()) {
|
||||||
return keys
|
return keys
|
||||||
.stream()
|
.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());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,9 @@ import org.bouncycastle.bcpg.ArmoredInputStream;
|
|||||||
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.operator.PGPContentVerifierBuilderProvider;
|
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
|
||||||
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
|
||||||
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;
|
||||||
@@ -43,26 +42,25 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
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);
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
private final String owner;
|
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.id = id;
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
try {
|
this.raw = raw;
|
||||||
getPgpPublicKey(raw).getUserIDs().forEachRemaining(contacts::add);
|
this.contacts = contacts;
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.error("Could not find contacts in public key", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -78,6 +76,11 @@ public class GpgKey implements PublicKey {
|
|||||||
return Optional.of(owner);
|
return Optional.of(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRaw() {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getContacts() {
|
public Set<String> getContacts() {
|
||||||
return contacts;
|
return contacts;
|
||||||
@@ -87,32 +90,34 @@ 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 {
|
||||||
PGPPublicKey publicKey = getPgpPublicKey(signature);
|
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
|
||||||
PGPSignature pgpSignature = ((PGPSignature) publicKey.getSignatures().next());
|
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, null);
|
||||||
|
PGPSignature pgpSignature = ((PGPSignatureList) pgpObjectFactory.nextObject()).get(0);
|
||||||
|
|
||||||
PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
|
PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider();
|
||||||
pgpSignature.init(provider, publicKey);
|
|
||||||
|
|
||||||
char[] buffer = new char[1024];
|
Optional<PGPPublicKey> pgpPublicKey = getFromRawKey(raw);
|
||||||
int bytesRead = 0;
|
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(stream));
|
|
||||||
|
|
||||||
while (bytesRead != -1) {
|
if (pgpPublicKey.isPresent()) {
|
||||||
bytesRead = in.read(buffer, 0, 1024);
|
pgpSignature.init(provider, pgpPublicKey.get());
|
||||||
pgpSignature.update(new String(buffer).getBytes(StandardCharsets.UTF_8));
|
|
||||||
|
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) {
|
} catch (IOException | PGPException e) {
|
||||||
LOG.error("Could not verify GPG key", e);
|
LOG.error("Could not verify GPG key", e);
|
||||||
}
|
}
|
||||||
return verified;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PGPPublicKey getPgpPublicKey(byte[] signature) throws IOException {
|
return verified;
|
||||||
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature));
|
|
||||||
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, new JcaKeyFingerprintCalculator());
|
|
||||||
return ((PGPPublicKeyRing) pgpObjectFactory.nextObject()).getPublicKey();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
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.ContextEntry;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.security.NotPublicKeyException;
|
import sonia.scm.security.NotPublicKeyException;
|
||||||
@@ -35,13 +38,19 @@ import sonia.scm.user.UserPermissions;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -70,7 +79,7 @@ public class PublicKeyStore {
|
|||||||
subKeyStore.put(subKey, new MasterKeyReference(master));
|
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);
|
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) {
|
public void delete(String id) {
|
||||||
RawGpgKey rawGpgKey = store.get(id);
|
RawGpgKey rawGpgKey = store.get(id);
|
||||||
if (rawGpgKey != null) {
|
if (rawGpgKey != null) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import javax.xml.bind.annotation.XmlRootElement;
|
|||||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -48,6 +49,7 @@ public class RawGpgKey {
|
|||||||
private String displayName;
|
private String displayName;
|
||||||
private String owner;
|
private String owner;
|
||||||
private String raw;
|
private String raw;
|
||||||
|
private Set<String> contacts;
|
||||||
|
|
||||||
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
||||||
private Instant created;
|
private Instant created;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
package sonia.scm.security.gpg;
|
package sonia.scm.security.gpg;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
@@ -34,8 +35,8 @@ import sonia.scm.security.PublicKey;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -51,7 +52,7 @@ class DefaultGPGTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindIdInSignature() throws IOException {
|
void shouldFindIdInSignature() throws IOException {
|
||||||
String raw = GPGTestHelper.readKey("signature.asc");
|
String raw = GPGTestHelper.readResource("signature.asc");
|
||||||
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
|
String publicKeyId = gpg.findPublicKeyId(raw.getBytes());
|
||||||
|
|
||||||
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
|
assertThat(publicKeyId).isEqualTo("0x1F17B79A09DAD5B9");
|
||||||
@@ -59,8 +60,8 @@ class DefaultGPGTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindPublicKey() throws IOException {
|
void shouldFindPublicKey() throws IOException {
|
||||||
String raw = GPGTestHelper.readKey("subkeys.asc");
|
String raw = GPGTestHelper.readResource("subkeys.asc");
|
||||||
RawGpgKey key1 = new RawGpgKey("42", "key_42", "trillian", raw, 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));
|
||||||
|
|
||||||
@@ -70,17 +71,16 @@ class DefaultGPGTest {
|
|||||||
assertThat(publicKey.get().getOwner()).isPresent();
|
assertThat(publicKey.get().getOwner()).isPresent();
|
||||||
assertThat(publicKey.get().getOwner().get()).contains("trillian");
|
assertThat(publicKey.get().getOwner().get()).contains("trillian");
|
||||||
assertThat(publicKey.get().getId()).isEqualTo("42");
|
assertThat(publicKey.get().getId()).isEqualTo("42");
|
||||||
assertThat(publicKey.get().getContacts()).contains("Sebastian Sdorra <s.sdorra@gmail.com>",
|
assertThat(publicKey.get().getContacts()).contains("trillian", "zaphod");
|
||||||
"Sebastian Sdorra <sebastian.sdorra@cloudogu.com>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindKeysForUsername() throws IOException {
|
void shouldFindKeysForUsername() throws IOException {
|
||||||
String raw = GPGTestHelper.readKey("single.asc");
|
String raw = GPGTestHelper.readResource("single.asc");
|
||||||
String raw2= GPGTestHelper.readKey("subkeys.asc");
|
String raw2= GPGTestHelper.readResource("subkeys.asc");
|
||||||
|
|
||||||
RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Instant.now());
|
RawGpgKey key1 = new RawGpgKey("1", "1", "trillian", raw, Collections.emptySet(), Instant.now());
|
||||||
RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Instant.now());
|
RawGpgKey key2 = new RawGpgKey("2", "2", "trillian", raw2, Collections.emptySet(), Instant.now());
|
||||||
when(store.findByUsername("trillian")).thenReturn(ImmutableList.of(key1, key2));
|
when(store.findByUsername("trillian")).thenReturn(ImmutableList.of(key1, key2));
|
||||||
|
|
||||||
Iterable<PublicKey> keys = gpg.findPublicKeysByUsername("trillian");
|
Iterable<PublicKey> keys = gpg.findPublicKeysByUsername("trillian");
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ final class GPGTestHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("UnstableApiUsage")
|
@SuppressWarnings("UnstableApiUsage")
|
||||||
static String readKey(String key) throws IOException {
|
static String readResource(String fileName) throws IOException {
|
||||||
URL resource = Resources.getResource("sonia/scm/security/gpg/" + key);
|
URL resource = Resources.getResource("sonia/scm/security/gpg/" + fileName);
|
||||||
return Resources.toString(resource, StandardCharsets.UTF_8);
|
return Resources.toString(resource, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ package sonia.scm.security.gpg;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class GpgKeyTest {
|
class GpgKeyTest {
|
||||||
|
|
||||||
@@ -37,13 +40,14 @@ class GpgKeyTest {
|
|||||||
longContent.append(i);
|
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.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.readKey;
|
import static sonia.scm.security.gpg.GPGTestHelper.readResource;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class KeysTest {
|
class KeysTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResolveSingleId() throws IOException {
|
void shouldResolveSingleId() throws IOException {
|
||||||
String rawPublicKey = readKey("single.asc");
|
String rawPublicKey = readResource("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 = readKey("subkeys.asc");
|
String rawPublicKey = readResource("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");
|
||||||
|
|||||||
@@ -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.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -103,8 +104,8 @@ class PublicKeyCollectionMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private RawGpgKey createPublicKey(String displayName) throws IOException {
|
private RawGpgKey createPublicKey(String displayName) throws IOException {
|
||||||
String raw = GPGTestHelper.readKey("single.asc");
|
String raw = GPGTestHelper.readResource("single.asc");
|
||||||
return new RawGpgKey(displayName, displayName, "trillian", raw, Instant.now());
|
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.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -68,8 +69,8 @@ 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.readKey("single.asc");
|
String raw = GPGTestHelper.readResource("single.asc");
|
||||||
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, 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);
|
||||||
|
|
||||||
@@ -82,8 +83,8 @@ class PublicKeyMapperTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotAppendDeleteLink() throws IOException {
|
void shouldNotAppendDeleteLink() throws IOException {
|
||||||
String raw = GPGTestHelper.readKey("single.asc");
|
String raw = GPGTestHelper.readResource("single.asc");
|
||||||
RawGpgKey key = new RawGpgKey("1", "key_42", "trillian", raw, 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.readKey("single.asc");
|
String raw = GPGTestHelper.readResource("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.readKey("single.asc");
|
String rawKey = GPGTestHelper.readResource("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.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));
|
assertThrows(NotPublicKeyException.class, () -> keyStore.add("SCM Package Key", "trillian", rawKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnStoredKey() throws IOException {
|
void shouldReturnStoredKey() throws IOException {
|
||||||
String rawKey = GPGTestHelper.readKey("single.asc");
|
String rawKey = GPGTestHelper.readResource("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.readKey("single.asc");
|
String rawKey = GPGTestHelper.readResource("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.readKey("single.asc");
|
String rawKey = GPGTestHelper.readResource("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.readKey("single.asc");
|
String singleKey = GPGTestHelper.readResource("single.asc");
|
||||||
keyStore.add("SCM Single Key", "trillian", singleKey);
|
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);
|
keyStore.add("SCM Multi Key", "trillian", multiKey);
|
||||||
|
|
||||||
List<RawGpgKey> keys = keyStore.findByUsername("trillian");
|
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