From 049c14b7798516a22e225ad188eb197db98a9ad5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Mar 2022 17:01:30 +0100 Subject: [PATCH] Add wkd-test-suite module --- build.gradle | 1 + settings.gradle | 3 +- wkd-java-cli/build.gradle | 2 +- wkd-test-suite/README.md | 78 +++++++ wkd-test-suite/build.gradle | 54 +++++ .../main/java/pgp/wkd/test_suite/Main.java | 122 +++++++++++ .../java/pgp/wkd/test_suite/TestCase.java | 27 +++ .../java/pgp/wkd/test_suite/TestSuite.java | 18 ++ .../wkd/test_suite/TestSuiteGenerator.java | 204 ++++++++++++++++++ .../wkd/test_suite/WkdDirectoryStructure.java | 131 +++++++++++ .../java/pgp/wkd/test_suite/package-info.java | 8 + wkd-test-suite/strategy.md | 31 +++ 12 files changed, 677 insertions(+), 2 deletions(-) create mode 100644 wkd-test-suite/README.md create mode 100644 wkd-test-suite/build.gradle create mode 100644 wkd-test-suite/src/main/java/pgp/wkd/test_suite/Main.java create mode 100644 wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestCase.java create mode 100644 wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuite.java create mode 100644 wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuiteGenerator.java create mode 100644 wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java create mode 100644 wkd-test-suite/src/main/java/pgp/wkd/test_suite/package-info.java create mode 100644 wkd-test-suite/strategy.md diff --git a/build.gradle b/build.gradle index a8e6c78..937bce4 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,7 @@ allprojects { junitVersion = '5.8.2' slf4jVersion = '1.7.32' logbackVersion = '1.2.10' + pgpainlessVersion = '1.1.1' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) diff --git a/settings.gradle b/settings.gradle index e090ad6..8213d40 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,5 +5,6 @@ rootProject.name = 'WKD-Java' include 'wkd-java', - 'wkd-java-cli' + 'wkd-java-cli', + 'wkd-test-suite' diff --git a/wkd-java-cli/build.gradle b/wkd-java-cli/build.gradle index 5935544..aa04d8b 100644 --- a/wkd-java-cli/build.gradle +++ b/wkd-java-cli/build.gradle @@ -20,7 +20,7 @@ dependencies { testImplementation 'com.ginsberg:junit5-system-exit:1.1.2' testImplementation 'org.mockito:mockito-core:4.3.1' - implementation("org.pgpainless:pgpainless-core:1.1.0") + implementation("org.pgpainless:pgpainless-core:$pgpainlessVersion") implementation project(':wkd-java') implementation "info.picocli:picocli:4.6.3" diff --git a/wkd-test-suite/README.md b/wkd-test-suite/README.md new file mode 100644 index 0000000..ee45e3d --- /dev/null +++ b/wkd-test-suite/README.md @@ -0,0 +1,78 @@ + + +# WKD Test Suite Generator + +This module contains a CLI application that can be used to generate WKD test vectors. + +```shell +$ java -jar wkd-test-suite.jar help +Usage: wkd-test-suite [-hV] [--json-summary[=]]... + [--xml-summary[=]]... -d= + [-m={direct|advanced}] -o= + -d, --domain= Root domain + -h, --help Show this help message and exit. + --json-summary[=] + Write JSON summary to file + -m, --method={direct|advanced} + Method for key discovery + -o, --output-dir= + Output directory + -V, --version Print version information and exit. + --xml-summary[=] + Write XML summary to file +``` + +Example output summary.json: + +```json +{ + "version" : "0.1", + "testCases" : [ { + "expectSuccess" : true, + "testTitle" : "Base Case", + "testDescription" : "Certificate has a single, valid user-id 'WKD-Test Base Case '", + "lookupMailAddress" : "base-case@pgpainless.github.io", + "certificatePath" : ".well-known/openpgpkey/hu/6q1ubufxsqh8fjuewbachy5ocz9seanp", + "lookupUri" : "https://pgpainless.github.io/.well-known/openpgpkey/hu/6q1ubufxsqh8fjuewbachy5ocz9seanp?l=base-case" + }, { + "expectSuccess" : false, + "testTitle" : "Wrong User-ID", + "testDescription" : "Certificate has a single, valid user-id 'WKD-Test Different User-ID ', but is deposited for mail address 'wrong-userid@pgpainless.github.io'.", + "lookupMailAddress" : "wrong-userid@pgpainless.github.io", + "certificatePath" : ".well-known/openpgpkey/hu/87rxmyhh4paokf1apw6qiej8hk6nwuxy", + "lookupUri" : "https://pgpainless.github.io/.well-known/openpgpkey/hu/87rxmyhh4paokf1apw6qiej8hk6nwuxy?l=wrong-userid" + }, { + "expectSuccess" : false, + "testTitle" : "No User-ID", + "testDescription" : "Certificate has no user-id, but is deposited for mail address 'absent-userid@pgpainless.github.io'.", + "lookupMailAddress" : "absent-userid@pgpainless.github.io", + "certificatePath" : ".well-known/openpgpkey/hu/caky1x1mawkc6gg4kge1icod96wqaeax", + "lookupUri" : "https://pgpainless.github.io/.well-known/openpgpkey/hu/caky1x1mawkc6gg4kge1icod96wqaeax?l=absent-userid" + }, { + "expectSuccess" : true, + "testTitle" : "Multi-User-ID - Primary User-ID Lookup", + "testDescription" : "Certificate has multiple, valid user-ids. Is looked up via primary user-id 'WKD-Test Primary User-ID ' using mail address 'primary-uid@pgpainless.github.io'.", + "lookupMailAddress" : "primary-uid@pgpainless.github.io", + "certificatePath" : ".well-known/openpgpkey/hu/iz5jxf9oi1mbc1p45s3nxcuxn38qazkw", + "lookupUri" : "https://pgpainless.github.io/.well-known/openpgpkey/hu/iz5jxf9oi1mbc1p45s3nxcuxn38qazkw?l=primary-uid" + }, { + "expectSuccess" : true, + "testTitle" : "Multi-User-ID - Secondary User-ID Lookup", + "testDescription" : "Certificate has multiple, valid user-ids. Is looked up via secondary user-id 'WKD-Test Secondary User-ID ' using mail address 'secondary-uid@pgpainless.github.io'.", + "lookupMailAddress" : "secondary-uid@pgpainless.github.io", + "certificatePath" : ".well-known/openpgpkey/hu/34i6oasjuzeunw5uwam7yqbtit1rtmjp", + "lookupUri" : "https://pgpainless.github.io/.well-known/openpgpkey/hu/34i6oasjuzeunw5uwam7yqbtit1rtmjp?l=secondary-uid" + }, { + "expectSuccess" : false, + "testTitle" : "Secret Key Material", + "testDescription" : "Certificate file contains secret key material.", + "lookupMailAddress" : "test-secret-key@pgpainless.github.io", + "certificatePath" : ".well-known/openpgpkey/hu/4uoqyth19ibwszqjaokiafhxc5sh6usu", + "lookupUri" : "https://pgpainless.github.io/.well-known/openpgpkey/hu/4uoqyth19ibwszqjaokiafhxc5sh6usu?l=test-secret-key" + } ] +} +``` \ No newline at end of file diff --git a/wkd-test-suite/build.gradle b/wkd-test-suite/build.gradle new file mode 100644 index 0000000..9f835b1 --- /dev/null +++ b/wkd-test-suite/build.gradle @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'application' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + // Testing + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Logging + implementation "org.slf4j:slf4j-api:$slf4jVersion" + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + // WKD and OpenPGP + implementation project (":wkd-java") + implementation "org.pgpainless:pgpainless-core:$pgpainlessVersion" + + // CLI + implementation "info.picocli:picocli:4.6.3" + + // Object Mapping + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.1' +} +mainClassName = 'pgp.wkd.test_suite.Main' + +jar { + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) + manifest { + attributes 'Main-Class': "$mainClassName" + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } +} + +test { + useJUnitPlatform() +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/Main.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/Main.java new file mode 100644 index 0000000..deee0de --- /dev/null +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/Main.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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 xmlOutputFiles = new ArrayList<>(); + + @CommandLine.Option(names = "--json-summary", + description = "Write JSON summary to file", + arity = "0..1") + private List 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? + } + } + +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestCase.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestCase.java new file mode 100644 index 0000000..7306256 --- /dev/null +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestCase.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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; + } +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuite.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuite.java new file mode 100644 index 0000000..e0abb44 --- /dev/null +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuite.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.wkd.test_suite; + +import java.util.List; + +public class TestSuite { + + final String version; + final List testCases; + + public TestSuite(String version, List testCases) { + this.version = version; + this.testCases = testCases; + } +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuiteGenerator.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuiteGenerator.java new file mode 100644 index 0000000..28415e0 --- /dev/null +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/TestSuiteGenerator.java @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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 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 "; + 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 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 "; + 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 keys = new ArrayList<>(); + Iterator 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)); + } + +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java new file mode 100644 index 0000000..7c00af7 --- /dev/null +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/WkdDirectoryStructure.java @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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(); + } + } +} diff --git a/wkd-test-suite/src/main/java/pgp/wkd/test_suite/package-info.java b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/package-info.java new file mode 100644 index 0000000..27f0d1e --- /dev/null +++ b/wkd-test-suite/src/main/java/pgp/wkd/test_suite/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * WKD Test Suite Generator. + */ +package pgp.wkd.test_suite; diff --git a/wkd-test-suite/strategy.md b/wkd-test-suite/strategy.md new file mode 100644 index 0000000..5495d6b --- /dev/null +++ b/wkd-test-suite/strategy.md @@ -0,0 +1,31 @@ + + +# WKD Test Suite + +The purpose of the WKD test suite is to generate a set of certificates which can be published to a WKD. +The certificates cover different scenarios and edge cases and can be used to validate WKD implementations experimentally. + +## Test Vectors + +| Test Case | Description | +|-----------------------------------|----------------------------------------------------------------------------------------| +| Base Case | Certificate with a single valid user-id A identified by A | +| Advanced Base Case | Certificate with multiple user-ids A and B identified by A, B | +| Wrong User-ID | Certificate with a single valid user-id A identified by B | +| Missing User-ID | Certificate without a user-id identified by A | +| Unbound User-ID | Certificate with a single unbound user-id A identified by A | +| Expired User-ID | Certificate with a single expired user-id A identified by A | +| Invalidly bound User-ID | Certificate with a single user-id A with broken binding identified by A | +| Revoked User-ID | Certificate with a single revoked user-id A identified by A | +| Revoked Certificate | Certificate with a single user-id A, with direct-key revocation identified by A | +| Third-Party User-ID | Certificate with an additional user-id B certified by third party, identified by B | +| Broken Data | Certificate file contains garbage | +| Secret Key | Certificate file contains secret key material | +| Signatures | Certificate file contains certification signatures only | +| Multiple Certificates | Certificate file contains multiple certificates (with valid user-id A) identified by A | +| Armored Certificate | Certificate file contains armored certificate | +| Duplicate mail address in user-id | Certificate contains user-id of form ".*.*.*" |