project-enclave-tools 0.1.1__tar.gz
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.
- project_enclave_tools-0.1.1/PKG-INFO +54 -0
- project_enclave_tools-0.1.1/README.md +44 -0
- project_enclave_tools-0.1.1/dev_tools/cli.py +58 -0
- project_enclave_tools-0.1.1/dev_tools/init.py +3 -0
- project_enclave_tools-0.1.1/dev_tools/tools/init.py +1 -0
- project_enclave_tools-0.1.1/dev_tools/tools/signing.py +271 -0
- project_enclave_tools-0.1.1/dev_tools/utils/init.py +1 -0
- project_enclave_tools-0.1.1/dev_tools/utils/keys.py +81 -0
- project_enclave_tools-0.1.1/project_enclave_tools.egg-info/PKG-INFO +54 -0
- project_enclave_tools-0.1.1/project_enclave_tools.egg-info/SOURCES.txt +13 -0
- project_enclave_tools-0.1.1/project_enclave_tools.egg-info/dependency_links.txt +1 -0
- project_enclave_tools-0.1.1/project_enclave_tools.egg-info/entry_points.txt +2 -0
- project_enclave_tools-0.1.1/project_enclave_tools.egg-info/top_level.txt +1 -0
- project_enclave_tools-0.1.1/pyproject.toml +19 -0
- project_enclave_tools-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: project-enclave-tools
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Tools made mainly to help the devs of project enclave and other sub projects under it
|
|
5
|
+
Author-email: Pradhya <Pradhya@projectenclave.dev>
|
|
6
|
+
Project-URL: Repository, https://github.com/Chinglen2080/dev-tools
|
|
7
|
+
Project-URL: Issues, https://github.com/Chinglen2080/dev-tools/issues
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Dev Tools
|
|
12
|
+
|
|
13
|
+
Dev Tools is a small command-line toolkit for contributors to [Project Enclave](https://github.com/project-enclave). It currently provides signed SHA256 manifests for release files.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install the current public release with `pipx`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pipx install "git+https://github.com/Chinglen2080/dev-tools.git@v0.1.1"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
OpenSSL must be installed and available on your `PATH` for key generation and signing.
|
|
24
|
+
|
|
25
|
+
## Signing release files
|
|
26
|
+
|
|
27
|
+
Generate a 2048-bit RSA key pair the first time you use the signing tool:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
tools sign --init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The keys are stored in `~/.config/dev-tools/keys/`. Keep `private.pem` private; distribute `public.pem` to anyone who needs to verify your releases. If keys already exist, initialization asks before replacing them.
|
|
34
|
+
|
|
35
|
+
From the release directory, create or update the manifest and signature:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
tools sign
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The manifest contains SHA256 hashes for `.py`, `.json`, `.yaml`, `.yml`, `.toml`, `.txt`, and `.md` files. Hidden files and common generated directories are skipped. Commit `manifest.json` and `manifest.sig` with the release.
|
|
42
|
+
|
|
43
|
+
To sign or verify a different directory, pass it with `--directory` (or `-C`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
tools sign --directory ./release
|
|
47
|
+
tools sign --verify -C ./release
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Verification checks the signature, each listed file's hash, missing files, and any new signable files that are absent from the manifest.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
This tool is provided under the GNU General Public License v3.0.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Dev Tools
|
|
2
|
+
|
|
3
|
+
Dev Tools is a small command-line toolkit for contributors to [Project Enclave](https://github.com/project-enclave). It currently provides signed SHA256 manifests for release files.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the current public release with `pipx`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install "git+https://github.com/Chinglen2080/dev-tools.git@v0.1.1"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
OpenSSL must be installed and available on your `PATH` for key generation and signing.
|
|
14
|
+
|
|
15
|
+
## Signing release files
|
|
16
|
+
|
|
17
|
+
Generate a 2048-bit RSA key pair the first time you use the signing tool:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
tools sign --init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The keys are stored in `~/.config/dev-tools/keys/`. Keep `private.pem` private; distribute `public.pem` to anyone who needs to verify your releases. If keys already exist, initialization asks before replacing them.
|
|
24
|
+
|
|
25
|
+
From the release directory, create or update the manifest and signature:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
tools sign
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The manifest contains SHA256 hashes for `.py`, `.json`, `.yaml`, `.yml`, `.toml`, `.txt`, and `.md` files. Hidden files and common generated directories are skipped. Commit `manifest.json` and `manifest.sig` with the release.
|
|
32
|
+
|
|
33
|
+
To sign or verify a different directory, pass it with `--directory` (or `-C`):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
tools sign --directory ./release
|
|
37
|
+
tools sign --verify -C ./release
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Verification checks the signature, each listed file's hash, missing files, and any new signable files that are absent from the manifest.
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
This tool is provided under the GNU General Public License v3.0.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Command line interface for dev-tools."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from dev_tools.tools.signing import SigningError, init_keys, sign_files, verify_files
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description="Developer tools toolkit",
|
|
13
|
+
prog="tools",
|
|
14
|
+
)
|
|
15
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
16
|
+
sign_parser = commands.add_parser("sign", help="Sign or verify release files")
|
|
17
|
+
action = sign_parser.add_mutually_exclusive_group()
|
|
18
|
+
action.add_argument(
|
|
19
|
+
"--init",
|
|
20
|
+
action="store_true",
|
|
21
|
+
help="Generate a signing key pair",
|
|
22
|
+
)
|
|
23
|
+
action.add_argument(
|
|
24
|
+
"--verify",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="Verify the manifest and its files",
|
|
27
|
+
)
|
|
28
|
+
sign_parser.add_argument(
|
|
29
|
+
"-C",
|
|
30
|
+
"--directory",
|
|
31
|
+
type=Path,
|
|
32
|
+
default=Path.cwd(),
|
|
33
|
+
metavar="DIR",
|
|
34
|
+
help="Directory to sign or verify (default: current directory)",
|
|
35
|
+
)
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> int:
|
|
40
|
+
parser = build_parser()
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
if args.command == "sign":
|
|
45
|
+
if args.init:
|
|
46
|
+
init_keys()
|
|
47
|
+
elif args.verify:
|
|
48
|
+
verify_files(args.directory)
|
|
49
|
+
else:
|
|
50
|
+
sign_files(args.directory)
|
|
51
|
+
except (SigningError, OSError) as exc:
|
|
52
|
+
parser.exit(1, f"ERROR: {exc}\n")
|
|
53
|
+
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tools package."""
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Create and verify signed SHA256 manifests for release files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from dev_tools.utils.keys import (
|
|
15
|
+
generate_keys,
|
|
16
|
+
get_private_key,
|
|
17
|
+
get_public_key,
|
|
18
|
+
keys_exist,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SigningError(Exception):
|
|
23
|
+
"""An expected signing or verification failure."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Directories to exclude from scanning.
|
|
27
|
+
EXCLUDE_DIRS = {
|
|
28
|
+
".git", ".venv", "venv", ".env", "__pycache__",
|
|
29
|
+
".mypy_cache", ".pytest_cache", "node_modules",
|
|
30
|
+
"dist", "build", ".tox",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# File extensions to sign.
|
|
34
|
+
SIGN_EXTENSIONS = {".py", ".json", ".yaml", ".yml", ".toml", ".txt", ".md"}
|
|
35
|
+
HASH_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def hash_file(path: Path) -> str:
|
|
39
|
+
"""Compute a SHA256 hash without loading the entire file into memory."""
|
|
40
|
+
digest = hashlib.sha256()
|
|
41
|
+
with path.open("rb") as file_obj:
|
|
42
|
+
for chunk in iter(lambda: file_obj.read(1024 * 1024), b""):
|
|
43
|
+
digest.update(chunk)
|
|
44
|
+
return digest.hexdigest()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def discover_files(root: Path) -> List[Path]:
|
|
48
|
+
"""Find signable regular files, returning paths relative to ``root`` sorted."""
|
|
49
|
+
root = Path(root)
|
|
50
|
+
found: List[Path] = []
|
|
51
|
+
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
|
|
52
|
+
dirnames[:] = sorted(
|
|
53
|
+
dirname
|
|
54
|
+
for dirname in dirnames
|
|
55
|
+
if dirname not in EXCLUDE_DIRS
|
|
56
|
+
and not dirname.startswith(".")
|
|
57
|
+
and not dirname.endswith(".egg-info")
|
|
58
|
+
and not (Path(dirpath) / dirname).is_symlink()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for filename in sorted(filenames):
|
|
62
|
+
if filename in ("manifest.json", "manifest.sig") or filename.startswith("."):
|
|
63
|
+
continue
|
|
64
|
+
candidate = Path(dirpath) / filename
|
|
65
|
+
if (
|
|
66
|
+
candidate.suffix.lower() in SIGN_EXTENSIONS
|
|
67
|
+
and candidate.is_file()
|
|
68
|
+
and not candidate.is_symlink()
|
|
69
|
+
):
|
|
70
|
+
found.append(candidate)
|
|
71
|
+
|
|
72
|
+
return sorted(found, key=lambda item: item.relative_to(root).as_posix())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _run_openssl(args: List[str], operation: str) -> subprocess.CompletedProcess:
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(args, capture_output=True, text=True, check=False)
|
|
78
|
+
except FileNotFoundError as exc:
|
|
79
|
+
raise SigningError("OpenSSL is required but was not found on PATH.") from exc
|
|
80
|
+
if result.returncode != 0:
|
|
81
|
+
detail = result.stderr.strip() or result.stdout.strip() or "OpenSSL failed."
|
|
82
|
+
raise SigningError(f"{operation} failed: {detail}")
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def sign_manifest(manifest_path: Path, sig_path: Path, private_key: Path) -> None:
|
|
87
|
+
"""Sign a manifest using the configured private key."""
|
|
88
|
+
_run_openssl(
|
|
89
|
+
[
|
|
90
|
+
"openssl", "dgst", "-sha256", "-sign", str(private_key),
|
|
91
|
+
"-out", str(sig_path), str(manifest_path),
|
|
92
|
+
],
|
|
93
|
+
"Signing the manifest",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def verify_manifest(manifest_path: Path, sig_path: Path, public_key: Path) -> bool:
|
|
98
|
+
"""Return whether the signature matches the manifest and public key."""
|
|
99
|
+
try:
|
|
100
|
+
result = subprocess.run(
|
|
101
|
+
[
|
|
102
|
+
"openssl", "dgst", "-sha256", "-verify", str(public_key),
|
|
103
|
+
"-signature", str(sig_path), str(manifest_path),
|
|
104
|
+
],
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
check=False,
|
|
108
|
+
)
|
|
109
|
+
except FileNotFoundError as exc:
|
|
110
|
+
raise SigningError("OpenSSL is required but was not found on PATH.") from exc
|
|
111
|
+
return result.returncode == 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def init_keys() -> None:
|
|
115
|
+
"""Initialize signing keys (first-time setup)."""
|
|
116
|
+
try:
|
|
117
|
+
generate_keys()
|
|
118
|
+
except RuntimeError as exc:
|
|
119
|
+
raise SigningError(str(exc)) from exc
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _release_directory(cwd: Optional[Path]) -> Path:
|
|
123
|
+
directory = Path.cwd() if cwd is None else Path(cwd)
|
|
124
|
+
directory = directory.expanduser().resolve()
|
|
125
|
+
if not directory.is_dir():
|
|
126
|
+
raise SigningError(f"Not a directory: {directory}")
|
|
127
|
+
return directory
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def sign_files(cwd: Optional[Path] = None) -> None:
|
|
131
|
+
"""Sign all signable files in a directory and write its manifest."""
|
|
132
|
+
cwd = _release_directory(cwd)
|
|
133
|
+
|
|
134
|
+
if not keys_exist():
|
|
135
|
+
raise SigningError("Signing keys not found. Run 'tools sign --init' first.")
|
|
136
|
+
|
|
137
|
+
private_key = get_private_key()
|
|
138
|
+
public_key = get_public_key()
|
|
139
|
+
print(f"\nScanning {cwd} for signable files...")
|
|
140
|
+
files = discover_files(cwd)
|
|
141
|
+
if not files:
|
|
142
|
+
print("No signable files found.")
|
|
143
|
+
return
|
|
144
|
+
print(f"Found {len(files)} file(s) to sign.\n")
|
|
145
|
+
|
|
146
|
+
manifest: Dict[str, str] = {}
|
|
147
|
+
for file_path in files:
|
|
148
|
+
relative_path = file_path.relative_to(cwd).as_posix()
|
|
149
|
+
try:
|
|
150
|
+
manifest[relative_path] = hash_file(file_path)
|
|
151
|
+
print(f" ✓ {relative_path}")
|
|
152
|
+
except OSError as exc:
|
|
153
|
+
raise SigningError(f"Could not read {relative_path}: {exc}") from exc
|
|
154
|
+
|
|
155
|
+
manifest_path = cwd / "manifest.json"
|
|
156
|
+
sig_path = cwd / "manifest.sig"
|
|
157
|
+
|
|
158
|
+
# Stage both artifacts first so a failed signing operation leaves the old
|
|
159
|
+
# release files untouched.
|
|
160
|
+
with tempfile.TemporaryDirectory(prefix=".dev-tools-", dir=str(cwd)) as temp_dir:
|
|
161
|
+
staged_manifest = Path(temp_dir) / "manifest.json"
|
|
162
|
+
staged_signature = Path(temp_dir) / "manifest.sig"
|
|
163
|
+
staged_manifest.write_text(
|
|
164
|
+
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
|
|
165
|
+
encoding="utf-8",
|
|
166
|
+
)
|
|
167
|
+
sign_manifest(staged_manifest, staged_signature, private_key)
|
|
168
|
+
|
|
169
|
+
print("\nVerifying signature...")
|
|
170
|
+
if not verify_manifest(staged_manifest, staged_signature, public_key):
|
|
171
|
+
raise SigningError("Signature verification failed; release files were not updated.")
|
|
172
|
+
|
|
173
|
+
os.replace(staged_manifest, manifest_path)
|
|
174
|
+
os.replace(staged_signature, sig_path)
|
|
175
|
+
|
|
176
|
+
print(f"✓ Wrote manifest.json ({len(manifest)} files)")
|
|
177
|
+
print("✓ Signed manifest.sig")
|
|
178
|
+
print("✓ Signature verified ✅\n")
|
|
179
|
+
print("Release ready — commit manifest.json and manifest.sig")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _manifest_file(root: Path, relative_path: str) -> Path:
|
|
183
|
+
"""Resolve a canonical manifest path and ensure it stays inside the release."""
|
|
184
|
+
posix_path = PurePosixPath(relative_path)
|
|
185
|
+
windows_path = PureWindowsPath(relative_path)
|
|
186
|
+
if (
|
|
187
|
+
not relative_path
|
|
188
|
+
or "\\" in relative_path
|
|
189
|
+
or posix_path.is_absolute()
|
|
190
|
+
or windows_path.is_absolute()
|
|
191
|
+
or windows_path.drive
|
|
192
|
+
or posix_path.as_posix() != relative_path
|
|
193
|
+
or not posix_path.parts
|
|
194
|
+
or any(part in (".", "..") for part in posix_path.parts)
|
|
195
|
+
):
|
|
196
|
+
raise SigningError(f"Invalid path in manifest: {relative_path!r}")
|
|
197
|
+
|
|
198
|
+
candidate = root.joinpath(*posix_path.parts)
|
|
199
|
+
resolved_root = root.resolve()
|
|
200
|
+
try:
|
|
201
|
+
resolved_candidate = candidate.resolve()
|
|
202
|
+
except OSError as exc:
|
|
203
|
+
raise SigningError(f"Could not resolve manifest path: {relative_path}") from exc
|
|
204
|
+
try:
|
|
205
|
+
resolved_candidate.relative_to(resolved_root)
|
|
206
|
+
except ValueError as exc:
|
|
207
|
+
raise SigningError(f"Manifest path escapes the release directory: {relative_path}") from exc
|
|
208
|
+
return candidate
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def verify_files(cwd: Optional[Path] = None) -> None:
|
|
212
|
+
"""Verify the manifest signature and every signable file in a directory."""
|
|
213
|
+
cwd = _release_directory(cwd)
|
|
214
|
+
manifest_path = cwd / "manifest.json"
|
|
215
|
+
sig_path = cwd / "manifest.sig"
|
|
216
|
+
|
|
217
|
+
if not manifest_path.is_file() or not sig_path.is_file():
|
|
218
|
+
raise SigningError(f"manifest.json or manifest.sig not found in {cwd}")
|
|
219
|
+
|
|
220
|
+
public_key = get_public_key()
|
|
221
|
+
if not public_key.is_file():
|
|
222
|
+
raise SigningError("Public key not found. Run 'tools sign --init' first.")
|
|
223
|
+
|
|
224
|
+
print(f"Verifying signatures in {cwd}...")
|
|
225
|
+
if not verify_manifest(manifest_path, sig_path, public_key):
|
|
226
|
+
raise SigningError("Manifest signature verification failed.")
|
|
227
|
+
print("✓ Signature verified ✅")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
231
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
232
|
+
raise SigningError(f"Could not read manifest.json: {exc}") from exc
|
|
233
|
+
if not isinstance(manifest, dict):
|
|
234
|
+
raise SigningError("manifest.json must contain an object mapping file paths to SHA256 hashes.")
|
|
235
|
+
|
|
236
|
+
for file_path, expected_hash in manifest.items():
|
|
237
|
+
if not isinstance(file_path, str) or not isinstance(expected_hash, str):
|
|
238
|
+
raise SigningError("Manifest paths and hashes must be strings.")
|
|
239
|
+
if not HASH_RE.fullmatch(expected_hash):
|
|
240
|
+
raise SigningError(f"Invalid SHA256 hash in manifest for {file_path!r}.")
|
|
241
|
+
|
|
242
|
+
print(f"\nChecking {len(manifest)} files...")
|
|
243
|
+
all_match = True
|
|
244
|
+
for relative_path, expected_hash in sorted(manifest.items()):
|
|
245
|
+
file_path = _manifest_file(cwd, relative_path)
|
|
246
|
+
if not file_path.is_file():
|
|
247
|
+
print(f" ✗ Missing: {relative_path}")
|
|
248
|
+
all_match = False
|
|
249
|
+
continue
|
|
250
|
+
try:
|
|
251
|
+
actual_hash = hash_file(file_path)
|
|
252
|
+
except OSError as exc:
|
|
253
|
+
print(f" ✗ Could not read {relative_path}: {exc}")
|
|
254
|
+
all_match = False
|
|
255
|
+
continue
|
|
256
|
+
if actual_hash == expected_hash:
|
|
257
|
+
print(f" ✓ {relative_path}")
|
|
258
|
+
else:
|
|
259
|
+
print(f" ✗ MODIFIED: {relative_path}")
|
|
260
|
+
all_match = False
|
|
261
|
+
|
|
262
|
+
listed_paths = set(manifest)
|
|
263
|
+
for file_path in discover_files(cwd):
|
|
264
|
+
relative_path = file_path.relative_to(cwd).as_posix()
|
|
265
|
+
if relative_path not in listed_paths:
|
|
266
|
+
print(f" ✗ UNLISTED: {relative_path}")
|
|
267
|
+
all_match = False
|
|
268
|
+
|
|
269
|
+
if not all_match:
|
|
270
|
+
raise SigningError("Some files are missing, changed, or absent from the signed manifest.")
|
|
271
|
+
print("\n✓ All files verified ✅")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utilities package."""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Key management utilities for the signing tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_keys_dir(create: bool = False) -> Path:
|
|
12
|
+
"""Return the key directory, creating it only when requested."""
|
|
13
|
+
keys_dir = Path.home() / ".config" / "dev-tools" / "keys"
|
|
14
|
+
if create:
|
|
15
|
+
keys_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
16
|
+
return keys_dir
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_private_key() -> Path:
|
|
20
|
+
"""Get the path to the private key."""
|
|
21
|
+
return get_keys_dir() / "private.pem"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_public_key() -> Path:
|
|
25
|
+
"""Get the path to the public key."""
|
|
26
|
+
return get_keys_dir() / "public.pem"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def keys_exist() -> bool:
|
|
30
|
+
"""Check if both keys exist without creating the key directory."""
|
|
31
|
+
return get_private_key().is_file() and get_public_key().is_file()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run_openssl(args: list[str], description: str) -> None:
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(args, capture_output=True, text=True, check=False)
|
|
37
|
+
except FileNotFoundError as exc:
|
|
38
|
+
raise RuntimeError("OpenSSL is required but was not found on PATH.") from exc
|
|
39
|
+
if result.returncode != 0:
|
|
40
|
+
detail = result.stderr.strip() or result.stdout.strip() or "OpenSSL failed."
|
|
41
|
+
raise RuntimeError(f"{description}: {detail}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def generate_keys() -> None:
|
|
45
|
+
"""Generate an RSA key pair, preserving existing files on failure."""
|
|
46
|
+
keys_dir = get_keys_dir(create=True)
|
|
47
|
+
private_key = get_private_key()
|
|
48
|
+
public_key = get_public_key()
|
|
49
|
+
|
|
50
|
+
if private_key.exists() or public_key.exists():
|
|
51
|
+
print("Keys already exist at:")
|
|
52
|
+
print(f" Private: {private_key}")
|
|
53
|
+
print(f" Public: {public_key}")
|
|
54
|
+
response = input("Overwrite? (y/n): ").strip().lower()
|
|
55
|
+
if response != "y":
|
|
56
|
+
print("Aborted.")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
print(f"Generating RSA keypair in {keys_dir}...")
|
|
60
|
+
with tempfile.TemporaryDirectory(prefix=".keygen-", dir=str(keys_dir)) as temp_dir:
|
|
61
|
+
staged_private = Path(temp_dir) / "private.pem"
|
|
62
|
+
staged_public = Path(temp_dir) / "public.pem"
|
|
63
|
+
_run_openssl(
|
|
64
|
+
["openssl", "genrsa", "-out", str(staged_private), "2048"],
|
|
65
|
+
"Could not generate private key",
|
|
66
|
+
)
|
|
67
|
+
_run_openssl(
|
|
68
|
+
[
|
|
69
|
+
"openssl", "rsa", "-in", str(staged_private), "-pubout",
|
|
70
|
+
"-out", str(staged_public),
|
|
71
|
+
],
|
|
72
|
+
"Could not generate public key",
|
|
73
|
+
)
|
|
74
|
+
staged_private.chmod(0o600)
|
|
75
|
+
staged_public.chmod(0o644)
|
|
76
|
+
os.replace(staged_private, private_key)
|
|
77
|
+
os.replace(staged_public, public_key)
|
|
78
|
+
|
|
79
|
+
print("✓ Keys generated successfully!")
|
|
80
|
+
print(f" Private key: {private_key}")
|
|
81
|
+
print(f" Public key: {public_key}")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: project-enclave-tools
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Tools made mainly to help the devs of project enclave and other sub projects under it
|
|
5
|
+
Author-email: Pradhya <Pradhya@projectenclave.dev>
|
|
6
|
+
Project-URL: Repository, https://github.com/Chinglen2080/dev-tools
|
|
7
|
+
Project-URL: Issues, https://github.com/Chinglen2080/dev-tools/issues
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Dev Tools
|
|
12
|
+
|
|
13
|
+
Dev Tools is a small command-line toolkit for contributors to [Project Enclave](https://github.com/project-enclave). It currently provides signed SHA256 manifests for release files.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install the current public release with `pipx`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pipx install "git+https://github.com/Chinglen2080/dev-tools.git@v0.1.1"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
OpenSSL must be installed and available on your `PATH` for key generation and signing.
|
|
24
|
+
|
|
25
|
+
## Signing release files
|
|
26
|
+
|
|
27
|
+
Generate a 2048-bit RSA key pair the first time you use the signing tool:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
tools sign --init
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The keys are stored in `~/.config/dev-tools/keys/`. Keep `private.pem` private; distribute `public.pem` to anyone who needs to verify your releases. If keys already exist, initialization asks before replacing them.
|
|
34
|
+
|
|
35
|
+
From the release directory, create or update the manifest and signature:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
tools sign
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The manifest contains SHA256 hashes for `.py`, `.json`, `.yaml`, `.yml`, `.toml`, `.txt`, and `.md` files. Hidden files and common generated directories are skipped. Commit `manifest.json` and `manifest.sig` with the release.
|
|
42
|
+
|
|
43
|
+
To sign or verify a different directory, pass it with `--directory` (or `-C`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
tools sign --directory ./release
|
|
47
|
+
tools sign --verify -C ./release
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Verification checks the signature, each listed file's hash, missing files, and any new signable files that are absent from the manifest.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
This tool is provided under the GNU General Public License v3.0.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
dev_tools/cli.py
|
|
4
|
+
dev_tools/init.py
|
|
5
|
+
dev_tools/tools/init.py
|
|
6
|
+
dev_tools/tools/signing.py
|
|
7
|
+
dev_tools/utils/init.py
|
|
8
|
+
dev_tools/utils/keys.py
|
|
9
|
+
project_enclave_tools.egg-info/PKG-INFO
|
|
10
|
+
project_enclave_tools.egg-info/SOURCES.txt
|
|
11
|
+
project_enclave_tools.egg-info/dependency_links.txt
|
|
12
|
+
project_enclave_tools.egg-info/entry_points.txt
|
|
13
|
+
project_enclave_tools.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dev_tools
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "project-enclave-tools"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Tools made mainly to help the devs of project enclave and other sub projects under it"
|
|
9
|
+
authors = [{name = "Pradhya", email = "Pradhya@projectenclave.dev"}]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Repository = "https://github.com/Chinglen2080/dev-tools"
|
|
16
|
+
Issues = "https://github.com/Chinglen2080/dev-tools/issues"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
tools = "dev_tools.cli:main"
|