Skip to content
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
33 changes: 17 additions & 16 deletions .github/workflows/python-integ.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ on:
workflow_call:
inputs:
python-version:
description: "Python version to use"
description: "Python version to use (ignored when matrix is used)"
default: "3.11"
required: false
type: string

jobs:
python-integ:
runs-on: macos-14-large
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
permissions:
id-token: write
contents: read
Expand All @@ -25,14 +30,16 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python-version || '3.11' }}

python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: Cache uv dependencies
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: ${{ runner.os }}-uv-${{ hashFiles('./test-server/python-v4-server/**/pyproject.toml') }}
key: ${{ runner.os }}-uv-py${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-uv-py${{ matrix.python-version }}-
${{ runner.os }}-uv-

- name: Install Uv
Expand All @@ -48,16 +55,10 @@ jobs:
aws-region: us-west-2

- name: Run unit tests
run: |
uv run pytest test/ --ignore=test/integration/ --ignore=test/performance/ --verbose \
--cov=src/s3_encryption --cov-report=term-missing --cov-report=html:coverage-unit \
--cov-fail-under=89
run: uv run pytest test/ --ignore=test/integration/ --ignore=test/performance/ --verbose --cov=src/s3_encryption --cov-report=term-missing --cov-report=html:coverage-unit --cov-fail-under=89

- name: Run integration tests
run: |
uv run pytest test/integration/ --verbose \
--cov=src/s3_encryption --cov-report=term-missing --cov-report=html:coverage-integ \
--cov-fail-under=83
run: uv run pytest test/integration/ --verbose --cov=src/s3_encryption --cov-report=term-missing --cov-report=html:coverage-integ --cov-fail-under=83
env:
CI_S3_BUCKET: ${{ vars.CI_S3_BUCKET }}
CI_KMS_KEY_ALIAS: ${{ vars.CI_KMS_KEY_ALIAS }}
Expand All @@ -67,16 +68,16 @@ jobs:
- name: Run examples
run: make test-examples

- name: Generate coverage HTML report
- name: Upload unit test coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-unit
name: coverage-unit-py${{ matrix.python-version }}-${{ matrix.os }}
path: coverage-unit/

- name: Upload integration test coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-integ
name: coverage-integ-py${{ matrix.python-version }}-${{ matrix.os }}
path: coverage-integ/
10 changes: 6 additions & 4 deletions examples/test/test_i_delayed_auth_streaming_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Test suite for the delayed auth streaming decrypt example."""

import uuid

import boto3
import pytest

Expand All @@ -9,19 +12,18 @@
pytestmark = [pytest.mark.examples]

BUCKET = "s3ec-python-github-test-bucket"
KEY = "examples/delayed-auth-streaming"
KMS_KEY_ID = "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key"


def test_delayed_auth_streaming_decrypt():
key = f"examples/delayed-auth-streaming-{uuid.uuid4()}"
s3_client = boto3.client("s3", region_name="us-west-2")
kms_client = boto3.client("kms", region_name="us-west-2")
delayed_auth_streaming_decrypt(
s3_client=s3_client,
kms_client=kms_client,
kms_key_id=KMS_KEY_ID,
bucket=BUCKET,
key=KEY,
key=key,
)
# Clean up
s3_client.delete_object(Bucket=BUCKET, Key=KEY)
s3_client.delete_object(Bucket=BUCKET, Key=key)
10 changes: 6 additions & 4 deletions examples/test/test_i_kms_keyring_put_get_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Test suite for the KMS Keyring put/get example."""

import uuid

import boto3
import pytest

Expand All @@ -9,19 +12,18 @@
pytestmark = [pytest.mark.examples]

BUCKET = "s3ec-python-github-test-bucket"
KEY = "examples/kms-keyring-put-get"
KMS_KEY_ID = "arn:aws:kms:us-west-2:370957321024:alias/S3EC-Python-Github-KMS-Key"


def test_kms_keyring_put_get():
key = f"examples/kms-keyring-put-get-{uuid.uuid4()}"
s3_client = boto3.client("s3", region_name="us-west-2")
kms_client = boto3.client("kms", region_name="us-west-2")
kms_keyring_put_get(
s3_client=s3_client,
kms_client=kms_client,
kms_key_id=KMS_KEY_ID,
bucket=BUCKET,
key=KEY,
key=key,
)
# Clean up
s3_client.delete_object(Bucket=BUCKET, Key=KEY)
s3_client.delete_object(Bucket=BUCKET, Key=key)
1 change: 1 addition & 0 deletions examples/test/test_i_legacy_decrypt_example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Test suite for the legacy decrypt example."""

import boto3
import pytest

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = [
]
license = {text = "Apache-2.0"}
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.10"
dependencies = [
"boto3>=1.43.6,<2",
"cryptography>=48.0.0,<49",
Expand All @@ -33,7 +33,7 @@ packages = ["src/s3_encryption"]

[tool.ruff]
line-length = 100
target-version = "py311"
target-version = "py310"
exclude = [".git", "__pycache__", "build", "dist"]

[tool.ruff.lint]
Expand Down
46 changes: 29 additions & 17 deletions test/integration/test_i_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,11 +462,18 @@ def test_wrong_key_and_tampered_ciphertext_produce_same_error(self):
iv = os.urandom(16)
ciphertext = self._encrypt_cbc(key, iv, b"test data for padding oracle check")

# Wrong key: decryption produces garbage, unpadding fails
wrong_key = os.urandom(32)
decryptor1 = self._make_cbc_decryptor(wrong_key, iv, len(ciphertext))
with pytest.raises(S3EncryptionClientSecurityError) as exc1:
decryptor1.finalize(ciphertext)
# Wrong key: decryption produces garbage, unpadding fails.
# ~1/256 chance random garbage has valid PKCS7 padding, so retry.
exc1 = None
for _ in range(10):
wrong_key = os.urandom(32)
decryptor1 = self._make_cbc_decryptor(wrong_key, iv, len(ciphertext))
try:
decryptor1.finalize(ciphertext)
except S3EncryptionClientSecurityError as e:
exc1 = e
break
assert exc1 is not None, "Wrong key did not produce padding error after 10 attempts"

# Tampered ciphertext: last byte flipped, unpadding fails
tampered = ciphertext[:-1] + bytes([ciphertext[-1] ^ 0x01])
Expand All @@ -475,13 +482,13 @@ def test_wrong_key_and_tampered_ciphertext_produce_same_error(self):
decryptor2.finalize(tampered)

# Both MUST produce the same error message
assert str(exc1.value) == str(exc2.value), (
f"Error messages differ: wrong_key={str(exc1.value)!r}, tampered={str(exc2.value)!r}"
assert str(exc1) == str(exc2.value), (
f"Error messages differ: wrong_key={str(exc1)!r}, tampered={str(exc2.value)!r}"
)

# Neither message should contain details about the underlying failure
assert "padding" not in str(exc1.value).lower(), (
f"Error message leaks padding information: {str(exc1.value)!r}"
assert "padding" not in str(exc1).lower(), (
f"Error message leaks padding information: {str(exc1)!r}"
)

def test_truncated_ciphertext_produces_same_error(self):
Expand All @@ -495,11 +502,17 @@ def test_truncated_ciphertext_produces_same_error(self):
iv = os.urandom(16)
ciphertext = self._encrypt_cbc(key, iv, b"test data for truncation check")

# Padding failure (wrong key)
wrong_key = os.urandom(32)
decryptor1 = self._make_cbc_decryptor(wrong_key, iv, len(ciphertext))
with pytest.raises(S3EncryptionClientSecurityError) as exc1:
decryptor1.finalize(ciphertext)
# Padding failure (wrong key) — retry for same reason as above
exc1 = None
for _ in range(10):
wrong_key = os.urandom(32)
decryptor1 = self._make_cbc_decryptor(wrong_key, iv, len(ciphertext))
try:
decryptor1.finalize(ciphertext)
except S3EncryptionClientSecurityError as e:
exc1 = e
break
assert exc1 is not None, "Wrong key did not produce padding error after 10 attempts"

# Truncated ciphertext (not block-aligned)
truncated = ciphertext[:-3]
Expand All @@ -508,9 +521,8 @@ def test_truncated_ciphertext_produces_same_error(self):
decryptor2.finalize(truncated)

# Both MUST produce the same error message
assert str(exc1.value) == str(exc2.value), (
f"Error messages differ: padding_fail={str(exc1.value)!r}, "
f"truncated={str(exc2.value)!r}"
assert str(exc1) == str(exc2.value), (
f"Error messages differ: padding_fail={str(exc1)!r}, truncated={str(exc2.value)!r}"
)


Expand Down
Loading