Merge 6a4ea5fbdb
into dd3ef89a5c
This commit is contained in:
commit
4f434dc350
|
@ -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`
|
||||
|
|
|
@ -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());
|
||||
```
|
|
@ -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()
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(""));
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -6,5 +6,6 @@ rootProject.name = 'PGPainless'
|
|||
|
||||
include 'pgpainless-core',
|
||||
'pgpainless-sop',
|
||||
'pgpainless-cli'
|
||||
'pgpainless-cli',
|
||||
'hsregex'
|
||||
|
||||
|
|
Loading…
Reference in New Issue