2626from typing import Iterable
2727
2828from 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
3033from wcm import (
3134 Ed25519Signer ,
3538 WeightCustodyManifest ,
3639 generate_ed25519 ,
3740 generate_transport_keypair ,
41+ model_signing_digest ,
3842 open_sealed ,
3943 verify_manifest ,
44+ verify_provenance ,
4045)
4146
4247from real_open_model import artifact_files , build_manifest , resolve_artifact , sha256_artifact
4348
4449FORMAT = "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+
4774def _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 )))
0 commit comments