mirror of
https://codeberg.org/PGPainless/wkd-java.git
synced 2025-09-10 11:49:39 +02:00
Add wkd-test-suite module
This commit is contained in:
parent
79af7ccab6
commit
049c14b779
12 changed files with 677 additions and 2 deletions
122
wkd-test-suite/src/main/java/pgp/wkd/test_suite/Main.java
Normal file
122
wkd-test-suite/src/main/java/pgp/wkd/test_suite/Main.java
Normal file
|
@ -0,0 +1,122 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd.test_suite;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import picocli.CommandLine;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@CommandLine.Command(name = "wkd-test-suite", mixinStandardHelpOptions = true, version = "0.1")
|
||||
public class Main implements Runnable {
|
||||
|
||||
private static final Pattern PATTERN_DOMAIN = Pattern.compile("^[a-zA-Z0-9.-]+$");
|
||||
|
||||
@CommandLine.Option(names = {"--output-dir", "-o"},
|
||||
description = "Output directory",
|
||||
required = true)
|
||||
private File rootDir;
|
||||
|
||||
@CommandLine.Option(names = "--xml-summary",
|
||||
description = "Write XML summary to file",
|
||||
arity = "0..1")
|
||||
private List<File> xmlOutputFiles = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = "--json-summary",
|
||||
description = "Write JSON summary to file",
|
||||
arity = "0..1")
|
||||
private List<File> jsonOutputFiles = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = {"--domain", "-d"},
|
||||
description = "Root domain",
|
||||
required = true, arity = "1")
|
||||
private String domain;
|
||||
|
||||
@CommandLine.Option(names = {"--method", "-m"},
|
||||
paramLabel = "{direct|advanced}",
|
||||
description = "Method for key discovery. If absent, assume direct.")
|
||||
private TestSuiteGenerator.Method method = TestSuiteGenerator.Method.direct;
|
||||
|
||||
@CommandLine.Spec // injected by picocli
|
||||
CommandLine.Model.CommandSpec spec;
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.exit(new CommandLine(new Main()).execute(args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
validate();
|
||||
|
||||
TestSuiteGenerator generator = new TestSuiteGenerator(domain);
|
||||
try {
|
||||
TestSuite suite = generator.generateTestSuiteInDirectory(rootDir, method);
|
||||
writeSummaries(suite);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (missing(xmlOutputFiles) && missing(jsonOutputFiles)) {
|
||||
throw new CommandLine.ParameterException(spec.commandLine(),
|
||||
"Missing option. At least on of '--xml-summary' or '--json-summary' options must be specified.");
|
||||
}
|
||||
if (!PATTERN_DOMAIN.matcher(domain).matches()) {
|
||||
throw new CommandLine.ParameterException(spec.commandLine(),
|
||||
"Value of option '--domain' must be a valid domain string.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean missing(List<?> list) {
|
||||
return list == null || list.isEmpty();
|
||||
}
|
||||
|
||||
private void writeSummaries(TestSuite suite) {
|
||||
ObjectMapper xmlMapper = new XmlMapper();
|
||||
xmlMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
ObjectMapper jsonMapper = new JsonMapper();
|
||||
jsonMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
ObjectWriter xmlWriter = xmlMapper.writer().withDefaultPrettyPrinter();
|
||||
for (File destination : xmlOutputFiles) {
|
||||
writeSummary(suite, destination, xmlWriter);
|
||||
}
|
||||
|
||||
ObjectWriter jsonWriter = jsonMapper.writer().withDefaultPrettyPrinter();
|
||||
for (File destination : jsonOutputFiles) {
|
||||
writeSummary(suite, destination, jsonWriter);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSummary(TestSuite suite, File destination, ObjectWriter objWriter) {
|
||||
try {
|
||||
destination.createNewFile();
|
||||
} catch (IOException e) {
|
||||
// Skip?
|
||||
return;
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOut = new FileOutputStream(destination); OutputStreamWriter osWriter = new OutputStreamWriter(fileOut, Charset.forName("UTF8"))) {
|
||||
objWriter.writeValue(osWriter, suite);
|
||||
} catch (IOException e) {
|
||||
// Skip?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd.test_suite;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TestCase {
|
||||
|
||||
final boolean expectSuccess;
|
||||
final String testTitle;
|
||||
final String testDescription;
|
||||
final String lookupMailAddress;
|
||||
final String certificatePath;
|
||||
final URI lookupUri;
|
||||
|
||||
public TestCase(boolean expectSuccess, String testTitle, String description, String lookupMailAddress, Path certificatePath, URI lookupUri) {
|
||||
this.expectSuccess = expectSuccess;
|
||||
this.testTitle = testTitle;
|
||||
this.testDescription = description;
|
||||
this.lookupMailAddress = lookupMailAddress;
|
||||
this.certificatePath = certificatePath.toString();
|
||||
this.lookupUri = lookupUri;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd.test_suite;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TestSuite {
|
||||
|
||||
final String version;
|
||||
final List<TestCase> testCases;
|
||||
|
||||
public TestSuite(String version, List<TestCase> testCases) {
|
||||
this.version = version;
|
||||
this.testCases = testCases;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd.test_suite;
|
||||
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class TestSuiteGenerator {
|
||||
|
||||
public enum Method {
|
||||
direct,
|
||||
advanced
|
||||
}
|
||||
|
||||
private final String domain;
|
||||
|
||||
public TestSuiteGenerator(String domain) {
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
public TestSuite generateTestSuiteInDirectory(File directory, Method method) throws Exception {
|
||||
WkdDirectoryStructure structure = directoryStructureForMethod(directory, method);
|
||||
structure.mkdirs();
|
||||
|
||||
List<TestCase> tests = new ArrayList<>();
|
||||
tests.add(baseCase(structure));
|
||||
tests.add(wrongUserId(structure));
|
||||
tests.add(noUserId(structure));
|
||||
tests.addAll(baseCaseMultiUserIds(structure));
|
||||
tests.add(secretKeyMaterial(structure));
|
||||
|
||||
return new TestSuite("0.1", tests);
|
||||
}
|
||||
|
||||
private WkdDirectoryStructure directoryStructureForMethod(File directory, Method method) {
|
||||
WkdDirectoryStructure structure;
|
||||
if (method == Method.direct) {
|
||||
structure = new WkdDirectoryStructure.DirectMethod(directory, domain);
|
||||
} else if (method == Method.advanced) {
|
||||
structure = new WkdDirectoryStructure.AdvancedMethod(directory, domain);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid value for parameter 'method'.");
|
||||
}
|
||||
return structure;
|
||||
}
|
||||
|
||||
private PGPPublicKeyRing certificate(String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing(userId, null);
|
||||
PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys);
|
||||
return publicKeys;
|
||||
}
|
||||
|
||||
private TestCase baseCase(WkdDirectoryStructure directoryStructure) throws Exception {
|
||||
String lookupMail = "base-case@" + domain;
|
||||
String userId = "WKD-Test Base Case <base-case@" + domain + ">";
|
||||
String description = "Certificate has a single, valid user-id '" + userId + "'";
|
||||
|
||||
PGPPublicKeyRing publicKeys = certificate(userId);
|
||||
URI lookupUri = directoryStructure.getAddress(lookupMail);
|
||||
Path path = directoryStructure.getRelativeCertificatePath(lookupMail);
|
||||
File file = directoryStructure.resolve(path);
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new IOException("Cannot create file " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOut = new FileOutputStream(file)) {
|
||||
publicKeys.encode(fileOut);
|
||||
}
|
||||
|
||||
return new TestCase(true, "Base Case", description, lookupMail, path, lookupUri);
|
||||
}
|
||||
|
||||
private List<TestCase> baseCaseMultiUserIds(WkdDirectoryStructure directoryStructure) throws Exception {
|
||||
String primaryLookupMail = "primary-uid@" + domain;
|
||||
String secondaryLookupMail = "secondary-uid@" + domain;
|
||||
String primaryUserId = "WKD-Test Primary User-ID <" + primaryLookupMail + ">";
|
||||
String secondaryUserId = "WKD-Test Secondary User-ID <" + secondaryLookupMail + ">";
|
||||
String primaryDescription = "Certificate has multiple, valid user-ids. Is looked up via primary user-id '" + primaryUserId + "' using mail address '" + primaryLookupMail + "'.";
|
||||
String secondaryDescription = "Certificate has multiple, valid user-ids. Is looked up via secondary user-id '" + secondaryUserId + "' using mail address '" + secondaryLookupMail + "'.";
|
||||
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing(primaryUserId, null);
|
||||
SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys();
|
||||
secretKeys = PGPainless.modifyKeyRing(secretKeys)
|
||||
.addUserId(secondaryUserId, protector)
|
||||
.done();
|
||||
PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys);
|
||||
|
||||
Path primaryPath = directoryStructure.getRelativeCertificatePath(primaryLookupMail);
|
||||
Path secondaryPath = directoryStructure.getRelativeCertificatePath(secondaryLookupMail);
|
||||
File primaryFile = directoryStructure.resolve(primaryPath);
|
||||
File secondaryFile = directoryStructure.resolve(secondaryPath);
|
||||
|
||||
if (!primaryFile.exists() && !primaryFile.createNewFile()) {
|
||||
throw new IOException("Cannot create file " + primaryFile.getAbsolutePath());
|
||||
}
|
||||
if (!secondaryFile.exists() && !secondaryFile.createNewFile()) {
|
||||
throw new IOException("Cannot create file " + secondaryFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOut = new FileOutputStream(primaryFile)) {
|
||||
publicKeys.encode(fileOut);
|
||||
}
|
||||
try (FileOutputStream fileOut = new FileOutputStream(secondaryFile)) {
|
||||
publicKeys.encode(fileOut);
|
||||
}
|
||||
|
||||
return Arrays.asList(
|
||||
new TestCase(true, "Multi-User-ID - Primary User-ID Lookup",
|
||||
primaryDescription, primaryLookupMail, primaryPath, directoryStructure.getAddress(primaryLookupMail)),
|
||||
new TestCase(true, "Multi-User-ID - Secondary User-ID Lookup",
|
||||
secondaryDescription, secondaryLookupMail, secondaryPath, directoryStructure.getAddress(secondaryLookupMail))
|
||||
);
|
||||
}
|
||||
|
||||
private TestCase wrongUserId(WkdDirectoryStructure directoryStructure) throws Exception {
|
||||
String lookupMail = "wrong-userid@" + domain;
|
||||
String userId = "WKD-Test Different User-ID <different-userid@" + domain + ">";
|
||||
String description = "Certificate has a single, valid user-id '" + userId + "', but is deposited for mail address '" + lookupMail + "'.";
|
||||
PGPPublicKeyRing publicKeys = certificate(userId);
|
||||
Path path = directoryStructure.getRelativeCertificatePath(lookupMail);
|
||||
File file = directoryStructure.resolve(path);
|
||||
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new IOException("Cannot create file " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOut = new FileOutputStream(file)) {
|
||||
publicKeys.encode(fileOut);
|
||||
}
|
||||
|
||||
return new TestCase(false, "Wrong User-ID", description, lookupMail, path, directoryStructure.getAddress(lookupMail));
|
||||
}
|
||||
|
||||
private TestCase noUserId(WkdDirectoryStructure directoryStructure) throws Exception {
|
||||
String lookupMail = "absent-userid@" + domain;
|
||||
String description = "Certificate has no user-id, but is deposited for mail address '" + lookupMail + "'.";
|
||||
// Generate certificate with temp user-id
|
||||
PGPPublicKeyRing publicKeys = certificate("DeleteMe");
|
||||
|
||||
// delete user-id
|
||||
List<PGPPublicKey> keys = new ArrayList<>();
|
||||
Iterator<PGPPublicKey> publicKeyIterator = publicKeys.iterator();
|
||||
PGPPublicKey primaryKey = publicKeyIterator.next();
|
||||
primaryKey = PGPPublicKey.removeCertification(primaryKey, "DeleteMe");
|
||||
keys.add(primaryKey);
|
||||
while (publicKeyIterator.hasNext()) {
|
||||
keys.add(publicKeyIterator.next());
|
||||
}
|
||||
publicKeys = new PGPPublicKeyRing(keys);
|
||||
|
||||
|
||||
Path path = directoryStructure.getRelativeCertificatePath(lookupMail);
|
||||
File file = directoryStructure.resolve(path);
|
||||
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new IOException("Cannot create file " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOut = new FileOutputStream(file)) {
|
||||
publicKeys.encode(fileOut);
|
||||
}
|
||||
|
||||
return new TestCase(false, "No User-ID", description, lookupMail, path, directoryStructure.getAddress(lookupMail));
|
||||
}
|
||||
|
||||
private TestCase secretKeyMaterial(WkdDirectoryStructure directoryStructure) throws Exception {
|
||||
String lookupMail = "test-secret-key@" + domain;
|
||||
String description = "Certificate file contains secret key material.";
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("WKD-Test Secret Key <" + lookupMail + ">", null);
|
||||
|
||||
Path path = directoryStructure.getRelativeCertificatePath(lookupMail);
|
||||
File file = directoryStructure.resolve(path);
|
||||
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new IOException("Cannot create file " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOut = new FileOutputStream(file)) {
|
||||
secretKeys.encode(fileOut);
|
||||
}
|
||||
|
||||
return new TestCase(false, "Secret Key Material", description, lookupMail, path, directoryStructure.getAddress(lookupMail));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pgp.wkd.test_suite;
|
||||
|
||||
import pgp.wkd.WKDAddress;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public abstract class WkdDirectoryStructure {
|
||||
|
||||
protected final String domain;
|
||||
protected final File rootDir;
|
||||
protected final File wellKnown;
|
||||
protected final File openpgpkey;
|
||||
|
||||
public WkdDirectoryStructure(File rootDirectory, String domain) {
|
||||
this.domain = domain;
|
||||
this.rootDir = rootDirectory;
|
||||
wellKnown = new File(rootDirectory, ".well-known");
|
||||
openpgpkey = new File(wellKnown, "openpgpkey");
|
||||
}
|
||||
|
||||
public abstract File getHu();
|
||||
|
||||
public abstract Path getRelativeCertificatePath(String mailAddress);
|
||||
|
||||
public abstract void mkdirs() throws IOException;
|
||||
|
||||
protected void mkdir(File dir) throws IOException {
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
throw new IOException("Cannot create directory '" + dir.getAbsolutePath() + "'.");
|
||||
}
|
||||
if (dir.isFile()) {
|
||||
throw new IOException("Cannot create directory '" + dir.getAbsolutePath() + "': Is a file.");
|
||||
}
|
||||
}
|
||||
|
||||
public abstract URI getAddress(String mail);
|
||||
|
||||
public abstract File resolve(Path path);
|
||||
|
||||
public static class DirectMethod extends WkdDirectoryStructure {
|
||||
|
||||
private final File hu;
|
||||
|
||||
public DirectMethod(File rootDirectory, String domain) {
|
||||
super(rootDirectory, domain);
|
||||
this.hu = new File(openpgpkey, "hu");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getHu() {
|
||||
return hu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getRelativeCertificatePath(String mailAddress) {
|
||||
WKDAddress address = WKDAddress.fromEmail(mailAddress);
|
||||
String path = address.getDirectMethodURI().getPath();
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
return rootDir.toPath().relativize(new File(getHu(), fileName).toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mkdirs() throws IOException {
|
||||
mkdir(rootDir);
|
||||
mkdir(wellKnown);
|
||||
mkdir(openpgpkey);
|
||||
mkdir(hu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getAddress(String mail) {
|
||||
return WKDAddress.fromEmail(mail).getDirectMethodURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File resolve(Path path) {
|
||||
return rootDir.toPath().resolve(path).toFile();
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdvancedMethod extends WkdDirectoryStructure {
|
||||
|
||||
private final File domainFile;
|
||||
private final File hu;
|
||||
|
||||
public AdvancedMethod(File rootDir, String domain) {
|
||||
super(rootDir, domain);
|
||||
this.domainFile = new File(openpgpkey, domain);
|
||||
this.hu = new File(domainFile, "hu");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getHu() {
|
||||
return hu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getRelativeCertificatePath(String mailAddress) {
|
||||
WKDAddress address = WKDAddress.fromEmail(mailAddress);
|
||||
String path = address.getAdvancedMethodURI().getPath();
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
return rootDir.toPath().relativize(new File(getHu(), fileName).toPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mkdirs() throws IOException {
|
||||
mkdir(rootDir);
|
||||
mkdir(wellKnown);
|
||||
mkdir(openpgpkey);
|
||||
mkdir(domainFile);
|
||||
mkdir(hu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getAddress(String mail) {
|
||||
return WKDAddress.fromEmail(mail).getAdvancedMethodURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File resolve(Path path) {
|
||||
return rootDir.toPath().resolve(path).toFile();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* WKD Test Suite Generator.
|
||||
*/
|
||||
package pgp.wkd.test_suite;
|
Loading…
Add table
Add a link
Reference in a new issue