Spaces:
Sleeping
Sleeping
Michael Stattelman commited on
Commit ·
f75eb4e
1
Parent(s): b5cfae6
Add application file
Browse files- README.md +4 -4
- app.py +81 -8
- engine/verify_attestation.py +64 -5
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: Provenance Verifier
|
| 3 |
emoji: ✔
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: green
|
|
@@ -10,9 +10,9 @@ pinned: false
|
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
-
# Provenance Verifier
|
| 14 |
|
| 15 |
-
A public, no-account
|
| 16 |
itself. Tap a sample, drop your own `.zip` onto the grid, or point it at a
|
| 17 |
Hugging Face repo — the verdict comes straight from the unmodified, audited
|
| 18 |
`verify_attestation.py` from
|
|
@@ -31,7 +31,7 @@ Product: [surgeon.falcons.ai](https://surgeon.falcons.ai) ·
|
|
| 31 |
| --- | --- |
|
| 32 |
| `app.py` | the Gradio page and its three handlers (sample · upload · Hub repo), the cancel control, the five-deep verdict stack with receipts |
|
| 33 |
| `engine_wrapper.py` | the verification wrapper — **every function byte-identical to the FastAPI original** (`_run_engine_sync`, `_parse_stdout`, `_normalize_container`, `_junk_entry`, `_zip_declared_size`, `_resolve_and_verify_repo_sync`, `_dsse_subject_names`, `_result`) |
|
| 34 |
-
| `engine/verify_attestation.py` | the pinned engine, verbatim,
|
| 35 |
| `static/style.css` | the product stylesheet, unchanged; `static/gradio.css` holds the few overrides that hide Gradio's own wrappers |
|
| 36 |
| `samples/` | SAMPLE_A (genuine) · SAMPLE_B (one byte flipped) |
|
| 37 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Provenance Verifier
|
| 3 |
emoji: ✔
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: green
|
|
|
|
| 10 |
license: apache-2.0
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Provenance Verifier · FALCONS.AI Model Surgeon
|
| 14 |
|
| 15 |
+
A public, no-account verifier where anyone can watch a Model Surgeon package prove
|
| 16 |
itself. Tap a sample, drop your own `.zip` onto the grid, or point it at a
|
| 17 |
Hugging Face repo — the verdict comes straight from the unmodified, audited
|
| 18 |
`verify_attestation.py` from
|
|
|
|
| 31 |
| --- | --- |
|
| 32 |
| `app.py` | the Gradio page and its three handlers (sample · upload · Hub repo), the cancel control, the five-deep verdict stack with receipts |
|
| 33 |
| `engine_wrapper.py` | the verification wrapper — **every function byte-identical to the FastAPI original** (`_run_engine_sync`, `_parse_stdout`, `_normalize_container`, `_junk_entry`, `_zip_declared_size`, `_resolve_and_verify_repo_sync`, `_dsse_subject_names`, `_result`) |
|
| 34 |
+
| `engine/verify_attestation.py` | the pinned engine, verbatim from Model Surgeon `tools/verify_attestation.py` (V7.13, 2026-09-08: names the legacy predicate id when it meets one) |
|
| 35 |
| `static/style.css` | the product stylesheet, unchanged; `static/gradio.css` holds the few overrides that hide Gradio's own wrappers |
|
| 36 |
| `samples/` | SAMPLE_A (genuine) · SAMPLE_B (one byte flipped) |
|
| 37 |
|
app.py
CHANGED
|
@@ -21,6 +21,51 @@ Standing rules (BUILD SPEC section 1), how each survives the transport:
|
|
| 21 |
4. Missing capability != tampering -> engine_wrapper (unchanged)
|
| 22 |
5. Weights never re-served -> the verdict stack shows text only;
|
| 23 |
no file output component exists.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
|
| 26 |
import base64
|
|
@@ -75,7 +120,7 @@ _slots = threading.Semaphore(MAX_CONCURRENCY)
|
|
| 75 |
def _busy_result():
|
| 76 |
return _result(
|
| 77 |
"error", None, "",
|
| 78 |
-
detail=("The
|
| 79 |
"try again. " + LOCAL_CMD),
|
| 80 |
)
|
| 81 |
|
|
@@ -228,7 +273,7 @@ def verify_upload(file_path, stack, progress=gr.Progress()):
|
|
| 228 |
received = target.stat().st_size
|
| 229 |
if received > MAX_UPLOAD_BYTES:
|
| 230 |
res = _result("error", None, "", detail=(
|
| 231 |
-
"That file is over this
|
| 232 |
+ " MB upload cap. " + LOCAL_CMD))
|
| 233 |
elif received == 0:
|
| 234 |
res = _result("error", None, "", detail="No file was received. " + LOCAL_CMD)
|
|
@@ -241,7 +286,7 @@ def verify_upload(file_path, stack, progress=gr.Progress()):
|
|
| 241 |
res = _result("error", None, "", detail=(
|
| 242 |
"This archive declares an uncompressed size more than "
|
| 243 |
"twice the " + str(MAX_UPLOAD_MB)
|
| 244 |
-
+ " MB cap, so
|
| 245 |
except zipfile.BadZipFile:
|
| 246 |
pass # let the engine speak for itself
|
| 247 |
if res is None:
|
|
@@ -304,13 +349,41 @@ MASTHEAD = f"""
|
|
| 304 |
<img src="{LOGO}" alt="FALCONS.AI falcon logo" width="72" height="72">
|
| 305 |
</a>
|
| 306 |
<div class="masthead-text">
|
| 307 |
-
<h1>PROVENANCE VERIFIER
|
| 308 |
<p class="byline">by <span class="brand-name">FALCONS.AI</span></p>
|
| 309 |
<p class="sub">free for everyone · no account needed · <span class="brand">Model Surgeon</span></p>
|
| 310 |
</div>
|
| 311 |
</header>"""
|
| 312 |
|
| 313 |
FOOTER = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
<footer class="footer">
|
| 315 |
<nav class="footlinks">
|
| 316 |
<a href="https://surgeon.falcons.ai/pricing">Pricing</a> ·
|
|
@@ -338,11 +411,11 @@ FORCE_DARK_JS = """
|
|
| 338 |
|
| 339 |
def build():
|
| 340 |
samples = _samples()
|
| 341 |
-
with gr.Blocks(title="Provenance Verifier
|
| 342 |
css=CSS, js=FORCE_DARK_JS, analytics_enabled=False,
|
| 343 |
delete_cache=(60, 60), # Gradio's cache: sweep every minute, nothing older than a minute
|
| 344 |
theme=gr.themes.Base(font=gr.themes.GoogleFont("IBM Plex Mono"),
|
| 345 |
-
font_mono=gr.themes.GoogleFont("IBM Plex Mono"))) as
|
| 346 |
stack = gr.State([])
|
| 347 |
gr.HTML(MASTHEAD)
|
| 348 |
|
|
@@ -395,8 +468,8 @@ if __name__ == "__main__":
|
|
| 395 |
# hand-rolled server (a second uvicorn fought the runtime for the port).
|
| 396 |
# SPEC 1.3 holds: Gradio starts uvicorn at log_level="warning", so no
|
| 397 |
# access log is written; this module never logs user input.
|
| 398 |
-
|
| 399 |
-
|
| 400 |
server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 401 |
server_port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))),
|
| 402 |
max_file_size=f"{MAX_UPLOAD_MB}mb", # the streamed upload cap (SPEC 4.2)
|
|
|
|
| 21 |
4. Missing capability != tampering -> engine_wrapper (unchanged)
|
| 22 |
5. Weights never re-served -> the verdict stack shows text only;
|
| 23 |
no file output component exists.
|
| 24 |
+
|
| 25 |
+
# Verify a Model Surgeon package
|
| 26 |
+
|
| 27 |
+
Drop in a signed package and this will tell you whether it is intact, what it
|
| 28 |
+
contains, and where it came from. **Free, permanently. No account, no sign-up,
|
| 29 |
+
nothing stored.**
|
| 30 |
+
|
| 31 |
+
## What it checks
|
| 32 |
+
|
| 33 |
+
- **Integrity** — whether a single byte has changed since the package was signed.
|
| 34 |
+
- **Lineage** — the chain of operations performed on the model, and by whom.
|
| 35 |
+
A model pulled, edited and re-pushed keeps its earlier attestation, so the
|
| 36 |
+
chain holds across multiple hands.
|
| 37 |
+
- **Scope** — attestations name precisely the files they cover, and this reports
|
| 38 |
+
exactly that. No more, no less.
|
| 39 |
+
|
| 40 |
+
## What it does not claim
|
| 41 |
+
|
| 42 |
+
Verification proves that **this file is the file that was signed, and records
|
| 43 |
+
what was done to it.** It does not establish that a model is original, or that
|
| 44 |
+
it is not derived from an open-weight base. That is a different question and
|
| 45 |
+
this tool does not pretend to answer it.
|
| 46 |
+
|
| 47 |
+
## Why it is free
|
| 48 |
+
|
| 49 |
+
Because a verification tool you have to buy from the party being verified is
|
| 50 |
+
not verification. The person who needs convincing should never have to trust
|
| 51 |
+
us, hold an account, or pay anything.
|
| 52 |
+
|
| 53 |
+
The same verifier is open source under Apache-2.0, and **a copy ships inside
|
| 54 |
+
every package** — so verification works offline, in an air-gapped environment,
|
| 55 |
+
and continues to work whether or not we are still here.
|
| 56 |
+
|
| 57 |
+
- **Source (CLI + spec):** https://github.com/Falcons-ai/surgeon-verify
|
| 58 |
+
- **Where the packages come from:** https://surgeon.falcons.ai/?utm_source=huggingface
|
| 59 |
+
|
| 60 |
+
## One result worth knowing
|
| 61 |
+
|
| 62 |
+
During launch testing, this verifier was handed a package signed by a build
|
| 63 |
+
**twenty-five releases earlier**. It honoured the old signature correctly. A
|
| 64 |
+
single byte was then altered — one byte out of roughly fourteen million — and
|
| 65 |
+
the verifier caught the tamper and **named the exact file**.
|
| 66 |
+
|
| 67 |
+
That is the whole point of the thing.
|
| 68 |
+
|
| 69 |
"""
|
| 70 |
|
| 71 |
import base64
|
|
|
|
| 120 |
def _busy_result():
|
| 121 |
return _result(
|
| 122 |
"error", None, "",
|
| 123 |
+
detail=("The verifier is at capacity right now — give it a few seconds and "
|
| 124 |
"try again. " + LOCAL_CMD),
|
| 125 |
)
|
| 126 |
|
|
|
|
| 273 |
received = target.stat().st_size
|
| 274 |
if received > MAX_UPLOAD_BYTES:
|
| 275 |
res = _result("error", None, "", detail=(
|
| 276 |
+
"That file is over this verifier's " + str(MAX_UPLOAD_MB)
|
| 277 |
+ " MB upload cap. " + LOCAL_CMD))
|
| 278 |
elif received == 0:
|
| 279 |
res = _result("error", None, "", detail="No file was received. " + LOCAL_CMD)
|
|
|
|
| 286 |
res = _result("error", None, "", detail=(
|
| 287 |
"This archive declares an uncompressed size more than "
|
| 288 |
"twice the " + str(MAX_UPLOAD_MB)
|
| 289 |
+
+ " MB cap, so it won't be opened here. " + LOCAL_CMD))
|
| 290 |
except zipfile.BadZipFile:
|
| 291 |
pass # let the engine speak for itself
|
| 292 |
if res is None:
|
|
|
|
| 349 |
<img src="{LOGO}" alt="FALCONS.AI falcon logo" width="72" height="72">
|
| 350 |
</a>
|
| 351 |
<div class="masthead-text">
|
| 352 |
+
<h1>PROVENANCE VERIFIER</h1>
|
| 353 |
<p class="byline">by <span class="brand-name">FALCONS.AI</span></p>
|
| 354 |
<p class="sub">free for everyone · no account needed · <span class="brand">Model Surgeon</span></p>
|
| 355 |
</div>
|
| 356 |
</header>"""
|
| 357 |
|
| 358 |
FOOTER = """
|
| 359 |
+
<h1>Verify a Model Surgeon package</h1>
|
| 360 |
+
<p>Drop in a signed package and this will tell you whether it is intact, what it contains, and where it came from. <strong>Free, permanently. No account, no sign-up, nothing stored.</strong></p>
|
| 361 |
+
|
| 362 |
+
<h2>What it checks</h2>
|
| 363 |
+
<ul>
|
| 364 |
+
<li><strong>Integrity</strong> — whether a single byte has changed since the package was signed.</li>
|
| 365 |
+
<li><strong>Lineage</strong> — the chain of operations performed on the model, and by whom. A model pulled, edited and re-pushed keeps its earlier attestation, so the chain holds across multiple hands.</li>
|
| 366 |
+
<li><strong>Scope</strong> — attestations name precisely the files they cover, and this reports exactly that. No more, no less.</li>
|
| 367 |
+
</ul>
|
| 368 |
+
|
| 369 |
+
<h2>What it does not claim</h2>
|
| 370 |
+
<p>Verification proves that <strong>this file is the file that was signed, and records what was done to it.</strong> It does not establish that a model is original, or that it is not derived from an open-weight base. That is a different question and this tool does not pretend to answer it.</p>
|
| 371 |
+
|
| 372 |
+
<h2>Why it is free</h2>
|
| 373 |
+
<p>Because a verification tool you have to buy from the party being verified is not verification. The person who needs convincing should never have to trust us, hold an account, or pay anything.</p>
|
| 374 |
+
<p>The same verifier is open source under Apache-2.0, and <strong>a copy ships inside every package</strong> — so verification works offline, in an air-gapped environment, and continues to work whether or not we are still here.</p>
|
| 375 |
+
<ul>
|
| 376 |
+
<li><strong>Source (CLI + spec):</strong> <a href="https://github.com/Falcons-ai/surgeon-verify">https://github.com/Falcons-ai/surgeon-verify</a></li>
|
| 377 |
+
<li><strong>Where the packages come from:</strong> <a href="https://surgeon.falcons.ai/?utm_source=huggingface">https://surgeon.falcons.ai/?utm_source=huggingface</a></li>
|
| 378 |
+
</ul>
|
| 379 |
+
|
| 380 |
+
<h2>One result worth knowing</h2>
|
| 381 |
+
<p>During launch testing, this verifier was handed a package signed by a build <strong>twenty-five releases earlier</strong>. It honoured the old signature correctly. A single byte was then altered — one byte out of roughly fourteen million — and the verifier caught the tamper and <strong>named the exact file</strong>.</p>
|
| 382 |
+
<p>That is the whole point of the thing.</p>
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
|
| 387 |
<footer class="footer">
|
| 388 |
<nav class="footlinks">
|
| 389 |
<a href="https://surgeon.falcons.ai/pricing">Pricing</a> ·
|
|
|
|
| 411 |
|
| 412 |
def build():
|
| 413 |
samples = _samples()
|
| 414 |
+
with gr.Blocks(title="Provenance Verifier · FALCONS.AI Model Surgeon",
|
| 415 |
css=CSS, js=FORCE_DARK_JS, analytics_enabled=False,
|
| 416 |
delete_cache=(60, 60), # Gradio's cache: sweep every minute, nothing older than a minute
|
| 417 |
theme=gr.themes.Base(font=gr.themes.GoogleFont("IBM Plex Mono"),
|
| 418 |
+
font_mono=gr.themes.GoogleFont("IBM Plex Mono"))) as ui:
|
| 419 |
stack = gr.State([])
|
| 420 |
gr.HTML(MASTHEAD)
|
| 421 |
|
|
|
|
| 468 |
# hand-rolled server (a second uvicorn fought the runtime for the port).
|
| 469 |
# SPEC 1.3 holds: Gradio starts uvicorn at log_level="warning", so no
|
| 470 |
# access log is written; this module never logs user input.
|
| 471 |
+
ui.queue(default_concurrency_limit=MAX_CONCURRENCY)
|
| 472 |
+
ui.launch(
|
| 473 |
server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 474 |
server_port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))),
|
| 475 |
max_file_size=f"{MAX_UPLOAD_MB}mb", # the streamed upload cap (SPEC 4.2)
|
engine/verify_attestation.py
CHANGED
|
@@ -1,4 +1,16 @@
|
|
| 1 |
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
"""
|
| 3 |
FALCONS.AI Model Surgeon — FREE attestation verifier.
|
| 4 |
|
|
@@ -10,12 +22,13 @@ Checks, in order:
|
|
| 10 |
1. the package carries a lineage.intoto.jsonl attestation;
|
| 11 |
2. every subject artifact's SHA-256 matches the signed digest;
|
| 12 |
3. the DSSE HMAC signature verifies under the given key
|
| 13 |
-
(--key or FALCONSAI_HMAC_KEY; defaults to the public
|
| 14 |
|
| 15 |
Exit codes: 0 VERIFIED · 1 TAMPERED/INCOMPLETE · 2 not a Surgeon package.
|
| 16 |
Verification is free by design: signed creation is the paid act, universal
|
| 17 |
verification is what makes the signature worth anything.
|
| 18 |
"""
|
|
|
|
| 19 |
import argparse
|
| 20 |
import base64
|
| 21 |
import hashlib
|
|
@@ -43,13 +56,18 @@ def main():
|
|
| 43 |
print(f"✖ cannot open {a.package}: {e}")
|
| 44 |
sys.exit(2)
|
| 45 |
names = z.namelist()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
prefix = ""
|
| 47 |
roots = {n.split("/", 1)[0] for n in names if n.strip("/")}
|
| 48 |
if len(roots) == 1 and all("/" in n for n in names if n.strip("/")):
|
| 49 |
prefix = next(iter(roots)) + "/" # Explorer-style re-zip: descend
|
| 50 |
|
| 51 |
def _zread(nm):
|
| 52 |
-
return z.read(prefix + nm)
|
| 53 |
try:
|
| 54 |
att = json.loads(_zread("lineage.intoto.jsonl"))
|
| 55 |
except KeyError:
|
|
@@ -62,6 +80,7 @@ def main():
|
|
| 62 |
# key, so verification needs no secret and forgery needs the private
|
| 63 |
# key. Free verification, restored properly.
|
| 64 |
env_missing = False
|
|
|
|
| 65 |
sig_entry = (att.get("signatures") or [{}])[0] or {}
|
| 66 |
scheme = sig_entry.get("scheme") or "hmac"
|
| 67 |
if scheme == "ed25519" and sig_entry.get("publicKey"):
|
|
@@ -91,11 +110,17 @@ def main():
|
|
| 91 |
"key could have produced this package")
|
| 92 |
else:
|
| 93 |
have = None
|
|
|
|
| 94 |
print("signature : legacy shared-key package; supply --key or "
|
| 95 |
"FALCONSAI_HMAC_KEY to check it (digests still verified)")
|
| 96 |
-
sig_ok = hmac.compare_digest(want, have)
|
| 97 |
stmt = json.loads(payload)
|
| 98 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
print(f"tool : {(stmt.get('predicate') or {}).get('tool')}")
|
| 100 |
all_ok = True
|
| 101 |
for sub in stmt.get("subject", []):
|
|
@@ -114,12 +139,46 @@ def main():
|
|
| 114 |
if not ok:
|
| 115 |
print(f" signed {digest}\n actual {actual}")
|
| 116 |
print(f"signature : {'✔ valid under this key' if sig_ok else '✖ does NOT verify under this key'}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
merges = (stmt.get("predicate") or {}).get("merges") or []
|
| 118 |
if merges:
|
| 119 |
print(f"merges : {len(merges)} recorded "
|
| 120 |
f"({', '.join(sorted({m.get('method','lerp') for m in merges}))})")
|
| 121 |
verdict = "VERIFIED" if sig_ok and all_ok and stmt.get("subject") \
|
| 122 |
-
else "INCOMPLETE" if (env_missing and all_ok) \
|
| 123 |
else "TAMPERED" if stmt.get("subject") else "INCOMPLETE"
|
| 124 |
print(f"\nVERDICT: {verdict}")
|
| 125 |
sys.exit(0 if verdict == "VERIFIED" else 1)
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
# Copyright (c) 2026 FALCONS.AI. Licensed under the Apache License, Version 2.0
|
| 4 |
+
# (the "License"); you may not use this file except in compliance with the
|
| 5 |
+
# License. You may obtain a copy of the License at
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 8 |
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
| 9 |
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
| 10 |
+
# License for the specific language governing permissions and limitations.
|
| 11 |
+
# This file — the free verifier — is the open-source component of Model
|
| 12 |
+
# Surgeon (T3-a, operator-ruled Apache-2.0, 2026-08-31). The product itself
|
| 13 |
+
# is proprietary; see LICENSE-verifier and NOTICE in the package.
|
| 14 |
"""
|
| 15 |
FALCONS.AI Model Surgeon — FREE attestation verifier.
|
| 16 |
|
|
|
|
| 22 |
1. the package carries a lineage.intoto.jsonl attestation;
|
| 23 |
2. every subject artifact's SHA-256 matches the signed digest;
|
| 24 |
3. the DSSE HMAC signature verifies under the given key
|
| 25 |
+
(--key or FALCONSAI_HMAC_KEY; defaults to the public key).
|
| 26 |
|
| 27 |
Exit codes: 0 VERIFIED · 1 TAMPERED/INCOMPLETE · 2 not a Surgeon package.
|
| 28 |
Verification is free by design: signed creation is the paid act, universal
|
| 29 |
verification is what makes the signature worth anything.
|
| 30 |
"""
|
| 31 |
+
VERIFIER_VERSION = "2" # D-4 (V6.98): chain-aware
|
| 32 |
import argparse
|
| 33 |
import base64
|
| 34 |
import hashlib
|
|
|
|
| 56 |
print(f"✖ cannot open {a.package}: {e}")
|
| 57 |
sys.exit(2)
|
| 58 |
names = z.namelist()
|
| 59 |
+
# v6.51 container fix (UPSTREAM_NOTE): some Windows tools zip with
|
| 60 |
+
# backslash separators — normalize member names so those packages
|
| 61 |
+
# verify instead of reading "not a Surgeon package".
|
| 62 |
+
canon = {n.replace("\\", "/"): n for n in names}
|
| 63 |
+
names = list(canon)
|
| 64 |
prefix = ""
|
| 65 |
roots = {n.split("/", 1)[0] for n in names if n.strip("/")}
|
| 66 |
if len(roots) == 1 and all("/" in n for n in names if n.strip("/")):
|
| 67 |
prefix = next(iter(roots)) + "/" # Explorer-style re-zip: descend
|
| 68 |
|
| 69 |
def _zread(nm):
|
| 70 |
+
return z.read(canon.get(prefix + nm, prefix + nm))
|
| 71 |
try:
|
| 72 |
att = json.loads(_zread("lineage.intoto.jsonl"))
|
| 73 |
except KeyError:
|
|
|
|
| 80 |
# key, so verification needs no secret and forgery needs the private
|
| 81 |
# key. Free verification, restored properly.
|
| 82 |
env_missing = False
|
| 83 |
+
sig_uncheckable = False
|
| 84 |
sig_entry = (att.get("signatures") or [{}])[0] or {}
|
| 85 |
scheme = sig_entry.get("scheme") or "hmac"
|
| 86 |
if scheme == "ed25519" and sig_entry.get("publicKey"):
|
|
|
|
| 110 |
"key could have produced this package")
|
| 111 |
else:
|
| 112 |
have = None
|
| 113 |
+
sig_uncheckable = True
|
| 114 |
print("signature : legacy shared-key package; supply --key or "
|
| 115 |
"FALCONSAI_HMAC_KEY to check it (digests still verified)")
|
| 116 |
+
sig_ok = have is not None and hmac.compare_digest(want, have)
|
| 117 |
stmt = json.loads(payload)
|
| 118 |
+
print(f"verifier : {VERIFIER_VERSION}")
|
| 119 |
+
pt = stmt.get("predicateType")
|
| 120 |
+
if pt == "https://falcons.ai/model-surgeon/lineage/v1": # packages written before V7.13: the legacy id (same schema)
|
| 121 |
+
print(f"attestation : https://surgeon.falcons.ai/lineage/v1 (predicate type; this package carries the legacy id {pt})")
|
| 122 |
+
else:
|
| 123 |
+
print(f"attestation : {pt}")
|
| 124 |
print(f"tool : {(stmt.get('predicate') or {}).get('tool')}")
|
| 125 |
all_ok = True
|
| 126 |
for sub in stmt.get("subject", []):
|
|
|
|
| 139 |
if not ok:
|
| 140 |
print(f" signed {digest}\n actual {actual}")
|
| 141 |
print(f"signature : {'✔ valid under this key' if sig_ok else '✖ does NOT verify under this key'}")
|
| 142 |
+
# D-4 (V6.98, VERIFIER 2): the lineage CHAIN. A prior attestation, when the
|
| 143 |
+
# package carries one, is checked for payload integrity and — when it was
|
| 144 |
+
# signed by THIS package's key — for signature. A prior that cannot be
|
| 145 |
+
# verified here is reported AMBER; it never makes this package TAMPERED.
|
| 146 |
+
chain = (stmt.get("predicate") or {}).get("chain")
|
| 147 |
+
if chain:
|
| 148 |
+
depth = chain.get("depth")
|
| 149 |
+
pr = chain.get("prior")
|
| 150 |
+
if not pr:
|
| 151 |
+
print(f"chain : depth {depth} · no prior attestation reachable at load")
|
| 152 |
+
else:
|
| 153 |
+
try:
|
| 154 |
+
p_env = pr.get("envelope") or {}
|
| 155 |
+
p_payload = base64.b64decode(p_env.get("payload", ""))
|
| 156 |
+
p_digest = hashlib.sha256(p_payload).hexdigest()
|
| 157 |
+
p_sig = (p_env.get("signatures") or [{}])[0]
|
| 158 |
+
intact = p_digest == pr.get("digest")
|
| 159 |
+
same_key = bool(sig_entry.get("publicKey")) and p_sig.get("keyid") == sig_entry.get("keyid")
|
| 160 |
+
p_ok = None
|
| 161 |
+
if intact and same_key and p_sig.get("scheme") == "ed25519" and p_sig.get("sig"):
|
| 162 |
+
try:
|
| 163 |
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey as _PK
|
| 164 |
+
_PK.from_public_bytes(bytes.fromhex(sig_entry["publicKey"])).verify(
|
| 165 |
+
bytes.fromhex(p_sig["sig"]), p_payload)
|
| 166 |
+
p_ok = True
|
| 167 |
+
except Exception:
|
| 168 |
+
p_ok = False
|
| 169 |
+
state = ("prior VERIFIED (same publisher key)" if p_ok else
|
| 170 |
+
"prior INVALID under this key — AMBER" if p_ok is False else
|
| 171 |
+
"prior intact · signed by another key — not verified here (AMBER)" if intact else
|
| 172 |
+
"prior payload does not match its recorded digest — AMBER")
|
| 173 |
+
print(f"chain : depth {depth} · {state} · prior keyid {str(p_sig.get('keyid') or '?')[:20]} · {len(pr.get('operations') or [])} earlier operation(s)")
|
| 174 |
+
except Exception as e:
|
| 175 |
+
print(f"chain : depth {depth} · prior present but unreadable ({e}) — AMBER")
|
| 176 |
merges = (stmt.get("predicate") or {}).get("merges") or []
|
| 177 |
if merges:
|
| 178 |
print(f"merges : {len(merges)} recorded "
|
| 179 |
f"({', '.join(sorted({m.get('method','lerp') for m in merges}))})")
|
| 180 |
verdict = "VERIFIED" if sig_ok and all_ok and stmt.get("subject") \
|
| 181 |
+
else "INCOMPLETE" if ((env_missing or sig_uncheckable) and all_ok) \
|
| 182 |
else "TAMPERED" if stmt.get("subject") else "INCOMPLETE"
|
| 183 |
print(f"\nVERDICT: {verdict}")
|
| 184 |
sys.exit(0 if verdict == "VERIFIED" else 1)
|