Skip to content

Commit 383012a

Browse files
Bind real LoRA provenance to OpenSSF model signing (#76)
* Verify complete model snapshots * Add real encrypted LoRA custody flow * Harden LoRA evidence reproducibility * Sign real LoRA provenance with OpenSSF
1 parent 1982348 commit 383012a

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

‎weight-custody-manifest/real_lora_custody.py‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
from typing import Iterable
2727

2828
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
29+
from cryptography.hazmat.primitives import serialization
30+
from cryptography.hazmat.primitives.asymmetric import ec
31+
from model_signing import signing as model_signing
2932

3033
from wcm import (
3134
Ed25519Signer,
@@ -35,15 +38,39 @@
3538
WeightCustodyManifest,
3639
generate_ed25519,
3740
generate_transport_keypair,
41+
model_signing_digest,
3842
open_sealed,
3943
verify_manifest,
44+
verify_provenance,
4045
)
4146

4247
from real_open_model import artifact_files, build_manifest, resolve_artifact, sha256_artifact
4348

4449
FORMAT = "wcm-encrypted-derivative/v1"
4550

4651

52+
def sign_artifact(root: pathlib.Path, output: pathlib.Path) -> tuple[str, pathlib.Path, pathlib.Path]:
53+
"""Create a detached OpenSSF signature, retaining no private signing key."""
54+
signature_path = output / "adapter.model-signing.sig"
55+
public_key_path = output / "adapter.model-signing.pub.pem"
56+
key = ec.generate_private_key(ec.SECP256R1())
57+
public_key_path.write_bytes(key.public_key().public_bytes(
58+
serialization.Encoding.PEM,
59+
serialization.PublicFormat.SubjectPublicKeyInfo,
60+
))
61+
with tempfile.TemporaryDirectory(prefix="wcm-model-signing-") as temp:
62+
private_key_path = pathlib.Path(temp) / "signer.key"
63+
private_key_path.write_bytes(key.private_bytes(
64+
serialization.Encoding.PEM,
65+
serialization.PrivateFormat.PKCS8,
66+
serialization.NoEncryption(),
67+
))
68+
model_signing.Config().use_elliptic_key_signer(
69+
private_key=private_key_path
70+
).sign(root, signature_path)
71+
return model_signing_digest(root), signature_path, public_key_path
72+
73+
4774
def _relative_files(root: pathlib.Path) -> Iterable[tuple[pathlib.Path, str]]:
4875
for path in artifact_files(root):
4976
yield path, path.relative_to(root).as_posix()
@@ -193,6 +220,10 @@ def main() -> int:
193220
adapter_path = args.output / "adapter-plaintext"
194221
train_lora(base_path, adapter_path, steps=args.steps)
195222

223+
provenance_digest, provenance_signature, provenance_public_key = sign_artifact(
224+
adapter_path, args.output
225+
)
226+
196227
key = AESGCM.generate_key(bit_length=256)
197228
envelope = encrypt_and_remove_staging(adapter_path, key)
198229
envelope_path = args.output / "adapter.encrypted.json"
@@ -204,7 +235,7 @@ def main() -> int:
204235
# evidence; the identical manifest/KBS path is fed hardware evidence later.
205236
builder, custodian = generate_ed25519(), generate_ed25519()
206237
serving = "sha256:" + hashlib.sha256(b"wcm-local-lora-serving-stack-v1").hexdigest()
207-
manifest = WeightCustodyManifest.model_validate(build_manifest(
238+
manifest_doc = build_manifest(
208239
weights_hash=envelope["artifact_digest"],
209240
license_text="Apache-2.0 + OPAQUE-private-derivative",
210241
serving=serving,
@@ -213,7 +244,16 @@ def main() -> int:
213244
derivatives="none",
214245
derived_from=base_digest,
215246
rights_holder={"base": model_id, "derivative": "OPAQUE"},
216-
))
247+
)
248+
manifest_doc["provenance"] = {
249+
"model_signing": {
250+
"method": "openssf-model-signing",
251+
"signed_digest": provenance_digest,
252+
"transparency": "local-key; publication pending",
253+
"signer": "opaque-wcm-builder",
254+
}
255+
}
256+
manifest = WeightCustodyManifest.model_validate(manifest_doc)
217257
manifest = manifest.with_signatures([
218258
Ed25519Signer(builder).sign(
219259
manifest.unsigned_dict(), role="builder", signer="opaque-wcm-builder"
@@ -260,11 +300,22 @@ def main() -> int:
260300
print("encrypted artifact:", envelope_path)
261301
print("signed manifest :", manifest_path)
262302
print("verification keys:", verification_keys_path)
303+
print("OpenSSF signature:", provenance_signature)
304+
print("OpenSSF public key:", provenance_public_key)
263305
print("KBS release : sealed to ephemeral transport key (software evidence tier)")
264306

265307
with tempfile.TemporaryDirectory(prefix="wcm-lora-") as temp:
266308
verified = pathlib.Path(temp) / "verified-adapter"
267309
decrypt_artifact(envelope, released_key, verified)
310+
provenance = verify_provenance(
311+
manifest,
312+
verified,
313+
provenance_signature,
314+
public_key=provenance_public_key,
315+
)
316+
if not provenance.verified:
317+
raise RuntimeError(f"OpenSSF provenance verification failed: {provenance.reason}")
318+
print("OpenSSF provenance: verified against decrypted exact artifact")
268319
print("verified local derivative before load:", verified)
269320
if args.infer:
270321
print("model output:", repr(run_inference(base_path, verified)))

‎weight-custody-manifest/requirements-lora.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ transformers==5.14.1
44
peft==0.20.0
55
accelerate==1.14.0
66
torch==2.13.0
7+
model-signing==1.1.1

‎weight-custody-manifest/test_real_lora_custody.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from cryptography.exceptions import InvalidTag
66

77
import real_lora_custody
8-
from real_lora_custody import decrypt_artifact, encrypt_artifact
8+
from real_lora_custody import decrypt_artifact, encrypt_artifact, sign_artifact
99

1010

1111
def _adapter(path: pathlib.Path) -> pathlib.Path:
@@ -54,3 +54,22 @@ def fail(_root: pathlib.Path, _key: bytes) -> dict:
5454
with pytest.raises(RuntimeError, match="synthetic encryption failure"):
5555
real_lora_custody.encrypt_and_remove_staging(source, bytes(32))
5656
assert not source.exists()
57+
58+
59+
def test_openssf_signature_covers_complete_adapter(tmp_path: pathlib.Path) -> None:
60+
source = _adapter(tmp_path / "source")
61+
digest, signature, public_key = sign_artifact(source, tmp_path)
62+
assert digest.startswith("sha256:")
63+
assert signature.is_file()
64+
assert public_key.is_file()
65+
66+
from model_signing import verifying
67+
68+
verifying.Config().use_elliptic_key_verifier(public_key=public_key).verify(
69+
source, signature
70+
)
71+
(source / "adapter_model.safetensors").write_bytes(b"tampered")
72+
with pytest.raises(Exception):
73+
verifying.Config().use_elliptic_key_verifier(public_key=public_key).verify(
74+
source, signature
75+
)

0 commit comments

Comments
 (0)