Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/api fuzzing #251

Merged
merged 20 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
ivarprudnikov marked this conversation as resolved.
Show resolved Hide resolved
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")
ivarprudnikov marked this conversation as resolved.
Show resolved Hide resolved
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.*
Loading