Skip to content

Commit

Permalink
Adds fuzzing to POST entries endpoint (#251)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarprudnikov authored Dec 11, 2024
1 parent 69b9f53 commit bfd6592
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 5 deletions.
34 changes: 32 additions & 2 deletions .github/workflows/long-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ jobs:
name: ASAN
if: ${{ contains(github.event.pull_request.labels.*.name, 'run-long-test') || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }}
runs-on: ubuntu-20.04
container:
image: ghcr.io/microsoft/ccf/ci/default:build-08-10-2024
container: ghcr.io/microsoft/ccf/ci/default:build-08-10-2024
env:
# Fast unwinder only gives us partial stack traces in LeakSanitzer
# Alloc/dealloc mismatch has been disabled in CCF: https://github.com/microsoft/CCF/pull/5157
Expand Down Expand Up @@ -53,3 +52,34 @@ jobs:
/tmp/pytest-of-root/*current/*current/*.{out,err}
/tmp/pytest-of-root/*current/*current/config.json
if-no-files-found: warn
fuzz-api:
name: fuzz
if: ${{ contains(github.event.pull_request.labels.*.name, 'run-fuzz-test') || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }}
runs-on: ubuntu-20.04
container: ghcr.io/microsoft/ccf/app/dev/virtual:ccf-6.0.0-dev8
env:
PLATFORM: virtual
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
./build.sh
- name: Fuzz tests
run: |
set +x
./run_fuzz_tests.sh 2>&1 > fuzz.log
echo "Preview test output:"
grep -A 1 -B 20 "Number of tests" fuzz.log
- name: "Upload logs"
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: boofuzz-results
path: |
fuzz.log
if-no-files-found: warn
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ build/
tmp/
out/
venv/
boofuzz-results/
.venv_ccf_sandbox/
workspace/
*.egg-info/
Expand Down
16 changes: 16 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ PLATFORM=virtual CMAKE_BUILD_TYPE=Debug BUILD_CCF_FROM_SOURCE=ON ./build.sh
PLATFORM=virtual ./run_functional_tests.sh
```

### Fuzzing

Run HTTP API fuzzing tests after building the application:

**Using Docker**

```sh
DOCKER=1 ./run_fuzz_tests.sh
```

**Using your host environment**

```sh
./run_fuzz_tests.sh
```

## AMD SEV-SNP platform

To use [AMD SEV-SNP](https://microsoft.github.io/CCF/main/operations/platforms/snp.html) as a platform, it is required to pass additional configuration values required by CCF for the attestation on AMD SEV-SNP hardware. These values may differ depending on which SNP platform you are using (e.g., Confidential Containers on ACI, Confidential Containers on AKS).
Expand Down
4 changes: 2 additions & 2 deletions pyscitt/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from setuptools import find_packages, setup

PACKAGE_NAME = "pyscitt"
PACKAGE_VERSION = "0.7.0"
PACKAGE_VERSION = "0.7.1"

path_here = path.abspath(path.dirname(__file__))

Expand All @@ -26,7 +26,7 @@
python_requires=">=3.8",
install_requires=[
"ccf==6.0.0-dev8",
"cryptography==43.*", # needs to match ccf
"cryptography==44.*", # needs to match ccf
"httpx",
"cbor2==5.4.*",
"pycose==1.1.0",
Expand Down
65 changes: 65 additions & 0 deletions run_fuzz_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

set -ex

DOCKER=${DOCKER:-0}
PLATFORM=virtual

wait_for_service() {
url=$1
timeout=120
while ! curl -s -f -k "$url" > /dev/null; do
echo "Waiting for service to be ready..."
sleep 1
timeout=$((timeout - 1))
if [ $timeout -eq 0 ]; then
echo "Service failed to become ready, exiting"
exit 1
fi
done
}

echo "Setting up python virtual environment."
if [ ! -f "venv/bin/activate" ]; then
python3.8 -m venv "venv"
fi
source venv/bin/activate
pip install --disable-pip-version-check -q -e ./pyscitt
pip install --disable-pip-version-check -q wheel
pip install --disable-pip-version-check -q -r test/requirements.txt

echo "Running fuzz tests..."
export CCF_HOST=${CCF_HOST:-"localhost"}
export CCF_PORT=${CCF_PORT:-8000}
export CCF_URL="https://${CCF_HOST}:${CCF_PORT}"
echo "Service URL: $CCF_URL"

if [ "$DOCKER" = "1" ]; then
echo "Will use a running docker instance for testing..."

PLATFORM=$PLATFORM ./docker/run-dev.sh &
CCF_NETWORK_PID=$!
trap "kill $CCF_NETWORK_PID" EXIT

wait_for_service "$CCF_URL/parameters"
else
echo "Will use a built SCITT binary for testing..."

PLATFORM=$PLATFORM ./start.sh &
# start script will launch cchost process
trap 'pkill -f cchost' EXIT

export CCF_URL="https://localhost:8000"
wait_for_service "$CCF_URL/node/network"

scitt governance local_development \
--url "$CCF_URL" \
--member-key workspace/member0_privk.pem \
--member-cert workspace/member0_cert.pem;

wait_for_service "$CCF_URL/parameters"
fi

python -m test.fuzz_api_submissions
191 changes: 191 additions & 0 deletions test/fuzz_api_submissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import os
import ssl
import time

import boofuzz # type: ignore

do_not_verify_tls = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
do_not_verify_tls.check_hostname = False
do_not_verify_tls.verify_mode = ssl.CERT_NONE
test_time_threshold_sec = 180


def response_must_be_400():

# save current time in python
start_time_sec = time.time()

def checker(target, fuzz_data_logger, session, *args, **kwargs):
is_success = True
try:
response = target.recv(10000)
except:
fuzz_data_logger.log_fail("Unable to connect. Target is down.")
is_success = False

# check response contains a substring foobar
if is_success and b"HTTP/1.1 400 BAD_REQUEST" not in response:
fuzz_data_logger.log_fail(
"Response does not contain 'HTTP/1.1 400 BAD_REQUEST'"
)
fuzz_data_logger.log_fail("Response: {}".format(response))
is_success = False

if time.time() - start_time_sec > test_time_threshold_sec:
fuzz_data_logger.log_info(
"Timeout reached: {} seconds".format(test_time_threshold_sec)
)
fuzz_data_logger.log_info(
"Started at: {} and now is: {}".format(start_time_sec, time.time())
)
session._index_end = (
0 # stop fuzzing https://github.com/jtpereyda/boofuzz/discussions/600
)

return is_success

return checker


def test_fuzz_api_submissions_random_payload():
"""
Generate random payloads and try to register them, each call should return 400 error
"""
session = boofuzz.Session(
target=boofuzz.Target(
connection=boofuzz.SSLSocketConnection(
host="127.0.0.1", port=8000, sslcontext=do_not_verify_tls
)
),
post_test_case_callbacks=[response_must_be_400()],
receive_data_after_each_request=False,
check_data_received_each_request=False,
receive_data_after_fuzz=False,
ignore_connection_issues_when_sending_fuzz_data=False,
ignore_connection_ssl_errors=True,
reuse_target_connection=False,
sleep_time=0.002,
web_port=None,
)

# Create a request variable with fuzzable fields
boofuzz.s_initialize(name="SubmitAny")
with boofuzz.s_block("Request-Line"):
boofuzz.s_static("POST /entries HTTP/1.1\r\n")
boofuzz.s_static(
"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0\r\n"
)
boofuzz.s_static(
"Accept: text/html,application/json;q=0.9,image/webp,*/*;q=0.8\r\n"
)
boofuzz.s_static("Accept-Language: en-US,en;q=0.5\r\n")
boofuzz.s_static("Accept-Encoding: gzip, deflate\r\n")
boofuzz.s_static("Content-Type: application/cose\r\n")

# Add claculated content length
boofuzz.s_static("Content-Length: ", name="Content-Length-Header")
boofuzz.s_size(
"Body-Content",
output_format="ascii",
name="Content-Length-Value",
fuzzable=False,
)
boofuzz.s_static("\r\n", "Content-Length-CRLF")

boofuzz.s_static("\r\n", "Request-CRLF")

# Add a fuzzable payload
with boofuzz.s_block("Body-Content"):
boofuzz.s_delim(b"\xD2", name="COSE tag")
boofuzz.s_delim(b"\x84", name="CBOR array tag")
boofuzz.s_string(
"Body content ...", name="Body-Content-Value", max_len=(1 << 20 - 2)
) # 1MB

test_request = boofuzz.s_get("SubmitAny")
session._fuzz_data_logger.log_info(
"Number of mutations: {}".format(test_request.num_mutations())
)
session.connect(test_request)
session.fuzz(max_depth=2)
session._fuzz_data_logger.log_info(
"Number of tests executed: {}".format(session.num_cases_actually_fuzzed)
)
session._fuzz_data_logger.log_info("Execution speed: {}".format(session.exec_speed))


def test_fuzz_api_submissions_cose_payload():
"""
Randomise parts of the cose envelope and do a successfull submission
"""
current_dir = os.path.dirname(__file__)

session = boofuzz.Session(
target=boofuzz.Target(
connection=boofuzz.SSLSocketConnection(
host="127.0.0.1", port=8000, sslcontext=do_not_verify_tls
)
),
post_test_case_callbacks=[response_must_be_400()],
receive_data_after_each_request=False,
check_data_received_each_request=False,
receive_data_after_fuzz=False,
ignore_connection_issues_when_sending_fuzz_data=False,
ignore_connection_ssl_errors=True,
reuse_target_connection=False,
sleep_time=0.002,
web_port=None,
)

# Create a request variable with fuzzable fields
boofuzz.s_initialize(name="SubmitCose")
with boofuzz.s_block("Request-Line"):
boofuzz.s_static("POST /entries HTTP/1.1\r\n")
boofuzz.s_static(
"User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0\r\n"
)
boofuzz.s_static(
"Accept: text/html,application/json;q=0.9,image/webp,*/*;q=0.8\r\n"
)
boofuzz.s_static("Accept-Language: en-US,en;q=0.5\r\n")
boofuzz.s_static("Accept-Encoding: gzip, deflate\r\n")
boofuzz.s_static("Content-Type: application/cose\r\n")
# Add claculated content length
boofuzz.s_static("Content-Length: ", name="Content-Length-Header")
boofuzz.s_size(
"Body-Content",
output_format="ascii",
name="Content-Length-Value",
fuzzable=False,
)
boofuzz.s_static("\r\n", "Content-Length-CRLF")
boofuzz.s_static("\r\n", "Request-CRLF")

filepath = os.path.join(current_dir, "payloads/cts-hashv-cwtclaims-b64url.cose")
session._fuzz_data_logger.log_info(
"Seeding test cose file for fuzzing: {}".format(filepath)
)
# Add a fuzzable payload
with boofuzz.s_block("Body-Content"):
boofuzz.s_from_file(
filename=filepath, name="Body-Content-Value", fuzzable=True, max_len=1 << 20
)

test_request = boofuzz.s_get("SubmitCose")
session._fuzz_data_logger.log_info(
"Number of mutations: {}".format(test_request.num_mutations())
)
session.connect(test_request)
session.fuzz(max_depth=2)
session._fuzz_data_logger.log_info(
"Number of tests executed: {}".format(session.num_cases_actually_fuzzed)
)
session._fuzz_data_logger.log_info("Execution speed: {}".format(session.exec_speed))


if __name__ == "__main__":
test_fuzz_api_submissions_random_payload()
test_fuzz_api_submissions_cose_payload()
3 changes: 2 additions & 1 deletion test/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
boofuzz
locust
httpx
pytest
loguru
aiotools
ccf==6.0.0-dev8
cryptography==43.*
cryptography==44.*

0 comments on commit bfd6592

Please sign in to comment.