diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh
index 63d1d72b..e148f854 100755
--- a/.clusterfuzzlite/build.sh
+++ b/.clusterfuzzlite/build.sh
@@ -40,7 +40,7 @@ cp fernet-fuzzer/target/fernet-fuzzer-*.jar "${OUT}/fernet-fuzzer.jar"
RUNTIME_CLASSPATH="\${this_dir}/fernet-java8.jar:\${this_dir}/fernet-fuzzer.jar"
-fuzzers="TokenEncryptDecryptFuzzer TokenDecryptFuzzer"
+fuzzers="TokenEncryptDecryptFuzzer TokenDecryptFuzzer TokenReplayFuzzer PayloadPaddingFuzzer"
echo "$fuzzers" | tr ' ' '\n' | while read -r fuzzer
do
cp "${SRC}/default.options" "${OUT}/${fuzzer}.options"
diff --git a/.clusterfuzzlite/fernet-fuzzer/pom.xml b/.clusterfuzzlite/fernet-fuzzer/pom.xml
index f88281a3..a8236f04 100644
--- a/.clusterfuzzlite/fernet-fuzzer/pom.xml
+++ b/.clusterfuzzlite/fernet-fuzzer/pom.xml
@@ -33,12 +33,22 @@
1.5.0
-
+
+
+
+ org.mockito
+ mockito-bom
+ 4.6.1
+ pom
+ import
+
+
+
com.macasaet.fernet
fernet-java8
- ${fernet.version}
+ [${fernet.version}]
com.code-intelligence
@@ -46,6 +56,21 @@
0.11.0
provided
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.2
+ test
+
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/main/java/PayloadPaddingFuzzer.java b/.clusterfuzzlite/fernet-fuzzer/src/main/java/PayloadPaddingFuzzer.java
new file mode 100644
index 00000000..eb0e84bb
--- /dev/null
+++ b/.clusterfuzzlite/fernet-fuzzer/src/main/java/PayloadPaddingFuzzer.java
@@ -0,0 +1,99 @@
+/*
+ Copyright 2022 Carlos Macasaet
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.function.Predicate;
+import javax.crypto.spec.IvParameterSpec;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
+import com.macasaet.fernet.*;
+
+/**
+ * This fuzzer simulates an attacker padding the content of a token
+ */
+public class PayloadPaddingFuzzer {
+
+ /**
+ * Freeze time for the fuzzer. In practice, the clock will return a different instant every second.
+ */
+ final static Clock clock = Clock.fixed(Instant.ofEpochSecond(581182474), ZoneId.of("UTC"));
+ /**
+ * Run each fuzz input against the same key. Note that in practice, the key is likely rotated on a regular basis.
+ */
+ final static Key key = new Key("UrNImCIJQuYODgrBU5NgH5rpTc7l52IS5ELuhwF4RHU=");
+ final static Validator validator = new StringValidator() {
+ public Charset getCharset() {
+ return StandardCharsets.UTF_8;
+ }
+
+ public Clock getClock() {
+ return clock;
+ }
+
+ public Predicate getObjectValidator() {
+ return candidate -> {
+ final var components = candidate.split("&");
+ final var map = new HashMap(components.length);
+ for (final var component : components) {
+ final var pair = component.split("=");
+ map.put(pair[0], pair[1]);
+ }
+ return map.containsKey("id") && map.containsKey("username");
+ };
+ }
+ };
+ final static Utility utility = new Utility();
+
+ public static void fuzzerTestOneInput(final FuzzedDataProvider data) {
+ // retrieve a valid token from the server
+ final var version = (byte) 0x80;
+ final var timestamp = clock.instant();
+ final var initializationVector = new IvParameterSpec(Base64.getUrlDecoder().decode("7x-FMghmHjn-6lVUKCsN-A=="));
+ final var id = UUID.fromString("5c8293ac-6c70-4be0-823b-6ec391fc164b");
+ final var username = "alice";
+ final var plain = "id=" + id + "&username=" + username;
+ final var cipherText = key.encrypt(plain.getBytes(StandardCharsets.UTF_8), initializationVector);
+ final var signature = key.sign(version, timestamp, initializationVector, cipherText);
+ final var validToken = new Token(version, timestamp, initializationVector, cipherText, signature) {
+ };
+ validator.validateAndDecrypt(key, validToken);
+
+ // tamper with the token
+ final var forgedTimestamp = timestamp.plusSeconds(data.consumeLong(0, 60));
+ final var forgedInitializationVector = new IvParameterSpec(utility.consumeBytes(data, 16));
+ final var forgedCipherText = data.consumeBytes(cipherText.length * 2);
+ final var forgedSignature = utility.consumeBytes(data, signature.length);
+ final var forgedToken = new Token(version, forgedTimestamp, forgedInitializationVector, forgedCipherText, forgedSignature) {
+ };
+ try {
+ final var result = validator.validateAndDecrypt(key, forgedToken);
+ if (result.length() > plain.length() && result.startsWith(plain)) {
+ throw new FuzzerSecurityIssueHigh("Able to pad a token with a malicious payload");
+ }
+ throw new FuzzerSecurityIssueHigh("Able to forge a token");
+ } catch (final TokenValidationException ignored) {
+ }
+ }
+
+}
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenDecryptFuzzer.java b/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenDecryptFuzzer.java
index 5f9fa99b..ad5f313d 100644
--- a/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenDecryptFuzzer.java
+++ b/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenDecryptFuzzer.java
@@ -14,13 +14,16 @@
limitations under the License.
*/
+import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
-import java.util.function.Function;
+import java.time.ZoneId;
+import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.spec.IvParameterSpec;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
import com.macasaet.fernet.Key;
import com.macasaet.fernet.Token;
import com.macasaet.fernet.TokenValidationException;
@@ -32,31 +35,36 @@
*/
public class TokenDecryptFuzzer {
+ /**
+ * Freeze time for the fuzzer. In practice, the clock will return a different instant every second.
+ */
+ final static Clock clock = Clock.fixed(Instant.ofEpochSecond(581182474), ZoneId.of("UTC"));
/*
* Run each fuzz input against the same key. Note that in practice, the key is likely rotated on a regular basis.
*/
final static Key key = new Key("UrNImCIJQuYODgrBU5NgH5rpTc7l52IS5ELuhwF4RHU=");
- final static Validator validator = () -> Function.identity();
+ final static Validator validator = new UuidValidator(clock);
+ final static Utility utility = new Utility();
public static void fuzzerTestOneInput(final FuzzedDataProvider data) {
- final var ivBytes = new byte[16];
- for (int i = ivBytes.length; --i >= 0; ivBytes[i] = data.consumeByte()) ;
- final var initializationVector = new IvParameterSpec(ivBytes);
- final var cipherTextLength = data.consumeInt(1, 4096) * 16;
- final var cipherText = new byte[cipherTextLength];
- for (int i = cipherTextLength; --i >= 0; cipherText[i] = data.consumeByte()) ;
- final var signature = new byte[32];
- for (int i = signature.length; --i >= 0; signature[i] = data.consumeByte()) ;
- final var timestamp = Instant.now().plus(Duration.ofSeconds(data.consumeLong(-60, 60)));
- final var token = new Token((byte) -128, timestamp, initializationVector, cipherText, signature) {
+ final var initializationVector = new IvParameterSpec(utility.consumeBytes(data, 16));
+ final var cipherText = utility.consumeBytes(data, 32); // random payload the size of an encrypted UUID
+ final var signature = utility.consumeBytes(data, 32); // random signature of the right size
+ final var timestamp = clock.instant().plus(Duration.ofSeconds(data.consumeLong(-60, 60)));
+ // generate the shape of a valid token without knowing the encryption or signing key
+ final var token = new Token((byte) 0x80, timestamp, initializationVector, cipherText, signature) {
};
+
try {
- token.validateAndDecrypt(key, validator);
- throw new IllegalStateException("Random input passed validation");
+ final var result = token.validateAndDecrypt(key, validator);
+ throw new FuzzerSecurityIssueHigh("Random input passed validation and generated UUID: " + result.toString());
} catch (final TokenValidationException tve) {
- if(tve.getCause() instanceof BadPaddingException) {
- throw new IllegalStateException("Random input forged signature");
+ if (tve.getCause() instanceof BadPaddingException) {
+ throw new FuzzerSecurityIssueHigh("Random input forged signature: " + tve.getCause().getMessage(), tve.getCause());
}
}
}
+
+
+
}
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenEncryptDecryptFuzzer.java b/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenEncryptDecryptFuzzer.java
index 62129104..719d2504 100644
--- a/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenEncryptDecryptFuzzer.java
+++ b/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenEncryptDecryptFuzzer.java
@@ -13,10 +13,16 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-import java.util.Arrays;
-import java.util.function.Function;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.UUID;
+import javax.crypto.spec.IvParameterSpec;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
import com.macasaet.fernet.Key;
import com.macasaet.fernet.Token;
import com.macasaet.fernet.Validator;
@@ -26,20 +32,38 @@
*/
public class TokenEncryptDecryptFuzzer {
+ /**
+ * Freeze time for the fuzzer. In practice, the clock will return a different instant every second.
+ */
+ final static Clock clock = Clock.fixed(Instant.ofEpochSecond(581182474), ZoneId.of("UTC"));
/*
* Run each fuzz input against the same key. Note that in practice, the key is likely rotated on a regular basis.
*/
final static Key key = new Key("UrNImCIJQuYODgrBU5NgH5rpTc7l52IS5ELuhwF4RHU=");
- final static Validator validator = () -> Function.identity();
+ final static Validator validator = new UuidValidator(clock);
+ final static Utility utility = new Utility();
public static void fuzzerTestOneInput(final FuzzedDataProvider data) {
- final var payload = data.consumeBytes(4096);
- final var token = Token.generate(key, payload);
+ final var version = (byte) 0x80;
+ final var timestamp = clock.instant().plus(Duration.ofSeconds(data.consumeLong(-60, 60)));
+ final var initializationVector = new IvParameterSpec(utility.consumeBytes(data, 16));
+ final var idBytes = utility.consumeBytes(data, 16);
+ // copied from JDK
+ idBytes[6] &= 0x0f; /* clear version */
+ idBytes[6] |= 0x40; /* set to version 4 */
+ idBytes[8] &= 0x3f; /* clear variant */
+ idBytes[8] |= 0x80; /* set to IETF variant */
+ final var cipherText = key.encrypt(idBytes, initializationVector);
+ final var signature = key.sign(version, timestamp, initializationVector, cipherText);
+
+ final var token = new Token(version, timestamp, initializationVector, cipherText, signature) {
+ };
final var serialised = token.serialise();
final var deserialised = Token.fromString(serialised);
final var decrypted = deserialised.validateAndDecrypt(key, validator);
- if (!Arrays.equals(payload, decrypted)) {
- throw new IllegalStateException("Encryption/decryption fault");
+ if (!validator.getTransformer().apply(idBytes).equals(decrypted)) {
+ throw new FuzzerSecurityIssueHigh("Encryption/decryption fault");
}
}
+
}
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenReplayFuzzer.java b/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenReplayFuzzer.java
new file mode 100644
index 00000000..a182e65c
--- /dev/null
+++ b/.clusterfuzzlite/fernet-fuzzer/src/main/java/TokenReplayFuzzer.java
@@ -0,0 +1,86 @@
+/*
+ Copyright 2022 Carlos Macasaet
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.UUID;
+import javax.crypto.spec.IvParameterSpec;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh;
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium;
+import com.macasaet.fernet.Key;
+import com.macasaet.fernet.Token;
+import com.macasaet.fernet.TokenValidationException;
+import com.macasaet.fernet.Validator;
+
+/**
+ * This fuzzer simulates a replay attack
+ */
+public class TokenReplayFuzzer {
+
+ /**
+ * Freeze time for the fuzzer. In practice, the clock will return a different instant every second.
+ */
+ static Clock clock = Clock.fixed(Instant.ofEpochSecond(581182474), ZoneId.of("UTC"));
+ /*
+ * Run each fuzz input against the same key. Note that in practice, the key is likely rotated on a regular basis.
+ */
+ final static Key key = new Key("UrNImCIJQuYODgrBU5NgH5rpTc7l52IS5ELuhwF4RHU=");
+ final static Validator validator = new UuidValidator(clock) {
+
+ public Clock getClock() {
+ return clock;
+ }
+ };
+ final static Utility utility = new Utility();
+
+ public static void fuzzerTestOneInput(final FuzzedDataProvider data) {
+ // retrieve a valid token from the server
+ final var version = (byte) 0x80;
+ final var timestamp = clock.instant();
+ final var initializationVector = new IvParameterSpec(Base64.getUrlDecoder().decode("7x-FMghmHjn-6lVUKCsN-A=="));
+ final var id = UUID.fromString("5c8293ac-6c70-4be0-823b-6ec391fc164b");
+ final var idBytes = utility.toBytes(id);
+ final var cipherText = key.encrypt(idBytes, initializationVector);
+ final var signature = key.sign(version, timestamp, initializationVector, cipherText);
+ final var validToken = new Token(version, timestamp, initializationVector, cipherText, signature) {
+ };
+ validator.validateAndDecrypt(key, validToken);
+
+ // fast-forward time
+ clock = Clock.fixed(clock.instant().plus(4, ChronoUnit.HOURS), ZoneId.of("UTC"));
+
+ // replay the expired token
+ final var forgedTimestamp = timestamp.plus(4, ChronoUnit.HOURS).plusSeconds(data.consumeLong(-60, 60));
+ final var forgedSignature = utility.consumeBytes(data, 32);
+ final var forgedToken = new Token(version, forgedTimestamp, initializationVector, cipherText, forgedSignature) {
+ };
+
+ try {
+ final var forgedId = validator.validateAndDecrypt(key, forgedToken);
+ if (forgedId.equals(id)) {
+ throw new FuzzerSecurityIssueHigh("Fuzz input replayed a token");
+ }
+ throw new FuzzerSecurityIssueMedium("Fuzz input forged a signature with a different timestamp");
+ } catch (final TokenValidationException ignored) {
+ }
+ }
+
+}
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/main/java/Utility.java b/.clusterfuzzlite/fernet-fuzzer/src/main/java/Utility.java
new file mode 100644
index 00000000..bfc278d7
--- /dev/null
+++ b/.clusterfuzzlite/fernet-fuzzer/src/main/java/Utility.java
@@ -0,0 +1,55 @@
+/*
+ Copyright 2022 Carlos Macasaet
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.UUID;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+class Utility {
+
+ /**
+ * @param data the fuzzing input source
+ * @param numBytes the _exact_ number of bytes to consume
+ * @return an array of exactly numBytes length
+ */
+ public byte[] consumeBytes(final FuzzedDataProvider data, final int numBytes) {
+ var result = new byte[0];
+ while (result.length < numBytes) {
+ final var bytes = data.consumeBytes(numBytes - result.length);
+ final var temp = new byte[result.length + bytes.length];
+ System.arraycopy(result, 0, temp, 0, result.length);
+ System.arraycopy(bytes, 0, temp, result.length, bytes.length);
+ result = temp;
+ }
+ return result;
+ }
+
+ public byte[] toBytes(final UUID uuid) {
+ try(var output = new ByteArrayOutputStream()) {
+ try(var data = new DataOutputStream(output)) {
+ data.writeLong(uuid.getMostSignificantBits());
+ data.writeLong(uuid.getLeastSignificantBits());
+ return output.toByteArray();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/main/java/UuidValidator.java b/.clusterfuzzlite/fernet-fuzzer/src/main/java/UuidValidator.java
new file mode 100644
index 00000000..585c689f
--- /dev/null
+++ b/.clusterfuzzlite/fernet-fuzzer/src/main/java/UuidValidator.java
@@ -0,0 +1,60 @@
+/*
+ Copyright 2022 Carlos Macasaet
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+
+import java.time.Clock;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import com.macasaet.fernet.Validator;
+
+class UuidValidator implements Validator {
+
+ private final Clock clock;
+
+ public UuidValidator(final Clock clock) {
+ this.clock = clock;
+ }
+
+ public Clock getClock() {
+ return clock;
+ }
+
+ public Function getTransformer() {
+ return bytes -> {
+ if (bytes.length != 16) {
+ throw new IllegalArgumentException("Invalid UUID");
+ }
+ long mostSignificantBits = 0;
+ for (int i = 0; i < 8; i++) {
+ mostSignificantBits = (mostSignificantBits << 8) | (bytes[i] & 0xff);
+ }
+
+ long leastSignificantBits = 0;
+ for (int i = 8; i < 16; i++) {
+ leastSignificantBits = (leastSignificantBits << 8) | (bytes[i] & 0xff);
+ }
+
+ return new UUID(mostSignificantBits, leastSignificantBits);
+ };
+ }
+
+ public Predicate getObjectValidator() {
+ return id -> id.version() == 4 && id.variant() == 2;
+ }
+
+}
diff --git a/.clusterfuzzlite/fernet-fuzzer/src/test/java/FuzzerTest.java b/.clusterfuzzlite/fernet-fuzzer/src/test/java/FuzzerTest.java
new file mode 100644
index 00000000..38734228
--- /dev/null
+++ b/.clusterfuzzlite/fernet-fuzzer/src/test/java/FuzzerTest.java
@@ -0,0 +1,70 @@
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.BDDMockito.given;
+
+import java.util.Random;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class FuzzerTest {
+
+ private final Random random = new Random();
+ @Mock
+ private FuzzedDataProvider data;
+
+ @BeforeEach
+ public void setUp() {
+ given(data.consumeBytes(anyInt())).willAnswer(invocation -> {
+ final int maxLength = invocation.getArgument(0);
+ final var bytes = new byte[maxLength];
+ random.nextBytes(bytes);
+ return bytes;
+ });
+ }
+
+ @Test
+ public final void decrypt() {
+ // given
+
+ // when
+ TokenDecryptFuzzer.fuzzerTestOneInput(data);
+
+ // then (no exception)
+ }
+
+ @Test
+ public final void encryptDecrypt() {
+ // given
+
+ // when
+ TokenEncryptDecryptFuzzer.fuzzerTestOneInput(data);
+
+ // then (no exception)
+ }
+
+ @Test
+ public final void replay() {
+ // given
+
+ // when
+ TokenReplayFuzzer.fuzzerTestOneInput(data);
+
+ // then (no exception)
+ }
+
+ @Test
+ public final void pad() {
+ // given
+
+ // when
+ PayloadPaddingFuzzer.fuzzerTestOneInput(data);
+
+ // then (no exception)
+ }
+
+}
diff --git a/.github/workflows/fuzz_pr.yml b/.github/workflows/fuzz_pr.yml
index c8570009..735137d5 100644
--- a/.github/workflows/fuzz_pr.yml
+++ b/.github/workflows/fuzz_pr.yml
@@ -16,8 +16,7 @@ name: PR Fuzzing
on:
pull_request:
paths:
- #- '**/src/main/java/**'
- - '**' # temporary for first commit
+ - '**/src/main/java/**'
permissions: read-all
jobs:
PR: