pysealer 0.2.0__pp39-pypy39_pp73-musllinux_1_2_armv7l.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pysealer might be problematic. Click here for more details.
- pysealer/__init__.py +24 -0
- pysealer/_pysealer.pypy39-pp73-arm-linux-gnu.so +0 -0
- pysealer/add_decorators.py +215 -0
- pysealer/check_decorators.py +179 -0
- pysealer/cli.py +316 -0
- pysealer/dummy_decorators.py +83 -0
- pysealer/github_secrets.py +170 -0
- pysealer/remove_decorators.py +88 -0
- pysealer/setup.py +137 -0
- pysealer-0.2.0.dist-info/METADATA +171 -0
- pysealer-0.2.0.dist-info/RECORD +15 -0
- pysealer-0.2.0.dist-info/WHEEL +4 -0
- pysealer-0.2.0.dist-info/entry_points.txt +2 -0
- pysealer-0.2.0.dist-info/licenses/LICENSE +21 -0
- pysealer.libs/libgcc_s-262c4f60.so.1 +0 -0
pysealer/setup.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Setup the storage of the pysealer keypair in a .env file."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from dotenv import load_dotenv, set_key
|
|
7
|
+
from pysealer import generate_keypair
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _find_env_file() -> Path:
|
|
11
|
+
"""
|
|
12
|
+
Search for .env file starting from current directory and walking up to parent directories.
|
|
13
|
+
Also checks PYSEALER_ENV_PATH environment variable.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Path: Path to the .env file
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
FileNotFoundError: If no .env file is found
|
|
20
|
+
"""
|
|
21
|
+
# First check if PYSEALER_ENV_PATH environment variable is set
|
|
22
|
+
env_path_var = os.getenv("PYSEALER_ENV_PATH")
|
|
23
|
+
if env_path_var:
|
|
24
|
+
env_path = Path(env_path_var)
|
|
25
|
+
if env_path.exists():
|
|
26
|
+
return env_path
|
|
27
|
+
|
|
28
|
+
# Start from current working directory and search upward
|
|
29
|
+
current = Path.cwd()
|
|
30
|
+
|
|
31
|
+
# Check current directory and all parent directories up to root
|
|
32
|
+
for parent in [current] + list(current.parents):
|
|
33
|
+
env_file = parent / '.env'
|
|
34
|
+
if env_file.exists():
|
|
35
|
+
return env_file
|
|
36
|
+
|
|
37
|
+
# If not found, return the default location (current directory)
|
|
38
|
+
# This will be used in error messages
|
|
39
|
+
return Path.cwd() / '.env'
|
|
40
|
+
|
|
41
|
+
def setup_keypair(env_path: Optional[str | Path] = None):
|
|
42
|
+
"""
|
|
43
|
+
Generate and store keypair securely.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
env_path: Optional path to .env file. If None, creates in current directory.
|
|
47
|
+
"""
|
|
48
|
+
# Determine .env location
|
|
49
|
+
if env_path is None:
|
|
50
|
+
env_path = Path.cwd() / '.env'
|
|
51
|
+
else:
|
|
52
|
+
env_path = Path(env_path)
|
|
53
|
+
|
|
54
|
+
# Check if keys already exist
|
|
55
|
+
if env_path.exists():
|
|
56
|
+
load_dotenv(env_path)
|
|
57
|
+
existing_private = os.getenv("PYSEALER_PRIVATE_KEY")
|
|
58
|
+
existing_public = os.getenv("PYSEALER_PUBLIC_KEY")
|
|
59
|
+
|
|
60
|
+
if existing_private or existing_public:
|
|
61
|
+
raise ValueError(f"Keys already exist in {env_path} Cannot overwrite existing keys.")
|
|
62
|
+
|
|
63
|
+
# Create .env if it doesn't exist
|
|
64
|
+
env_path.touch(exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Generate keypair using the Rust function
|
|
67
|
+
private_key_hex, public_key_hex = generate_keypair()
|
|
68
|
+
|
|
69
|
+
# Store keys in .env file
|
|
70
|
+
set_key(str(env_path), "PYSEALER_PRIVATE_KEY", private_key_hex)
|
|
71
|
+
set_key(str(env_path), "PYSEALER_PUBLIC_KEY", public_key_hex)
|
|
72
|
+
|
|
73
|
+
return private_key_hex, public_key_hex
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_public_key(env_path: Optional[str | Path] = None) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Retrieve the public key from the .env file.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
env_path: Optional path to .env file. If None, searches from current directory upward.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
str: The public key hex string, or None if not found.
|
|
85
|
+
"""
|
|
86
|
+
# Determine .env location
|
|
87
|
+
if env_path is None:
|
|
88
|
+
env_path = _find_env_file()
|
|
89
|
+
else:
|
|
90
|
+
env_path = Path(env_path)
|
|
91
|
+
|
|
92
|
+
# Check if .env exists
|
|
93
|
+
if not env_path.exists():
|
|
94
|
+
raise FileNotFoundError(f"No .env file found at {env_path}. Run setup_keypair() first.")
|
|
95
|
+
|
|
96
|
+
# Load environment variables from .env
|
|
97
|
+
load_dotenv(env_path)
|
|
98
|
+
|
|
99
|
+
# Get public key
|
|
100
|
+
public_key = os.getenv("PYSEALER_PUBLIC_KEY")
|
|
101
|
+
|
|
102
|
+
if public_key is None:
|
|
103
|
+
raise ValueError(f"PYSEALER_PUBLIC_KEY not found in {env_path}. Run setup_keypair() first.")
|
|
104
|
+
|
|
105
|
+
return public_key
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_private_key(env_path: Optional[str | Path] = None) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Retrieve the private key from the .env file.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
env_path: Optional path to .env file. If None, searches from current directory upward.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
str: The private key hex string, or None if not found.
|
|
117
|
+
"""
|
|
118
|
+
# Determine .env location
|
|
119
|
+
if env_path is None:
|
|
120
|
+
env_path = _find_env_file()
|
|
121
|
+
else:
|
|
122
|
+
env_path = Path(env_path)
|
|
123
|
+
|
|
124
|
+
# Check if .env exists
|
|
125
|
+
if not env_path.exists():
|
|
126
|
+
raise FileNotFoundError(f"No .env file found at {env_path}. Run setup_keypair() first.")
|
|
127
|
+
|
|
128
|
+
# Load environment variables from .env
|
|
129
|
+
load_dotenv(env_path)
|
|
130
|
+
|
|
131
|
+
# Get private key
|
|
132
|
+
private_key = os.getenv("PYSEALER_PRIVATE_KEY")
|
|
133
|
+
|
|
134
|
+
if private_key is None:
|
|
135
|
+
raise ValueError(f"PYSEALER_PRIVATE_KEY not found in {env_path}. Run setup_keypair() first.")
|
|
136
|
+
|
|
137
|
+
return private_key
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysealer
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Rust
|
|
14
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
19
|
+
Requires-Dist: typer>=0.9.0
|
|
20
|
+
Requires-Dist: pygithub>=2.1.1
|
|
21
|
+
Requires-Dist: pynacl>=1.5.0
|
|
22
|
+
Requires-Dist: gitpython>=3.1.0
|
|
23
|
+
Requires-Dist: pytest>=7.0.0 ; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest-cov>=4.0.0 ; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'test'
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Summary: Cryptographically sign Python functions and classes for defense-in-depth security
|
|
29
|
+
Keywords: rust,python,decorator,cryptography
|
|
30
|
+
Author: Aidan Dyga
|
|
31
|
+
License: MIT
|
|
32
|
+
Requires-Python: >=3.8
|
|
33
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
34
|
+
Project-URL: Issues, https://github.com/MCP-Security-Research/pysealer/issues
|
|
35
|
+
Project-URL: Repository, https://github.com/MCP-Security-Research/pysealer
|
|
36
|
+
|
|
37
|
+
# pysealer
|
|
38
|
+
|
|
39
|
+
Cryptographically sign Python functions and classes for defense-in-depth security
|
|
40
|
+
|
|
41
|
+
> 💡 **code version controls code**
|
|
42
|
+
|
|
43
|
+
- 🦀 Built with the [maturin build system](https://www.maturin.rs/) for easy Rust-Python packaging
|
|
44
|
+
- 🔗 [PyO3](https://pyo3.rs/v0.27.1/index.html) bindings for seamless Python-Rust integration
|
|
45
|
+
- 🔏 [Ed25519](https://docs.rs/ed25519-dalek/latest/ed25519_dalek/) signatures to ensure code integrity and authorship
|
|
46
|
+
- 🖥️ [Typer](https://typer.tiangolo.com/) for a clean and user-friendly command line interface
|
|
47
|
+
|
|
48
|
+
Pysealer helps you maintain code integrity by automatically adding cryptographic signatures to your Python functions and classes. Each function or class receives a unique decorator containing a cryptographic signature that verifies both authorship and integrity, making it easy to detect unauthorized code modifications.
|
|
49
|
+
|
|
50
|
+
## Table of Contents
|
|
51
|
+
|
|
52
|
+
1. [Getting Started](#getting-started)
|
|
53
|
+
2. [Usage](#usage)
|
|
54
|
+
3. [How It Works](#how-it-works)
|
|
55
|
+
4. [Model Context Protocol (MCP) Security Use Cases](#model-context-protocol-mcp-security-use-cases)
|
|
56
|
+
5. [Contributing](#contributing)
|
|
57
|
+
6. [License](#license)
|
|
58
|
+
|
|
59
|
+
## Getting Started
|
|
60
|
+
|
|
61
|
+
```shell
|
|
62
|
+
pip install pysealer
|
|
63
|
+
# or
|
|
64
|
+
uv pip install pysealer
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```shell
|
|
70
|
+
pysealer init [ENV_FILE] # Initialize the pysealer tool by generating and saving keys to an ENV_FILE (default: .env)
|
|
71
|
+
pysealer decorate <file.py>... # Add cryptographic decorators to all functions/classes in one or more .py files
|
|
72
|
+
pysealer check <file.py>... # Verify the integrity and validity of pysealer decorators in one or more .py files
|
|
73
|
+
pysealer remove <file.py>... # Remove all pysealer decorators from one or more .py files
|
|
74
|
+
pysealer --help # Show all available commands and options
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How It Works
|
|
78
|
+
|
|
79
|
+
Pysealer works by automatically injecting cryptographic decorators into your Python functions and classes. Here's how the process works:
|
|
80
|
+
|
|
81
|
+
### Step-by-Step Example
|
|
82
|
+
|
|
83
|
+
Suppose you have a file `fibonacci.py`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def fibonacci(n):
|
|
87
|
+
if n <= 0:
|
|
88
|
+
return 0
|
|
89
|
+
elif n == 1:
|
|
90
|
+
return 1
|
|
91
|
+
else:
|
|
92
|
+
return fibonacci(n-1) + fibonacci(n-2)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### 1. Decorate the file
|
|
96
|
+
|
|
97
|
+
```shell
|
|
98
|
+
pysealer decorate examples/fibonacci.py
|
|
99
|
+
|
|
100
|
+
Successfully added decorators to 1 file:
|
|
101
|
+
✓ /path/to/examples/fibonacci.py
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@pysealer._GnCLaWr9B6TD524JZ3v1CENXmo5Drwfgvc9arVagbghQ6hMH4Aqc8whs3Tf57pkTjsAVNDybviW9XG5Eu3JSP6T()
|
|
106
|
+
def fibonacci(n):
|
|
107
|
+
if n <= 0:
|
|
108
|
+
return 0
|
|
109
|
+
elif n == 1:
|
|
110
|
+
return 1
|
|
111
|
+
else:
|
|
112
|
+
return fibonacci(n-1) + fibonacci(n-2)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### 2. Check integrity
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
pysealer check examples/fibonacci.py
|
|
119
|
+
|
|
120
|
+
All decorators are valid in 1 file:
|
|
121
|
+
✓ /path/to/examples/fibonacci.py: 1 decorators valid
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### 3. Tamper with the code (change return 0 to return 42)
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
@pysealer._GnCLaWr9B6TD524JZ3v1CENXmo5Drwfgvc9arVagbghQ6hMH4Aqc8whs3Tf57pkTjsAVNDybviW9XG5Eu3JSP6T()
|
|
128
|
+
def fibonacci(n):
|
|
129
|
+
if n <= 0:
|
|
130
|
+
return 42
|
|
131
|
+
elif n == 1:
|
|
132
|
+
return 1
|
|
133
|
+
else:
|
|
134
|
+
return fibonacci(n-1) + fibonacci(n-2)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### 4. Check again
|
|
138
|
+
|
|
139
|
+
```shell
|
|
140
|
+
pysealer check examples/fibonacci.py
|
|
141
|
+
|
|
142
|
+
1/1 decorators failed verification across 1 file:
|
|
143
|
+
✗ /path/to/examples/fibonacci.py: 1/1 decorators failed
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Model Context Protocol (MCP) Security Use Cases
|
|
147
|
+
|
|
148
|
+
One use case of Pysealer is to protect MCP servers from upstream attacks by cryptographically signing tool functions and their docstrings. Since LLMs rely on docstrings to understand tool behavior, attackers can inject malicious instructions or create fake tools that mimic legitimate ones. Pysealer's signatures ensure tool authenticity and detect tampering because any modification to code or docstrings breaks the signature and flags compromised tools.
|
|
149
|
+
|
|
150
|
+
- **Detect Version Control Changes**
|
|
151
|
+
- Automatically detect unauthorized code modifications through cryptographic signatures
|
|
152
|
+
- Each function's decorator contains a signature based on its code and docstring
|
|
153
|
+
- Any mismatch between code and signature is immediately flagged
|
|
154
|
+
|
|
155
|
+
- **Defense-in-Depth for Source Control**
|
|
156
|
+
- Add an additional security layer to version control systems
|
|
157
|
+
- Complement existing security measures with cryptographic verification
|
|
158
|
+
- Reduce risk through multiple layers of protection
|
|
159
|
+
|
|
160
|
+
## Contributing
|
|
161
|
+
|
|
162
|
+
**🙌 Contributions are welcome!**
|
|
163
|
+
|
|
164
|
+
If you have suggestions, bug reports, or want to help improve Pysealer, feel free to open an [issue](https://github.com/MCP-Security-Research/pysealer/issues) or submit a pull request.
|
|
165
|
+
|
|
166
|
+
All ideas and contributions are appreciated—thanks for helping make pysealer better!
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
Pysealer is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
171
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pysealer/__init__.py,sha256=iKSceDzfR6fiNPSoKYAhvllIYpxfWXRlOK6TxDbfxbc,944
|
|
2
|
+
pysealer/_pysealer.pypy39-pp73-arm-linux-gnu.so,sha256=4MN7AoTDT732QzgBt8l-gCrU_YAvr8wa6F1gqWoi8ww,690305
|
|
3
|
+
pysealer/add_decorators.py,sha256=-Jq1no002PfFnYeYVQsIGptzZpqVpoopox7RVV5QbtU,8750
|
|
4
|
+
pysealer/check_decorators.py,sha256=LdcVBWS_PbuqJvXNBkOYSaPRiDOMvAlA5SZhjkxGGk8,7203
|
|
5
|
+
pysealer/cli.py,sha256=FMS3Xir3gcwKLLFeInZR6tBg6QhipKMzD5PGrCDp3hY,13837
|
|
6
|
+
pysealer/dummy_decorators.py,sha256=9ORNnMZ6lT5e1gU4Dt9lifAEDfjJ1wCXGiOUfMiqP-w,2807
|
|
7
|
+
pysealer/github_secrets.py,sha256=7SQP1k97vcLsPQqLOBEt-XiSep4dOIHPQI8NCuR-xFg,6120
|
|
8
|
+
pysealer/remove_decorators.py,sha256=v1VpxFid8kqqoj-NuWSWWzCoYUwl1XVYyGdTE8BLbeI,3276
|
|
9
|
+
pysealer/setup.py,sha256=M8wgv4GeVbbC_t0a8PBD5Hn6piqGTFIUTlo1ds6JtAc,4206
|
|
10
|
+
pysealer-0.2.0.dist-info/METADATA,sha256=SnUho5P0sJqiD52V14bZ0Ahh568mJrJxaQKpNkfYfCM,6229
|
|
11
|
+
pysealer-0.2.0.dist-info/WHEEL,sha256=2-ZUT3rJGxEH-GQtf92q-Kd-xPuJgsnTvM22qgJ-JHY,113
|
|
12
|
+
pysealer-0.2.0.dist-info/entry_points.txt,sha256=DUDRyFGp10a8FMaLUFE6X94kCD2dciDY66ISXuUySmA,45
|
|
13
|
+
pysealer-0.2.0.dist-info/licenses/LICENSE,sha256=FtcYsMyXNwAqNhOMxR3TwptCUnwb5coRK_UQyWGLJnc,1067
|
|
14
|
+
pysealer.libs/libgcc_s-262c4f60.so.1,sha256=xPsZgCvL7EO-llmjqc5bm96baehLsO4avBqUhih0xZg,2810501
|
|
15
|
+
pysealer-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aidan Dyga
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
Binary file
|