Pixseal is a Python-based image integrity and authenticity verification tool designed to detect whether an image has been modified since signing.
Pixseal embeds a cryptographically verifiable integrity seal into an image in an invisible manner. During verification, any modification — including editing, filtering, cropping, resizing, re-encoding — will cause verification to immediately fail.
If even a single pixel is altered after signing, Pixseal will detect it.
Pixseal is not a visual watermarking or branding tool.
The watermark exists solely as a means to achieve strict, deterministic image
tamper detection.
Pixseal prioritizes tamper sensitivity over robustness against intentional adversarial manipulation.
- GitHub: https://github.com/kyj9447/Pixseal
- Changelog: https://github.com/kyj9447/Pixseal/blob/main/CHANGELOG.md
-
Image Integrity Verification
- Cryptographically proves that an image remains in its original, unmodified state
- Detects single-pixel changes with deterministic verification results
-
Tamper Detection
- Detects all forms of image modification, including:
- editing
- filters and color adjustments
- cropping and resizing
- re-encoding and recompression
- pixel-level changes
- Detects all forms of image modification, including:
-
Invisible Integrity Seal
- Embeds verification data without any visible watermark
- Preserves the original visual appearance of the image
-
RSA-Based Encryption (Optional)
- Supports RSA public/private key encryption for embedded verification data
- Allows separation of signing and verification roles
-
Verification & Extraction
- Payloads may be partially or fully extractable even after modification
- Automatically fails verification when tampering is detected
-
Fully Local & Offline
- No external servers or network dependencies
- Pure Python implementation
-
Lossless Format Support
- Supports PNG and BMP (24-bit) images
- Lossy formats (e.g., JPEG, WebP) are intentionally excluded to preserve integrity guarantees
pip install Pixseal
# or for local development
pip install -e ./pip_packagePython 3.8+ is required. Wheels published to PyPI already include the compiled
Cython extension, so pip install Pixseal automatically selects the right build
for your operating system and CPU.
If you cloned the repository (or downloaded the source), run the helper script
to compile the simpleImage_ext extension for your environment:
git clone https://github.com/kyj9447/Pixseal.git
cd Pixseal
python3 -m pip install -r requirements.txt
./compile_extension.shThis command regenerates the C source via Cython and invokes your local C
compiler (clang or gcc) to produce pip_package/Pixseal/simpleImage_ext*.so.
You still need a working build toolchain (gcc/clang and Python headers)
installed through your OS package manager. If you skip this step, Pixseal falls
back to the pure Python implementation, which works but is significantly slower.
from Pixseal import signImage
result = signImage(
imageInput="assets/original.png", # accepts a file path or raw PNG/BMP bytes
hiddenString="!Validation:[email protected]",
publicKeyPath="assets/RSA/public_key.pem", # omit for plain-text embedding
)
result.save("assets/signed_original.png")- The payload is looped if it runs out before the image ends, so even small files carry the full sentinel/payload/end pattern.
- When
publicKeyPathis omitted, the payload remains plain text.
from Pixseal import validateImage
report = validateImage(
imageInput="assets/signed_original.png", # accepts a file path or raw PNG/BMP bytes
privKeyPath="assets/RSA/private_key.pem", # omit for plain-text payloads
)
print(report["extractedString1"])
print(report["validationReport"])validateImage returns:
{
"extractedString1": "<payload or encrypted blob>",
"extractedString2": "<truncated payload or encrypted blob>",
"validationReport": {
"arrayLength": 4,
"lengthCheck": True,
"startCheck": True,
"endCheck": True,
"isDecrypted": True,
"tailCheckResult": True,
"verdict": True,
# decryptSkipMessage when a decrypt request was skipped
}
}python testRun.py offers an interactive flow:
- Choose 1 to sign an image. It reads
assets/original.png, asks for a payload (default!Validation:[email protected]), optionally encrypts withassets/RSA/public_key.pem, and writesassets/signed_<name>.png. - Choose 2 to validate. It reads
assets/signed_original.png, optionally decrypts withassets/RSA/private_key.pem, and prints both the extracted string and verdict. - Choose 3 to benchmark performance. It reads
assets/original.png, encrypts it withassets/RSA/public_key.pem, and writesassets/signed_original.png, printing the elapsed signing time. Then it readsassets/signed_original.png, performs extraction/decryption/validation, and prints the elapsed validation time along with the total elapsed time. - Choose 4 to test signing and validation with file-path input option.
- Choose 5 to test signing and validation with byte-stream input option.
- Choose 6 to run the optional line-profiler demo. It benchmarks
signImageandvalidateImage, printing per-line timings when the script is executed throughkernprof.
Option 6 requires the optional dependency line_profiler and must be run via kernprof -l testRun.py so that builtins.profile is provided. Without line_profiler installed the script will continue to work, but the profiling option will display an informative message instead of running.
Generate a test RSA pair (PKCS#8) with OpenSSL:
openssl genpkey -algorithm RSA -out assets/RSA/private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in assets/RSA/private_key.pem -out assets/RSA/public_key.pemPoint publicKeyPath / privKeyPath to these files.
| Function | Description |
|---|---|
signImage(imageInput, hiddenString, publicKeyPath=None) |
Loads a PNG/BMP from a filesystem path or raw bytes, injects hiddenString plus sentinels, encrypting each chunk when publicKeyPath is provided. Returns a SimpleImage that you can save() or saveBmp(). |
validateImage(imageInput, privKeyPath=None) |
Reads the hidden bit stream from a path or raw bytes, splits by newlines, deduplicates, optionally decrypts each chunk (Base64 indicates ciphertext), and returns the payload plus a validation report. |
| Original | Signed (!Validation:[email protected]) |
|---|---|
Validation output excerpt:
[Validate] verdict: True
[Validate] extracted string: !Validation:[email protected]
[Validate] decrypted with private key: RSA/private_key.pem
Validation Report
{'extractedString1': '!Validation:[email protected]',
'extractedString2': 'DMnWAzbd6NFycGAxcPkzzmGjL33WXovG...',
'validationReport': {'arrayLength': 4,
'endCheck': True,
'isDecrypted': True,
'lengthCheck': True,
'startCheck': True,
'tailCheckResult': True,
'verdict': True}}
(When encrypted, each line appears as Base64 until decrypted with the RSA private key.)
| Corrupted after signing |
|---|
Validation output excerpt:
...
string argument should contain only ASCII characters
string argument should contain only ASCII characters
string argument should contain only ASCII characters
[Validate] verdict: False
[Validate] extracted string: !Validation:[email protected]
[Validate] decrypted with private key: RSA/private_key.pem
Validation Report
{'extractedString1': '!Validation:[email protected]',
'extractedString2': 'hh78IWEsRgfTWMw3Rg02hTnCdErjx0O4...',
'validationReport': {'arrayLength': 400,
'decryptSkipMessage': 'Skip decrypt: payload was plain '
'or corrupted text despite decrypt '
'request.',
'endCheck': True,
'isDecrypted': True,
'lengthCheck': False,
'startCheck': True,
'tailCheckResult': 'Not Required',
'verdict': False}}
https://github.com/kyj9447/imageSignerCamera
- Mobile camera that signs images on capture:
- Server-side validator that decrypts and verifies payloads.