epi-recorder 2.1.1__py3-none-any.whl → 2.1.2__py3-none-any.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.
- epi_core/__init__.py +1 -1
- epi_core/container.py +10 -2
- epi_core/schemas.py +6 -1
- epi_core/serialize.py +38 -9
- epi_core/trust.py +10 -0
- epi_recorder/__init__.py +1 -1
- epi_recorder-2.1.2.dist-info/METADATA +574 -0
- {epi_recorder-2.1.1.dist-info → epi_recorder-2.1.2.dist-info}/RECORD +14 -13
- epi_viewer_static/app.js +54 -20
- epi_viewer_static/crypto.js +517 -0
- epi_recorder-2.1.1.dist-info/METADATA +0 -159
- {epi_recorder-2.1.1.dist-info → epi_recorder-2.1.2.dist-info}/WHEEL +0 -0
- {epi_recorder-2.1.1.dist-info → epi_recorder-2.1.2.dist-info}/entry_points.txt +0 -0
- {epi_recorder-2.1.1.dist-info → epi_recorder-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {epi_recorder-2.1.1.dist-info → epi_recorder-2.1.2.dist-info}/top_level.txt +0 -0
epi_core/__init__.py
CHANGED
epi_core/container.py
CHANGED
|
@@ -80,6 +80,8 @@ class EPIContainer:
|
|
|
80
80
|
# Read template and assets
|
|
81
81
|
template_html = template_path.read_text(encoding="utf-8")
|
|
82
82
|
app_js = app_js_path.read_text(encoding="utf-8") if app_js_path.exists() else ""
|
|
83
|
+
crypto_js_path = viewer_static_dir / "crypto.js"
|
|
84
|
+
crypto_js = crypto_js_path.read_text(encoding="utf-8") if crypto_js_path.exists() else ""
|
|
83
85
|
css_styles = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
|
|
84
86
|
|
|
85
87
|
# Read steps from steps.jsonl
|
|
@@ -112,10 +114,16 @@ class EPIContainer:
|
|
|
112
114
|
f'<style>{css_styles}</style>'
|
|
113
115
|
)
|
|
114
116
|
|
|
115
|
-
# Inline app.js
|
|
117
|
+
# Inline crypto.js and app.js
|
|
118
|
+
js_content = ""
|
|
119
|
+
if crypto_js:
|
|
120
|
+
js_content += f"<script>{crypto_js}</script>\n"
|
|
121
|
+
if app_js:
|
|
122
|
+
js_content += f"<script>{app_js}</script>"
|
|
123
|
+
|
|
116
124
|
html_with_js = html_with_css.replace(
|
|
117
125
|
'<script src="app.js"></script>',
|
|
118
|
-
|
|
126
|
+
js_content
|
|
119
127
|
)
|
|
120
128
|
|
|
121
129
|
return html_with_js
|
epi_core/schemas.py
CHANGED
|
@@ -18,7 +18,7 @@ class ManifestModel(BaseModel):
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
spec_version: str = Field(
|
|
21
|
-
default="1.
|
|
21
|
+
default="1.1-json",
|
|
22
22
|
description="EPI specification version"
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -47,6 +47,11 @@ class ManifestModel(BaseModel):
|
|
|
47
47
|
description="Mapping of file paths to their SHA-256 hashes for integrity verification"
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
+
public_key: Optional[str] = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description="Hex-encoded public key used for verification"
|
|
53
|
+
)
|
|
54
|
+
|
|
50
55
|
signature: Optional[str] = Field(
|
|
51
56
|
default=None,
|
|
52
57
|
description="Ed25519 signature of the canonical CBOR hash of this manifest (excluding signature field)"
|
epi_core/serialize.py
CHANGED
|
@@ -87,27 +87,56 @@ def get_canonical_hash(model: BaseModel, exclude_fields: set[str] | None = None)
|
|
|
87
87
|
else:
|
|
88
88
|
return value
|
|
89
89
|
|
|
90
|
+
# Normalize datetime and UUID fields to strings
|
|
90
91
|
model_dict = normalize_value(model_dict)
|
|
91
92
|
|
|
92
93
|
if exclude_fields:
|
|
93
94
|
for field in exclude_fields:
|
|
94
95
|
model_dict.pop(field, None)
|
|
95
|
-
|
|
96
|
+
|
|
97
|
+
# JSON Canonicalization for Spec v1.1+
|
|
98
|
+
# Check if model has spec_version and if it indicates JSON usage
|
|
99
|
+
# We default to CBOR for backward compatibility
|
|
100
|
+
|
|
101
|
+
use_json = False
|
|
102
|
+
|
|
103
|
+
# Check spec_version in model or dict
|
|
104
|
+
spec_version = model_dict.get("spec_version")
|
|
105
|
+
if spec_version and (spec_version.startswith("1.1") or "json" in spec_version):
|
|
106
|
+
use_json = True
|
|
107
|
+
|
|
108
|
+
if use_json:
|
|
109
|
+
return _get_json_canonical_hash(model_dict)
|
|
110
|
+
else:
|
|
111
|
+
return _get_cbor_canonical_hash(model_dict)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_json_canonical_hash(data: Any) -> str:
|
|
115
|
+
"""Compute canonical SHA-256 hash using JSON (RFC 8785 style)."""
|
|
116
|
+
import json
|
|
117
|
+
|
|
118
|
+
# Dump to JSON with sorted keys and no whitespace
|
|
119
|
+
json_bytes = json.dumps(
|
|
120
|
+
data,
|
|
121
|
+
sort_keys=True,
|
|
122
|
+
separators=(',', ':'),
|
|
123
|
+
ensure_ascii=False
|
|
124
|
+
).encode("utf-8")
|
|
125
|
+
|
|
126
|
+
return hashlib.sha256(json_bytes).hexdigest()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_cbor_canonical_hash(data: Any) -> str:
|
|
130
|
+
"""Compute canonical SHA-256 hash using CBOR (Legacy v1.0)."""
|
|
96
131
|
# Encode to canonical CBOR
|
|
97
|
-
# canonical=True ensures:
|
|
98
|
-
# - Keys are sorted lexicographically
|
|
99
|
-
# - Minimal encoding is used
|
|
100
|
-
# - Deterministic representation
|
|
101
132
|
cbor_bytes = cbor2.dumps(
|
|
102
|
-
|
|
133
|
+
data,
|
|
103
134
|
canonical=True,
|
|
104
135
|
default=_cbor_default_encoder
|
|
105
136
|
)
|
|
106
137
|
|
|
107
138
|
# Compute SHA-256 hash
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return hash_obj.hexdigest()
|
|
139
|
+
return hashlib.sha256(cbor_bytes).hexdigest()
|
|
111
140
|
|
|
112
141
|
|
|
113
142
|
def verify_hash(model: BaseModel, expected_hash: str, exclude_fields: set[str] | None = None) -> bool:
|
epi_core/trust.py
CHANGED
|
@@ -53,6 +53,16 @@ def sign_manifest(
|
|
|
53
53
|
SigningError: If signing fails
|
|
54
54
|
"""
|
|
55
55
|
try:
|
|
56
|
+
# Derive public key and add to manifest
|
|
57
|
+
public_key_obj = private_key.public_key()
|
|
58
|
+
public_key_hex = public_key_obj.public_bytes(
|
|
59
|
+
encoding=serialization.Encoding.Raw,
|
|
60
|
+
format=serialization.PublicFormat.Raw
|
|
61
|
+
).hex()
|
|
62
|
+
|
|
63
|
+
# We must update the manifest BEFORE hashing so the public key is signed
|
|
64
|
+
manifest.public_key = public_key_hex
|
|
65
|
+
|
|
56
66
|
# Compute canonical hash (excluding signature field)
|
|
57
67
|
manifest_hash = get_canonical_hash(manifest, exclude_fields={"signature"})
|
|
58
68
|
hash_bytes = bytes.fromhex(manifest_hash)
|
epi_recorder/__init__.py
CHANGED