Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All versions prior to 1.0.0 are untracked.
- Standardized CLI flags to use hyphens (e.g., `--trust-config` instead of `--trust_config`). Underscore variants are still accepted for backwards compatibility via token normalization.

### Fixed
- Fixed a path traversal during verification with `ignore_unsigned_files`, where a signed manifest whose resource paths contained `..` caused files outside the model directory to be read and hashed. Such paths are now rejected.
- Fixed a bug where installing from the sdist produced an empty wheel with zero Python modules. The hatch `packages` directive was scoped to all build targets instead of the wheel target only, causing the sdist's flattened layout to not match the expected `src/` path. ([#636](https://github.com/sigstore/model-transparency/issues/636))
- Fixed a bug where ignored symlinks could raise `ValueError`s if allow_symlinks was unset, even though they were skipped during serialization. ([#550](https://github.com/sigstore/model-transparency/pull/550))
- Fixed a bug where any PEM encoded key could be read during the key-based flows which resulted in a Python exception because the rest of the code only supported elliptic curve keys. ([#573](https://github.com/sigstore/model-transparency/pull/573))
Expand Down
17 changes: 13 additions & 4 deletions src/model_signing/verifying.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,19 @@ def verify(
)

if self._ignore_unsigned_files:
files_to_hash = [
model_path / rd.identifier
for rd in expected_manifest.resource_descriptors()
]
model_root = pathlib.Path(model_path)
resolved_root = model_root.resolve()
files_to_hash = []
for rd in expected_manifest.resource_descriptors():
file_to_hash = model_root / rd.identifier
try:
file_to_hash.resolve().relative_to(resolved_root)
except ValueError:
raise ValueError(
"Signed manifest references a path outside the model "
f"directory: {rd.identifier}"
) from None
files_to_hash.append(file_to_hash)
else:
files_to_hash = None

Expand Down
67 changes: 67 additions & 0 deletions tests/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@
import pytest

from model_signing import hashing
from model_signing import manifest
from model_signing import signing
from model_signing import verifying
from model_signing._hashing import hashing as _hashing
from model_signing._hashing import memory
from model_signing._signing import sign_ec_key
from model_signing._signing import signing as _signing


# Directory with testdata for this test
Expand Down Expand Up @@ -417,3 +422,65 @@ def test_sign_and_verify_sharded(self, base_path, populate_tmpdir):
signature, ignore_git_paths, ["model.sig", "ignored"]
)
assert get_model_name(signature) == os.path.basename(model_path)


class TestIgnoreUnsignedFilesTraversal:
def _sha256(self, path: Path) -> _hashing.Digest:
hasher = memory.SHA256()
hasher.update(path.read_bytes())
digest = hasher.compute()
return _hashing.Digest("sha256", digest.digest_value)

def _sign_manifest(
self, mani: manifest.Manifest, private_key: Path, out: Path
) -> None:
signer = sign_ec_key.Signer(private_key)
signature = signer.sign(_signing.Payload(mani))
signature.write(out)

def test_verify_rejects_path_outside_model(self, tmp_path):
model = tmp_path / "model"
model.mkdir()
(model / "weights.bin").write_bytes(b"legit weights")
secret = tmp_path / "secret.txt"
secret.write_bytes(b"outside the model root")

items = [
manifest.FileManifestItem(
path=Path("weights.bin"),
digest=self._sha256(model / "weights.bin"),
),
manifest.FileManifestItem(
path=Path("../secret.txt"),
digest=self._sha256(secret),
),
]
serialization = manifest._FileSerialization("sha256")
mani = manifest.Manifest("model", items, serialization)

private_key = Path(TESTDATA / "keys/certificate/signing-key.pem")
public_key = Path(TESTDATA / "keys/certificate/signing-key-pub.pem")
signature = tmp_path / "model.sig"
self._sign_manifest(mani, private_key, signature)

with pytest.raises(ValueError, match="outside the model directory"):
verifying.Config().use_elliptic_key_verifier(
public_key=public_key
).set_ignore_unsigned_files(True).verify(model, signature)

def test_verify_accepts_in_model_paths(self, tmp_path):
model = tmp_path / "model"
model.mkdir()
(model / "weights.bin").write_bytes(b"legit weights")

private_key = Path(TESTDATA / "keys/certificate/signing-key.pem")
public_key = Path(TESTDATA / "keys/certificate/signing-key-pub.pem")
signature = tmp_path / "model.sig"

signing.Config().use_elliptic_key_signer(
private_key=private_key
).sign(model, signature)

verifying.Config().use_elliptic_key_verifier(
public_key=public_key
).set_ignore_unsigned_files(True).verify(model, signature)