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: