Port wot-dijkstra Java classes to Kotlin

This commit is contained in:
Paul Schaub 2023-06-28 19:28:05 +02:00
parent 987492056d
commit 2922bc5bd6
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
21 changed files with 395 additions and 824 deletions

View File

@ -42,7 +42,7 @@ allprojects {
}
// For library modules, enable android api compatibility check
if (it.name != 'pgpainless-cli') {
if (it.name != 'pgpainless-cli' && it.name != 'wot-dijkstra') {
// animalsniffer
apply plugin: 'ru.vyarus.animalsniffer'
dependencies {

View File

@ -14,7 +14,6 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
import org.pgpainless.wot.dijkstra.sq.CertSynopsis;
import org.pgpainless.wot.dijkstra.sq.Certification;
import org.pgpainless.wot.dijkstra.sq.Depth;
import org.pgpainless.wot.dijkstra.sq.Optional;
import org.pgpainless.wot.dijkstra.sq.RegexSet;
/**
@ -34,7 +33,7 @@ public class CertificationFactory {
public static Certification fromDelegation(CertSynopsis issuer,
CertSynopsis target,
PGPSignature signature) {
return fromSignature(issuer, Optional.empty(), target, signature);
return fromSignature(issuer, null, target, signature);
}
/**
@ -50,7 +49,7 @@ public class CertificationFactory {
String targetUserId,
CertSynopsis target,
PGPSignature signature) {
return fromSignature(issuer, Optional.just(targetUserId), target, signature);
return fromSignature(issuer, targetUserId, target, signature);
}
/**
@ -63,7 +62,7 @@ public class CertificationFactory {
* @return certification
*/
public static Certification fromSignature(CertSynopsis issuer,
Optional<String> targetUserId,
String targetUserId,
CertSynopsis target,
PGPSignature signature) {
return new Certification(
@ -71,7 +70,7 @@ public class CertificationFactory {
target,
targetUserId,
SignatureSubpacketsUtil.getSignatureCreationTime(signature).getTime(),
Optional.maybe(SignatureSubpacketsUtil.getSignatureExpirationTimeAsDate(signature)),
SignatureSubpacketsUtil.getSignatureExpirationTimeAsDate(signature),
SignatureSubpacketsUtil.isExportable(signature),
getTrustAmountFrom(signature),
getTrustDepthFrom(signature),

View File

@ -33,7 +33,6 @@ import org.pgpainless.wot.dijkstra.sq.CertSynopsis;
import org.pgpainless.wot.dijkstra.sq.Certification;
import org.pgpainless.wot.dijkstra.sq.CertificationSet;
import org.pgpainless.wot.dijkstra.sq.Network;
import org.pgpainless.wot.dijkstra.sq.Optional;
import org.pgpainless.wot.dijkstra.sq.ReferenceTime;
import org.pgpainless.wot.sugar.IterableIterator;
import org.pgpainless.wot.sugar.PrefixedIterator;
@ -80,7 +79,7 @@ public class WebOfTrust implements CertificateAuthority {
Iterator<Certificate> certificates = certificateStore.items();
Iterator<Certificate> withTrustRoot = new PrefixedIterator<>(trustRoot, certificates);
IterableIterator<Certificate> iterable = new IterableIterator<>(withTrustRoot);
network = fromCertificates(iterable, PGPainless.getPolicy(), Optional.just(ReferenceTime.now()));
network = fromCertificates(iterable, PGPainless.getPolicy(), ReferenceTime.now());
}
@ -95,9 +94,9 @@ public class WebOfTrust implements CertificateAuthority {
public static Network fromCertificates(
Iterable<Certificate> certificates,
Policy policy,
Optional<ReferenceTime> optReferenceTime) {
ReferenceTime optReferenceTime) {
ReferenceTime referenceTime = optReferenceTime.isPresent() ? optReferenceTime.get() : ReferenceTime.now();
ReferenceTime referenceTime = optReferenceTime == null ? ReferenceTime.now() : optReferenceTime;
List<KeyRingInfo> validCerts = parseValidCertificates(certificates, policy, referenceTime.getTimestamp());
LOGGER.debug("Successfully parsed " + validCerts.size() + " certificates.");

View File

@ -1,24 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra;
public final class IntegerUtils {
private IntegerUtils() {
}
/**
* Backported method from Java 8.
*
* @param x x
* @param y y
* @return result of comparison
*/
public static int compare(int x, int y) {
// noinspection UseCompareMethod
return x < y ? -1 : (x == y ? 0 : 1);
}
}

View File

@ -1,148 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import java.util.Date;
public class Certification {
private final CertSynopsis issuer;
private final CertSynopsis target;
private final Optional<String> userId;
private final Date creationTime;
private final Optional<Date> expirationTime;
private final boolean exportable;
private final int trustAmount;
private final Depth trustDepth;
private final RegexSet regex;
public Certification(
CertSynopsis issuer,
CertSynopsis target,
Optional<String> userId,
Date creationTime,
Optional<Date> expirationTime,
boolean exportable,
int trustAmount,
Depth trustDepth,
RegexSet regex) {
this.issuer = issuer;
this.target = target;
this.userId = userId;
this.creationTime = creationTime;
this.expirationTime = expirationTime;
this.exportable = exportable;
this.trustAmount = trustAmount;
this.trustDepth = trustDepth;
this.regex = regex;
}
public Certification(CertSynopsis issuer,
Optional<String> targetUserId,
CertSynopsis target,
Date creationTime) {
this.issuer = issuer;
this.target = target;
this.userId = targetUserId;
this.creationTime = creationTime;
this.expirationTime = Optional.empty();
this.exportable = true;
this.trustDepth = Depth.limited(0);
this.trustAmount = 120;
this.regex = RegexSet.wildcard();
}
/**
* Get the issuer of the certification.
*
* @return issuer
*/
public CertSynopsis getIssuer() {
return issuer;
}
/**
* Get the target of the certification.
*
* @return target
*/
public CertSynopsis getTarget() {
return target;
}
/**
* Get the target user-id.
*
* @return user-id
*/
public Optional<String> getUserId() {
return userId;
}
/**
* Get the creation time of the certification.
*
* @return creation time
*/
public Date getCreationTime() {
return creationTime;
}
/**
* Get the (optional) expiration time of the certification.
*
* @return optional expiration time
*/
public Optional<Date> getExpirationTime() {
return expirationTime;
}
/**
* Return true if the certification is marked as exportable.
*
* @return exportable
*/
public boolean isExportable() {
return exportable;
}
/**
* Get the trust amount of the certification.
*
* @return trust amount
*/
public int getTrustAmount() {
return trustAmount;
}
/**
* Get the trust depth of the certification.
*
* @return trust depth
*/
public Depth getTrustDepth() {
return trustDepth;
}
/**
* Return the set of regular expressions.
*
* @return regex set
*/
public RegexSet getRegexes() {
return regex;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(issuer.getFingerprint()).append((issuer.getUserIds().isEmpty() ? " " : " (" + issuer.getUserIds().keySet().iterator().next() + ") "));
sb.append(userId.isPresent() ? "certifies" : "delegates to").append(userId.isPresent() ? " [" + userId.get() + "] " : " ").append(target.getFingerprint())
.append(userId.isEmpty() && !target.getUserIds().isEmpty() ? " (" + target.getUserIds().keySet().iterator().next() + ")" : "");
return sb.toString();
}
}

View File

@ -1,127 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
/**
* A {@link CertificationSet} is a set of {@link Certification Certifications} made by the same issuer, on the same
* target certificate.
* In some sense, a {@link CertificationSet} can be considered an edge in the web of trust.
*/
public final class CertificationSet {
private final CertSynopsis issuer;
private final CertSynopsis target;
private final Map<Optional<String>, List<Certification>> certifications;
/**
* Create an empty {@link CertificationSet}.
*
* @param issuer issuer
* @param target target
* @return empty set
*/
public static CertificationSet empty(CertSynopsis issuer, CertSynopsis target) {
return new CertificationSet(issuer, target, new HashMap<>());
}
public static CertificationSet fromCertification(Certification certification) {
CertificationSet set = CertificationSet.empty(certification.getIssuer(), certification.getTarget());
set.add(certification);
return set;
}
private CertificationSet(CertSynopsis issuer,
CertSynopsis target,
Map<Optional<String>, List<Certification>> certifications) {
this.issuer = issuer;
this.target = target;
this.certifications = new HashMap<>(certifications);
}
public CertSynopsis getIssuer() {
return issuer;
}
public CertSynopsis getTarget() {
return target;
}
public Map<Optional<String>, List<Certification>> getCertifications() {
// Copy to avoid side effects
Map<Optional<String>, List<Certification>> copy = new HashMap<>();
for (Optional<String> key : certifications.keySet()) {
copy.put(key, new ArrayList<>(certifications.get(key)));
}
return copy;
}
/**
* Merge this {@link CertificationSet} with another instance.
* After the operation, this will contain {@link Certification Certifications} from both sets.
*
* @param other other {@link CertificationSet}
*/
public void merge(@Nonnull CertificationSet other) {
if (other == this) {
return;
}
if (!issuer.getFingerprint().equals(other.issuer.getFingerprint())) {
throw new IllegalArgumentException("Issuer fingerprint mismatch.");
}
if (!target.getFingerprint().equals(other.target.getFingerprint())) {
throw new IllegalArgumentException("Target fingerprint mismatch.");
}
for (Map.Entry<Optional<String>, List<Certification>> entry : other.certifications.entrySet()) {
for (Certification certification : entry.getValue()) {
add(certification);
}
}
}
/**
* Add a {@link Certification} into this {@link CertificationSet}.
*
* @param certification certification
*/
public void add(@Nonnull Certification certification) {
if (!issuer.getFingerprint().equals(certification.getIssuer().getFingerprint())) {
throw new IllegalArgumentException("Issuer fingerprint mismatch.");
}
if (!target.getFingerprint().equals(certification.getTarget().getFingerprint())) {
throw new IllegalArgumentException("Target fingerprint mismatch.");
}
List<Certification> certificationsForUserId = certifications.get(certification.getUserId());
// noinspection Java8MapApi
if (certificationsForUserId == null) {
certificationsForUserId = new ArrayList<>();
certifications.put(certification.getUserId(), certificationsForUserId);
}
// TODO: Prevent duplicates, only keep newest timestamped sig?
certificationsForUserId.add(certification);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<Optional<String>, List<Certification>> entry : certifications.entrySet()) {
for (Certification certification : entry.getValue()) {
sb.append(certification).append('\n');
}
}
return sb.toString();
}
}

View File

@ -1,82 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import org.pgpainless.wot.dijkstra.IntegerUtils;
import javax.annotation.Nonnull;
public final class Depth implements Comparable<Depth> {
private final Optional<Integer> depth;
private Depth(Optional<Integer> depth) {
this.depth = depth;
if (!isUnconstrained() && (getLimit().get() < 0 || getLimit().get() > 255)) {
throw new IllegalArgumentException("Depth must be a value between 0 and 255");
}
}
public static Depth unconstrained() {
return new Depth(Optional.empty());
}
public static Depth limited(int depth) {
return new Depth(Optional.just(depth));
}
public static Depth auto(int depth) {
return depth == 255 ? unconstrained() : limited(depth);
}
public Optional<Integer> getLimit() {
return depth;
}
public boolean isUnconstrained() {
return getLimit().isEmpty();
}
public Depth decrease(int value) {
if (isUnconstrained()) {
return unconstrained();
}
if (getLimit().get() >= value) {
return limited(getLimit().get() - value);
}
throw new IllegalArgumentException("Depth cannot be decreased.");
}
@Override
public int compareTo(@Nonnull Depth o) {
if (isUnconstrained()) {
if (o.isUnconstrained()) {
return 0;
} else {
return 1;
}
} else {
if (o.isUnconstrained()) {
return -1;
} else {
return IntegerUtils.compare(getLimit().get(), o.getLimit().get());
}
}
}
@Override
public String toString() {
return isUnconstrained() ? "unconstrained" : getLimit().get().toString();
}
public Depth min(Depth other) {
if (compareTo(other) <= 0) {
return this;
} else {
return other;
}
}
}

View File

@ -1,156 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import org.pgpainless.key.OpenPgpFingerprint;
/**
* A network consists of nodes, and edges between them.
* For the Web of Trust, nodes consist of {@link CertSynopsis CertSynopses}, while the edges between the nodes are
* {@link CertificationSet CertificationSets}.
* Edges can hereby be accessed in two ways:
* <ul>
* <li>{@link #getEdges()} returns a {@link Map} keyed by the {@link OpenPgpFingerprint fingerprint} of an issuer,
* whose values are {@link List Lists} containing all edges originating from the issuer.</li>
* <li>{@link #getReverseEdges()} on the other hand returns a {@link Map} keyed by the
* {@link OpenPgpFingerprint fingerprint} of a target, whose value are {@link List Lists} containing all edges
* pointing to the target.</li>
* </ul>
*/
public class Network {
private final Map<OpenPgpFingerprint, CertSynopsis> nodes;
private final Map<OpenPgpFingerprint, List<CertificationSet>> edges;
private final Map<OpenPgpFingerprint, List<CertificationSet>> reverseEdges;
private final ReferenceTime referenceTime;
/**
* Create a {@link Network} from a set of nodes, edges, reversed edges and a reference time.
*
* @param nodes map containing all nodes of the network, keyed by their fingerprints
* @param edges map containing all edges of the network, keyed by the fingerprint of the issuer
* @param reverseEdges map containing all reversed edges of the network, keyed by the fingerprint of the target
* @param referenceTime reference time
*/
public Network(Map<OpenPgpFingerprint, CertSynopsis> nodes,
Map<OpenPgpFingerprint, List<CertificationSet>> edges,
Map<OpenPgpFingerprint, List<CertificationSet>> reverseEdges,
ReferenceTime referenceTime) {
this.nodes = nodes;
this.edges = edges;
this.reverseEdges = reverseEdges;
this.referenceTime = referenceTime;
}
/**
* Create an empty {@link Network}.
*
* @param referenceTime reference time for evaluation
* @return network
*/
public static Network empty(@Nonnull ReferenceTime referenceTime) {
return new Network(
new HashMap<>(),
new HashMap<>(),
new HashMap<>(),
referenceTime);
}
/**
* Return all nodes ({@link CertSynopsis}) of the {@link Network}, indexed by their
* {@link OpenPgpFingerprint fingerprints}.
*
* @return nodes of the network
*/
public Map<OpenPgpFingerprint, CertSynopsis> getNodes() {
return new HashMap<>(nodes);
}
/**
* Return all edges of the {@link Network}, indexed by the {@link OpenPgpFingerprint fingerprint} of the issuer.
* An edge consists of a {@link CertificationSet} containing all signatures made by the issuer on the target.
* TODO: Do we care about immutability, or can we ignore that issue since Network is only used by
* WebOfTrust anyways?
*
* @return map of edges
*/
public Map<OpenPgpFingerprint, List<CertificationSet>> getEdges() {
return new HashMap<>(edges);
}
/**
* Return all reversed edges of the {@link Network}, indexed by the {@link OpenPgpFingerprint fingerprint} of the target.
* TODO: Do we care about immutability, or can we ignore that issue since Network is only used by
* WebOfTrust anyways?
*
* @return map of reversed edges
*/
public Map<OpenPgpFingerprint, List<CertificationSet>> getReverseEdges() {
return new HashMap<>(reverseEdges);
}
/**
* Return the total number of edges on the network.
*
* @return number of edges
*/
public int getNumberOfEdges() {
int num = 0;
for (List<CertificationSet> outEdges : edges.values()) {
num += outEdges.size();
}
return num;
}
/**
* Return the total number of signatures the network comprises.
*
* @return number of signatures
*/
public int getNumberOfSignatures() {
int num = 0;
for (List<CertificationSet> edgesPerIssuer : edges.values()) {
for (CertificationSet edge : edgesPerIssuer) {
for (List<Certification> sigsPerDatum : edge.getCertifications().values()) {
num += sigsPerDatum.size();
}
}
}
return num;
}
/**
* Return the {@link ReferenceTime} which was used when creating the {@link Network}.
*
* @return reference time
*/
public ReferenceTime getReferenceTime() {
return referenceTime;
}
public String toString() {
int edgeNum = 0;
for (List<CertificationSet> edgesFrom : edges.values()) {
edgeNum += edgesFrom.size();
}
StringBuilder sb = new StringBuilder("Network with " + getNodes().size() + " nodes, " + edgeNum + " edges:\n");
for (OpenPgpFingerprint issuer : getNodes().keySet()) {
List<CertificationSet> edges = getEdges().get(issuer);
if (edges == null) {
continue;
}
for (CertificationSet edge : edges) {
sb.append(edge);
}
}
return sb.toString();
}
}

View File

@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class Optional<T> {
private final T item;
public static <T> Optional<T> empty() {
return new Optional<>();
}
public static <T> Optional<T> just(@Nonnull T item) {
return new Optional<>(item);
}
public static <T> Optional<T> maybe(@Nullable T item) {
return item == null ? empty() : just(item);
}
private Optional() {
this.item = null;
}
private Optional(@Nonnull T item) {
this.item = item;
}
public boolean isEmpty() {
return item == null;
}
public boolean isPresent() {
return item != null;
}
public @Nonnull T get() {
if (item == null) {
throw new NullPointerException("Item is null.");
}
return item;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Optional)) {
return false;
}
Optional other = (Optional) obj;
if (isEmpty() && other.isEmpty()) {
return true;
}
if (isPresent() && isPresent()) {
return get().equals(other.get());
}
return false;
}
}

View File

@ -1,94 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import java.util.ArrayList;
import java.util.List;
public class Path {
private CertSynopsis root;
private List<Certification> edges;
private Depth residualDepth;
public Path(CertSynopsis root) {
this.root = root;
this.edges = new ArrayList<>();
this.residualDepth = Depth.unconstrained();
}
public CertSynopsis getRoot() {
return root;
}
public CertSynopsis getTarget() {
if (edges.isEmpty()) {
return getRoot();
} else {
return edges.get(edges.size() - 1).getTarget();
}
}
public List<CertSynopsis> getCertificates() {
List<CertSynopsis> certs = new ArrayList<>();
certs.add(getRoot());
for (Certification edge : edges) {
certs.add(edge.getTarget());
}
return certs;
}
public int getLength() {
return edges.size() + 1;
}
public List<Certification> getCertifications() {
return new ArrayList<>(edges);
}
public Depth getResidualDepth() {
return residualDepth;
}
public int getAmount() {
if (edges.isEmpty()) {
return 120;
}
int min = 255;
for (Certification edge : edges) {
min = Math.min(min, edge.getTrustAmount());
}
return min;
}
public void append(Certification certification) {
if (!getTarget().getFingerprint().equals(certification.getIssuer().getFingerprint())) {
throw new IllegalArgumentException("Cannot append certification to path: Path's tail is not issuer of the certification.");
}
if (!residualDepth.isUnconstrained() && residualDepth.getLimit().get() == 0) {
throw new IllegalArgumentException("Not enough depth.");
}
boolean cyclic = getRoot().getFingerprint().equals(certification.getTarget().getFingerprint());
for (int i = 0; i < edges.size() && !cyclic; i++) {
Certification edge = edges.get(i);
if (edge.getTarget().getFingerprint().equals(certification.getTarget().getFingerprint())) {
if (i == edges.size() - 1) {
cyclic = edge.getUserId().equals(certification.getUserId());
} else {
cyclic = true;
}
}
}
if (cyclic) {
throw new IllegalArgumentException("Adding the certification to the path would create a cycle.");
}
residualDepth = certification.getTrustDepth().min(residualDepth.decrease(1));
edges.add(certification);
}
}

View File

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import java.util.ArrayList;
import java.util.List;
public class Paths {
private final List<Item> paths = new ArrayList<>();
public List<Item> getPaths() {
return new ArrayList<>(paths);
}
public void add(Path path, int amount) {
if (amount <= path.getAmount()) {
throw new AssertionError();
}
this.paths.add(new Item(path, amount));
}
public int getAmount() {
int sum = 0;
for (Item item : paths) {
sum += item.amount;
}
return sum;
}
public static class Item {
private final Path path;
private final int amount;
public Item(Path path, int amount) {
this.path = path;
this.amount = amount;
}
public Path getPath() {
return path;
}
public int getAmount() {
return amount;
}
}
}

View File

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
public final class RegexSet {
private final Set<String> regexStrings;
private RegexSet(Set<String> regexStrings) {
this.regexStrings = regexStrings;
}
public static RegexSet fromExpressionList(@Nonnull List<String> regexList) {
Set<String> regexStringSet = new HashSet<>(regexList);
return new RegexSet(regexStringSet);
}
public static RegexSet fromExpression(@Nonnull String regex) {
return fromExpressionList(Collections.singletonList(regex));
}
public static RegexSet wildcard() {
return fromExpressionList(Collections.emptyList());
}
public boolean matches(String string) {
if (regexStrings.isEmpty()) {
return true;
}
for (String regex : regexStrings) {
Matcher matcher = Pattern.compile(regex).matcher(string);
if (matcher.matches()) {
return true;
}
}
return false;
}
}

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
import org.pgpainless.algorithm.RevocationState

View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
import java.util.*
data class Certification(
val issuer: CertSynopsis,
val target: CertSynopsis,
val userId: String?,
val creationTime: Date,
val expirationTime: Date?,
val exportable: Boolean,
val trustAmount: Int,
val trustDepth: Depth,
val regex: RegexSet
) {
constructor(
issuer: CertSynopsis,
targetUserId: String?,
target: CertSynopsis,
creationTime: Date) :
this(issuer, target, targetUserId, creationTime, null, true, 120, Depth.limited(0), RegexSet.wildcard())
override fun toString(): String {
val relation = if (userId != null) {
"certifies"
} else {
"delegates to"
}
val relationTarget = if (userId != null) {
"[$userId] ${target.fingerprint}"
} else {
"$target"
}
return "$issuer $relation $relationTarget"
}
}

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
data class CertificationSet(
val issuer: CertSynopsis,
val target: CertSynopsis,
val certifications: MutableMap<String?, MutableList<Certification>>) {
companion object {
@JvmStatic
fun empty(issuer: CertSynopsis, target: CertSynopsis): CertificationSet {
return CertificationSet(issuer, target, HashMap())
}
@JvmStatic
fun fromCertification(certification: Certification) : CertificationSet {
val set = empty(certification.issuer, certification.target)
set.add(certification)
return set
}
}
fun merge(other: CertificationSet) {
if (other == this) {
return
}
require(issuer.fingerprint == other.issuer.fingerprint) { "Issuer fingerprint mismatch." }
require(target.fingerprint == other.target.fingerprint) { "Target fingerprint mismatch." }
for (userId in other.certifications.keys) {
for (certification in other.certifications[userId]!!) {
add(certification)
}
}
}
fun add(certification : Certification) {
require(issuer.fingerprint == certification.issuer.fingerprint) { "Issuer fingerprint mismatch." }
require(target.fingerprint == certification.target.fingerprint) { "Target fingerprint mismatch." }
var certificationsForUserId: MutableList<Certification>? = certifications[certification.userId]
if (certificationsForUserId == null) {
certificationsForUserId = ArrayList()
certifications[certification.userId] = certificationsForUserId
}
certificationsForUserId.add(certification)
}
override fun toString(): String {
return "$certifications"
}
}

View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
class Depth(val limit: Int?) : Comparable<Depth> {
companion object {
@JvmStatic
fun unconstrained() : Depth {
return Depth(null)
}
@JvmStatic
fun limited(limit: Int): Depth {
require(limit in 0..255) {
"Trust depth MUST be a value between 0 and 255."
}
return Depth(limit)
}
@JvmStatic
fun auto(limit: Int): Depth {
return if (limit == 255) {
unconstrained()
} else {
limited(limit)
}
}
}
fun isUnconstrained() : Boolean {
return limit == null
}
fun decrease(value : Int) : Depth {
return if (isUnconstrained()) {
unconstrained()
} else {
if (limit!! >= value) {
limited(limit - value)
} else {
throw IllegalArgumentException("Depth cannot be decreased.")
}
}
}
fun min(other: Depth) : Depth {
return if (compareTo(other) <= 0) {
this
} else {
other
}
}
override fun compareTo(o: Depth): Int {
return if (isUnconstrained()) {
if (o.isUnconstrained()) {
0
} else {
1
}
} else {
if (o.isUnconstrained()) {
-1
} else {
limit!!.compareTo(o.limit!!)
}
}
}
override fun toString() : String {
return if (isUnconstrained()) { "unconstrained" } else { limit!!.toString() }
}
}

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
import org.pgpainless.key.OpenPgpFingerprint
/**
* A network consists of nodes, and edges between them.
* For the Web of Trust, nodes consist of [CertSynopses][CertSynopsis], while the edges between the nodes are
* [CertificationSets][CertificationSet].
*
* @constructor creates a new network
* @param nodes contains a [Map] of [CertSynopsis] keyed by their [OpenPgpFingerprint]
* @param edges [Map] keyed by the [fingerprint][OpenPgpFingerprint] of an issuer, whose values are [Lists][List] containing all edges originating from the issuer.
* @param reverseEdges [Map] keyed by the [fingerprint][OpenPgpFingerprint] of a target, whose values are [Lists][List] containing all edges pointing to the target
* @param referenceTime reference time at which the [Network] was built
*/
class Network(
val nodes: Map<OpenPgpFingerprint, CertSynopsis>,
val edges: Map<OpenPgpFingerprint, List<CertificationSet>>,
val reverseEdges: Map<OpenPgpFingerprint, List<CertificationSet>>,
val referenceTime: ReferenceTime) {
companion object {
@JvmStatic
fun empty(referenceTime: ReferenceTime): Network {
return Network(HashMap(), HashMap(), HashMap(), referenceTime)
}
}
val numberOfEdges: Int
get() {
return edges.values.sumOf { it.size }
}
val numberOfSignatures: Int
get() {
return edges.values
.flatten()
.flatMap { it.certifications.values }
.sumOf { it.size }
}
override fun toString(): String {
val sb = StringBuilder()
sb.append("Network with ${nodes.size} nodes, $numberOfEdges edges:\n")
for (issuer in nodes.keys) {
val outEdges = edges[issuer] ?: continue
for (edge in outEdges) {
sb.append(edge)
}
}
return sb.toString()
}
}

View File

@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
import kotlin.math.min
class Path(
val root: CertSynopsis,
val edges: MutableList<Certification>,
var residualDepth: Depth
) {
constructor(root: CertSynopsis) : this(
root, mutableListOf<Certification>(), Depth.unconstrained())
val target: CertSynopsis
get() {
return if (edges.isEmpty()) {
root
} else {
edges[edges.size - 1].target
}
}
val certificates: List<CertSynopsis>
get() {
val certs: MutableList<CertSynopsis> = ArrayList()
certs.add(root)
for (certification in edges) {
certs.add(certification.target)
}
return certs
}
val length: Int
get() = edges.size + 1
val certifications: List<Certification>
get() = ArrayList(edges)
val amount: Int
get() = if (edges.isEmpty()) {
120
} else {
var min = 255
for (edge in edges) {
min = min(min, edge.trustAmount)
}
min
}
fun append(certification: Certification) {
require(target.fingerprint == certification.issuer.fingerprint) {
"Cannot append certification to path: Path's tail is not issuer of the certification."
}
require(residualDepth.isUnconstrained() || residualDepth.limit!! > 0) {
"Not enough depth."
}
var cyclic = root.fingerprint == certification.target.fingerprint
for (i in 0..edges.size) {
val edge = edges[i]
if (cyclic) {
break
}
if (edge.target.fingerprint == certification.target.fingerprint) {
cyclic = if (i == edges.size - 1) {
edge.userId == certification.userId
} else {
true
}
}
}
require(!cyclic) { "Adding the certification to the path would create a cycle." }
residualDepth = certification.trustDepth.min(residualDepth.decrease(1))
edges.add(certification)
}
}

View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
class Paths(val paths: MutableList<Item>) {
fun add(path: Path, amount: Int) {
require(amount <= path.amount) {
"Amount too small. TODO: Better error message"
}
paths.add(Item(path, amount))
}
val amount: Int
get() {
return paths.sumOf { it.amount }
}
data class Item(val path: Path, val amount: Int) {
}
}

View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.wot.dijkstra.sq
import java.util.regex.Pattern
data class RegexSet(val regexStrings: Set<String>) {
companion object {
@JvmStatic
fun fromExpressionList(regexList: List<String>): RegexSet {
return RegexSet(regexList.toSet())
}
@JvmStatic
fun fromExpression(regex: String): RegexSet {
return fromExpressionList(listOf(regex))
}
@JvmStatic
fun wildcard(): RegexSet {
return fromExpressionList(listOf())
}
}
fun matches(string: String): Boolean {
if (regexStrings.isEmpty()) {
return true
}
for (regex in regexStrings) {
val matcher = Pattern.compile(regex).matcher(string)
if (matcher.matches()) {
return true
}
}
return false
}
}

View File

@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -17,15 +19,15 @@ public class DepthTest {
public void testUnlimitedItem() {
Depth depth = Depth.unconstrained();
assertTrue(depth.isUnconstrained());
assertFalse(depth.getLimit().isPresent());
assertNull(depth.getLimit());
}
@Test
public void testLimitedItem() {
Depth limited = Depth.limited(2);
assertFalse(limited.isUnconstrained());
assertTrue(limited.getLimit().isPresent());
assertEquals(2, limited.getLimit().get());
assertNotNull(limited.getLimit());
assertEquals(2, limited.getLimit());
}
@Test
@ -40,7 +42,7 @@ public class DepthTest {
Depth limited = Depth.limited(1);
Depth decreased = limited.decrease(1);
assertFalse(decreased.isUnconstrained());
assertEquals(0, decreased.getLimit().get());
assertEquals(0, decreased.getLimit());
}
@Test
@ -76,7 +78,7 @@ public class DepthTest {
public void testAutoLimited() {
Depth depth = Depth.auto(120);
assertFalse(depth.isUnconstrained());
assertEquals(120, depth.getLimit().get());
assertEquals(120, depth.getLimit());
}
@Test