This commit is contained in:
Paul Schaub 2024-04-14 22:01:45 +05:30 committed by GitHub
commit 4f434dc350
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 458 additions and 1 deletions

View File

@ -188,6 +188,9 @@ SPDX-License-Identifier: CC0-1.0
- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable
- Countermeasures are now disabled by default since they are costly and have a specific threat model
- Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)`
- Add support for parsing RegularExpressions
- Add module `hsregex` which uses `tcl-regex-java` implementing Henry Spencers Regular Expression library
## 1.4.0-rc2
- Bump `bcpg-jdk15to18` to `1.72.3`

18
hsregex/README.md Normal file
View File

@ -0,0 +1,18 @@
<!--
SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
SPDX-License-Identifier: Apache-2.0
-->
# Evaluate Regular Expressions in OpenPGP Signatures using TCL-Regex
RFC4880 specifies contains a section about RegularExpression subpackets on signatures.
Within this section, the syntax of the RegularExpression subpackets is defined to be the same as Henry Spencer's "almost public domain" regular expression package.
Since Java's `java.util.regex` syntax is too powerful, this module exists to implement regex evaluation using [tcl-regex](https://github.com/basis-technology-corp/tcl-regex-java)
which appears to be a Java port of Henry Spencers regex package.
To make use of this implementation, simply call
```java
RegexInterpreterFactory.setInstance(new HSRegexInterpreterFactory());
```

28
hsregex/build.gradle Normal file
View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
plugins {
id 'java-library'
}
group 'org.pgpainless'
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
implementation(project(":pgpainless-core"))
// Henry Spencers Regular Expression (RegEx packets)
implementation 'com.basistech.tclre:tcl-regex:0.14.5'
}
test {
useJUnitPlatform()
}

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import com.basistech.tclre.HsrePattern;
import com.basistech.tclre.PatternFlags;
import com.basistech.tclre.RePattern;
import com.basistech.tclre.RegexException;
public class HSRegexInterpreterFactory extends RegexInterpreterFactory {
public Regex instantiate(String regex) {
return new Regex() {
private final RePattern pattern;
{
try {
pattern = HsrePattern.compile(regex, PatternFlags.ADVANCED);
} catch (RegexException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public boolean matches(String string) {
return pattern.matcher(string).find();
}
};
}
}

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Regex interpreter implementation based on Henry Spencers Regular Expression library.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-8">RFC4880 - §8. Regular Expressions</a>
*/
package org.pgpainless.algorithm;

View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HSRegexInterpreterFactoryTest {
@Test
public void dummyRegexTest() {
HSRegexInterpreterFactory factory = new HSRegexInterpreterFactory();
RegexInterpreterFactory.setInstance(factory);
Regex regex = RegexInterpreterFactory.create("Alice|Bob");
assertTrue(regex.matches("Alice"));
assertTrue(regex.matches("Bob"));
assertFalse(regex.matches("Charlie"));
}
}

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Regex interpreter implementation based on Henry Spencers Regular Expression library.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-8">RFC4880 - §8. Regular Expressions</a>
*/
package org.pgpainless.algorithm;

View File

@ -26,6 +26,9 @@ dependencies {
// @Nullable, @Nonnull annotations
implementation "com.google.code.findbugs:jsr305:3.0.2"
// HSRE regex for tests
testImplementation project(":hsregex")
}
// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.pgpainless.key.util.UserId;
public interface Regex {
/**
* Return true, if the regex matches the given user-id.
*
* @param userId userId
* @return true if matches, false otherwise
*/
default boolean matches(UserId userId) {
return matches(userId.toString());
}
/**
* Return true, if the regex matches the given string.
*
* @param string string
* @return true if matches, false otherwise
*/
boolean matches(String string);
static Regex wildcard() {
return new Regex() {
@Override
public boolean matches(String string) {
return true;
}
};
}
}

View File

@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
public abstract class RegexInterpreterFactory {
private static RegexInterpreterFactory INSTANCE;
public static RegexInterpreterFactory getInstance() {
if (INSTANCE == null) {
INSTANCE = new JavaRegexInterpreterFactory();
}
return INSTANCE;
}
public static void setInstance(@Nonnull RegexInterpreterFactory instance) {
INSTANCE = instance;
}
public static Regex create(String regex) {
return getInstance().instantiate(regex);
}
/**
* Regex that matches any mail address on the given mail server.
* For example, calling this method with parameter <pre>pgpainless.org</pre> will return a regex
* that matches any of the following user ids:
* <pre>
* Alice <alice@pgpainless.org>
* <bob@pgpainless.org>
* Issuer (code signing) <issuer@pgpainless.org>
* </pre>
* It will however not match the following mail addresses:
* <pre>
* Alice <alice@example.org>
* alice@pgpainless.org
* alice@pgpainless.org <alice@example.org>
* Bob <bob@PGPainless.org>
* </pre>
* Note: This method will not validate the given domain string, so that is your responsibility!
*
* @param mailDomain domain
* @return regex matching the domain
*/
public static Regex createDefaultMailDomainRegex(String mailDomain) {
String escaped = mailDomain.replace(".", "\\.");
return create("<[^>]+[@.]" + escaped + ">$");
}
public abstract Regex instantiate(String regex) throws IllegalArgumentException;
public static class JavaRegexInterpreterFactory extends RegexInterpreterFactory {
@Override
public Regex instantiate(String regex) {
return new Regex() {
private final Pattern pattern = Pattern.compile(regex);
@Override
public boolean matches(String string) {
return pattern.matcher(string).find();
}
};
}
}
}

View File

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public final class RegexSet {
private final Set<Regex> regexSet = new HashSet<>();
private RegexSet(Collection<Regex> regexes) {
this.regexSet.addAll(regexes);
}
public static RegexSet matchAnything() {
return new RegexSet(Collections.singleton(Regex.wildcard()));
}
public static RegexSet matchNothing() {
return new RegexSet(Collections.emptySet());
}
public static RegexSet matchSome(Collection<Regex> regexes) {
return new RegexSet(regexes);
}
public boolean matches(String userId) {
for (Regex regex : regexSet) {
if (regex.matches(userId)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
public class RegexSetTest {
@Test
public void matchNothingTest() {
RegexSet set = RegexSet.matchNothing();
assertFalse(set.matches("<alice@pgpainless.org>"));
assertFalse(set.matches("Alice"));
assertFalse(set.matches("Alice <alice@pgpainless.org>"));
assertFalse(set.matches(""));
}
@Test
public void matchAnything() {
RegexSet set = RegexSet.matchAnything();
assertTrue(set.matches("Alice"));
assertTrue(set.matches("<alice@pgpainless.org>"));
assertTrue(set.matches("Alice <alice@pgpainless.org>"));
assertTrue(set.matches("Alice <alice@example.org>"));
assertTrue(set.matches(""));
}
@Test
public void matchSome() {
Regex pgpainless_org = RegexInterpreterFactory.createDefaultMailDomainRegex("pgpainless.org");
Regex example_org = RegexInterpreterFactory.createDefaultMailDomainRegex("example.org");
RegexSet set = RegexSet.matchSome(Arrays.asList(pgpainless_org, example_org));
assertTrue(set.matches("Alice <alice@pgpainless.org>"));
assertTrue(set.matches("<alice@pgpainless.org>"));
assertTrue(set.matches("Bob <bob@example.org>"));
assertTrue(set.matches("<bob@example.org>"));
assertFalse(set.matches("Bob <bob@example.com>"));
assertFalse(set.matches("Alice <alice@PGPainless.org>"));
assertFalse(set.matches("alice@pgpainless.org"));
assertFalse(set.matches("Alice"));
assertFalse(set.matches(""));
}
}

View File

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.stream.Stream;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.pgpainless.key.util.UserId;
public class RegexTest {
private static Stream<Arguments> provideRegexInterpreterFactories() {
return Stream.of(
Arguments.of(Named.of("Default JavaRegexInterpreterFactory",
new RegexInterpreterFactory.JavaRegexInterpreterFactory())),
Arguments.of(Named.of("HSRegexInterpreterFactory",
new HSRegexInterpreterFactory()))
);
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void simpleTest(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate("Alice|Bob");
assertTrue(regex.matches("Alice"));
assertTrue(regex.matches("Bob"));
assertFalse(regex.matches("Charlie"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testEmailRegexMatchesDomain(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
assertTrue(regex.matches("Alice <alice@pgpainless.org>"));
assertTrue(regex.matches("Bob <bob@pgpainless.org>"));
assertFalse(regex.matches("Alice <alice@example.com>"), "wrong domain");
assertFalse(regex.matches("Bob <bob@example.com>"), "wrong domain");
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testEmailRegexMatchesOnlyWrappedAddresses(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
assertTrue(regex.matches("<alice@pgpainless.org>"));
assertFalse(regex.matches("alice@pgpainless.org"), "only match mails in <>");
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testCaseSensitivity(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
assertFalse(regex.matches("Alice <alice@PGPAINLESS.ORG>"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testWildCard(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate(".*");
assertTrue(regex.matches(""));
assertTrue(regex.matches("Alice"));
assertTrue(regex.matches("<alice@pgpainless.org>"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testExclusion(RegexInterpreterFactory factory) {
// Test [^>] matches all but '>'
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
assertFalse(regex.matches("<alice>appendix@pgpainless.org>"));
assertFalse(regex.matches("<>alice@pgpainless.org>"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testOnlyMatchAtTheEnd(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
assertFalse(regex.matches("Alice <alice@pgpainless.org><bob@example.org>"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testRanges(RegexInterpreterFactory factory) {
Regex regex = factory.instantiate("<[^>]+[0-9][@.]pgpainless\\.org>$");
for (int i = 0; i < 10; i++) {
String mail = "<user" + i + "@pgpainless.org>";
assertTrue(regex.matches(mail));
}
assertFalse(regex.matches("<user@pgpainless.org>"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testExactMailMatch(RegexInterpreterFactory factory) {
Regex exactMail = factory.instantiate("<exact@pgpainless\\.org>$");
assertTrue(exactMail.matches("<exact@pgpainless.org>"));
assertTrue(exactMail.matches("Exact Match <exact@pgpainless.org>"));
assertFalse(exactMail.matches("<roughly-exact@pgpainless.org>"));
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testSetInstance(RegexInterpreterFactory factory) {
RegexInterpreterFactory before = RegexInterpreterFactory.getInstance();
RegexInterpreterFactory.setInstance(factory);
Regex regex = RegexInterpreterFactory.create("<[^>]+[@.]pgpainless\\.org>$");
assertTrue(regex.matches(UserId.nameAndEmail("Alice", "alice@pgpainless.org")));
RegexInterpreterFactory.setInstance(before);
}
@ParameterizedTest
@MethodSource("provideRegexInterpreterFactories")
public void testInvalidRegex(RegexInterpreterFactory factory) {
assertThrows(IllegalArgumentException.class, () -> factory.instantiate("[ab"));
}
}

View File

@ -6,5 +6,6 @@ rootProject.name = 'PGPainless'
include 'pgpainless-core',
'pgpainless-sop',
'pgpainless-cli'
'pgpainless-cli',
'hsregex'