Skip to main content

modelstamp

Model files with receipts.

Tests Python 3.8–3.13 License: MIT DOI arXiv

Modelstamp detecting a tampered machine-learning artifact

Pre-deserialization verification for persisted Python machine-learning artifacts: detect integrity failures and relevant dependency drift before loading pickle or joblib models.

pip install modelstamp
import modelstamp as ms

model = {"feature_names": ["age", "income"], "weights": [0.3, 0.7]}
ms.save(model, "model.pkl")
restored, manifest = ms.load("model.pkl", on_mismatch="raise")
print(restored, manifest.relevant_packages)

Documentation · Benchmarks · Drift matrix · Preprint · Security policy

modelstamp is a lightweight Python model-persistence layer that records an artifact's SHA-256 digest, runtime metadata, and artifact-specific dependencies. It verifies integrity, compares environments, and can authenticate manifests with HMAC before deserialization. It complements lock files and model registries; it does not make untrusted pickle or joblib payloads safe to execute.

Loading a persisted model under different dependency versions is unsupported and may fail or behave differently. The scikit-learn model-persistence documentation therefore recommends preserving the training environment alongside the model. modelstamp packages that practice into a small API.

Research evidence

The current arXiv preprint evaluates 14 controlled environment-drift scenarios, eight controlled trust-boundary scenarios, and verification scaling from 10 MiB to 1 GiB.

Subsequent repository experiments extend that evidence without changing the preprint's historical scope:

These experiments evaluate verification ordering and its consequence. They do not claim that Modelstamp detects malicious models or makes pickle/joblib safe for untrusted artifacts.

Why modelstamp?

A normal model.pkl remembers the fitted object, but not the environment that made it work. modelstamp adds the missing receipt:

  • Integrity: detect truncated, replaced, or corrupted artifacts.
  • Compatibility: identify Python and relevant dependency changes.
  • Traceability: record model details, metadata, time, and optional Git state.
  • Familiarity: keep using pickle or joblib through save() and load().

When to use it

Modelstamp is designed for scikit-learn and other persisted Python ML models when you need artifact-level integrity checks, dependency-drift detection, or reproducibility metadata without adopting a full model registry.

It complements lock files and registries: those tools recreate environments or manage model lifecycles, while Modelstamp connects one artifact to its digest and recorded runtime. It does not make untrusted pickle or joblib payloads safe to execute and does not currently provide asymmetric public-key signatures.

See the guides for dependency drift, joblib artifact verification, CI/CD checks, and comparisons with related tools.

Optional joblib support

Install joblib support explicitly when it is not already available in the model environment:

pip install "modelstamp[joblib]"

Save and load

import modelstamp as ms

manifest = ms.save(
    model,
    "model.joblib",
    metadata={"validation_roc_auc": 0.883},
)

model, manifest = ms.load("model.joblib")

Saving creates two files:

model.joblib
model.joblib.manifest.json

The manifest records:

  • SHA-256 and byte size of the artifact
  • pickle or joblib serialization backend
  • model class and scikit-learn pipeline components
  • Python, platform, and relevant package versions
  • creation time and optional Git commit/worktree status
  • caller-provided JSON metadata

load() verifies the artifact before deserializing it. It then compares the current runtime with the saved runtime and warns when a relevant dependency has changed.

Relevance follows the persisted model type rather than every installed package. For example, LightGBM's scikit-learn-compatible estimators track scikit-learn alongside LightGBM, NumPy, and SciPy, so embedded sklearn drift is visible before loading. The CI-enforced dependency-drift matrix documents 14 CI-enforced cross-environment, relevant-dependency, and noise-control cases.

Operations targeting the same artifact are serialized across local processes. During loading, verification and deserialization use the same open file so a concurrent replacement cannot bypass the digest check.

Mismatch policy

# Default: verify, then warn about environment changes.
model, manifest = ms.load("model.joblib")

# Refuse to load when the runtime differs.
model, manifest = ms.load("model.joblib", on_mismatch="raise")

# Verify integrity but skip the environment warning.
model = ms.load(
    "model.joblib",
    on_mismatch="ignore",
    return_manifest=False,
)

Integrity failures always raise ArtifactIntegrityError; on_mismatch does not disable the digest check.

Inspect without loading

manifest = ms.inspect("model.joblib")
report = ms.check("model.joblib")
ms.verify("model.joblib")

The same operations are available from the command line:

modelstamp inspect model.joblib
modelstamp check model.joblib
modelstamp verify model.joblib

You can also use python -m modelstamp in environments where the console script is not on PATH. inspect validates the manifest structure but does not authenticate its contents; use verify or check when trust matters.

check exits with status 0 for a clean artifact, 1 for a compatibility or integrity mismatch, and 2 when the manifest cannot be read.

Signed manifests

A checksum detects accidental corruption, but someone who can replace both files can also create a matching checksum. For artifacts crossing a trust boundary, sign the manifest with a secret key:

import os
import modelstamp as ms

key = os.environ["MODELSTAMP_SIGNING_KEY"].encode()
ms.save(
    model,
    "model.joblib",
    signing_key=key,
    key_id="production-2026-q3",
)
model, manifest = ms.load("model.joblib", signing_key=key)

The signature is an HMAC-SHA-256 over the complete manifest, including the artifact digest. A signed artifact cannot be loaded or verified without its key. Supplying a key also rejects an unsigned manifest, preventing silent downgrades. Keep the key outside source control and separate from the artifact.

HMAC is symmetric, so anyone who can verify with the shared secret can also forge a valid manifest. modelstamp does not currently provide asymmetric public-key verification such as Ed25519 or Sigstore.

For CLI verification, name the environment variable containing the key:

modelstamp verify model.joblib --signing-key-env MODELSTAMP_SIGNING_KEY

For key rotation, verify through a registry. The authenticated key_id chooses the correct secret without changing old artifacts:

keys = {
    "production-2026-q2": old_key,
    "production-2026-q3": current_key,
}
model, manifest = ms.load("model.joblib", signing_keys=keys)

See the signing and key-rotation guide for the migration and security model.

Complete scikit-learn example

from pathlib import Path

from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

import modelstamp as ms

X, y = load_iris(return_X_y=True)
pipeline = make_pipeline(StandardScaler(), LogisticRegression(max_iter=500))
pipeline.fit(X, y)

path = Path("iris.joblib")
ms.save(pipeline, path, metadata={"dataset": "iris"})
restored, manifest = ms.load(path)
print(restored.predict(X[:3]))
print(manifest.relevant_packages)

# Safe inspection does not deserialize the model.
print(ms.check(path))

# Corruption is detected before pickle/joblib can execute anything.
path.write_bytes(path.read_bytes() + b"changed")
try:
    ms.verify(path)
except ms.ArtifactIntegrityError as exc:
    print(f"Rejected: {exc}")

Additional standalone examples cover a scikit-learn pipeline, tamper detection, and HMAC-authenticated manifests in the examples/ directory.

The model-risk case study provides a runnable trust-boundary matrix for tampering, unsigned replacement, HMAC enforcement, shared-key forgery, and rollback/replay.

API at a glance

Operation Purpose Deserializes the model?
save(model, path) Save an artifact and its environment receipt No
load(path) Verify, compare environments, and load Yes
verify(path) Check artifact size and SHA-256 No
check(path) Check integrity and runtime compatibility No
inspect(path) Read schema-validated, unauthenticated manifest metadata No

Security boundary

Pickle and joblib can execute code during loading. The SHA-256 recorded by modelstamp detects accidental changes and mismatched sidecars; it is not a digital signature and does not make an untrusted model safe. Only load artifacts from sources you trust. For a safer serialization format, consider skops.io or ONNX where they fit your model.

Security issues should be reported according to the security policy.

Supported Python versions

Python 3.8 through 3.13 are declared for the initial release. The package has no required runtime dependency; joblib is optional.

Development

python -m pip install ".[dev]"
pytest
ruff format --check .
ruff check .
python -m build
twine check dist/*
mkdocs build --strict

GitHub Actions runs the test suite on Python 3.8 through 3.13. Publishing is configured for PyPI Trusted Publishing and runs when a GitHub release is published. Production releases must use a protected v* tag whose commit is contained in main; the PyPI deployment also requires maintainer approval.

pyproject.toml is the runtime source of truth for the package version; modelstamp.__version__ reads the resulting installed distribution metadata. Release preparation also keeps the human-facing version and date in CITATION.cff synchronized.

Maintainers should follow the release checklist. A merge to main does not publish to PyPI by itself, and lockfile-only maintenance does not require a package release.

Property-based tests exercise malformed manifest structures. Verification throughput for representative artifact sizes is recorded in BENCHMARKS.md, while cross-version compatibility behavior is covered by the dependency-drift matrix.

Contributions are welcome. See the contribution guide for the local development and pull-request workflow.

Citation

The Modelstamp preprint describes the threat model, controlled evaluation, and performance results:

Anagha Dhekne. "Modelstamp: Pre-Deserialization Verification of Machine-Learning Artifacts and Runtime Environment State." arXiv:2609.01781, 2026.

For software citation metadata and the archived release, use CITATION.cff and the Zenodo software DOI.

Maintainer

Anagha Dhekne

License

MIT

Metadata

Release files for modelstamp 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for modelstamp 0.1.5
File Size Uploaded
modelstamp-0.1.5.tar.gz 321.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for modelstamp 0.1.5
File Interpreter ABI Platform
modelstamp-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 341.7 kB

Release files / modelstamp-0.1.5.tar.gz

Download URL modelstamp-0.1.5.tar.gz
Size 321.2 kB
Tags Source
SHA-256 checksum
How to use checksums
1523cb9a95714d646a4c2ad0eb0ecc5e019e6e826180eb18ca1e82dfd265b714
BLAKE2b-256 checksum
How to use checksums
a18e1e2fa8cf15f6adbd130ab70dd9b6ccfa825ee052cac86c017009b54d5e0a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release files / modelstamp-0.1.5-py3-none-any.whl

Download URL modelstamp-0.1.5-py3-none-any.whl
Size 20.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3036d4efbf11566c3837b2bd871906aa58e3fd75f18f2590fb11ef8e9ca6662d
BLAKE2b-256 checksum
How to use checksums
45e75a3dea3ca9cb7bd72aacf34b233a50eddcb637b2465704d66c8d455c05ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.5 This release

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page