gabriel / musehub public
patch task/commit-sig-verification #1 / 1
AI Agent gabriel · 145 days ago · Apr 29, 2026 · Diff

security: verify Ed25519 commit signatures server-side in wire_push_stream

Prior to this commit, phase 5 of wire_push_stream only checked whether signature and signer_key_id fields were non-empty (a presence check). A pusher who cleared MSign auth could forge any signature string, any agent_id, any model_id — and the server would store it as verified provenance.

Fix: for every commit where signature AND signer_public_key are both present, call verify_commit_ed25519(provenance_payload(...), signature, pub_bytes). Any verification failure rejects the entire push with 422. A commit with signature present but signer_public_key absent is also rejected — we cannot verify without the key and accepting unverifiable claims undermines the provenance guarantee.

Unsigned commits (both fields empty) continue to follow the existing require_signed_commits gate — no behavior change for unsigned workflows.

Covered by 7 new TDD tests in test_commit_signature_verification.py: SV1 valid signature accepted SV2 forged/garbage signature rejected SV3 valid sig + wrong public key rejected SV4 signature without public key rejected SV5 unsigned commit accepted (require_signed_commits=False, regression guard) SV6 unsigned commit rejected (require_signed_commits=True, regression guard) SV7 mixed batch one forged → entire push rejected

sha256:de914b09ef19fc604f6128006c0ac7cf64dfed9a601000897295587050979999 sha
+55 ~6 symbols
sha256:cb201758a87bc4550a750d2193ed725b01ca3035418ff27fbe38aa010b7592e3 snapshot
+55
symbols added
~6
symbols modified
0
dead code introduced
Semantic Changes 61 symbols
+ _commit_pack_frame function function _commit_pack_frame L81–87
+ _decode_result function function _decode_result L206–207
+ _end_frame function function _end_frame L90–91
+ _fw variable variable _fw L51–51
+ _header_frame function function _header_frame L74–78
+ _make_commit function function _make_commit L109–162
+ _make_repo function async_function _make_repo L173–194
+ _make_settings function function _make_settings L536–545
+ _make_snapshot function function _make_snapshot L165–166
+ _new_keypair function function _new_keypair L98–102
+ _pack function function _pack L66–67
+ _sha256_oid function function _sha256_oid L58–59
+ _stub_r2 function function _stub_r2 L197–203
+ _utc function function _utc L62–63
+ _wrap function function _wrap L70–71
+ AsyncClient import import AsyncClient L28–28
+ AsyncMock import import AsyncMock L23–23
+ AsyncSession import import AsyncSession L29–29
+ Ed25519PrivateKey import import Ed25519PrivateKey L27–27
+ JSONObject import import JSONObject L49–49
+ MuseWireFrameWriter import import MuseWireFrameWriter L48–48
+ MusehubBranch import import MusehubBranch L39–39
+ MusehubRepo import import MusehubRepo L39–39
+ SFRAME_COMMIT_PACK import import SFRAME_COMMIT_PACK L41–41
+ SFRAME_END import import SFRAME_END L41–41
+ SFRAME_ERROR import import SFRAME_ERROR L41–41
+ SFRAME_HEADER import import SFRAME_HEADER L41–41
+ SFRAME_RESULT import import SFRAME_RESULT L41–41
+ StrDict import import StrDict L49–49
+ WIRE_CONTENT_TYPE import import WIRE_CONTENT_TYPE L31–31
+ annotations import import annotations L19–19
+ compute_branch_id import import compute_branch_id L40–40
+ compute_identity_id import import compute_identity_id L40–40
+ compute_repo_id import import compute_repo_id L40–40
+ datetime import import datetime L22–22
+ encode_pubkey import import encode_pubkey L38–38
+ encode_public_key import import encode_public_key L32–32
+ hashlib import import hashlib L21–21
+ msgpack import import msgpack L25–25
+ provenance_payload import import provenance_payload L32–32
+ public_key_fingerprint import import public_key_fingerprint L38–38
+ pytest import import pytest L26–26
+ sign_commit_ed25519 import import sign_commit_ed25519 L32–32
+ sign_commit_record import import sign_commit_record L32–32
+ timezone import import timezone L22–22
+ test_sv1_valid_signature_accepted function async_function test_sv1_valid_signature_accepted L215–236
+ test_sv2_forged_signature_rejected function async_function test_sv2_forged_signature_rejected L244–285
+ test_sv3_wrong_public_key_rejected function async_function test_sv3_wrong_public_key_rejected L293–348
+ test_sv4_signature_without_public_key_rejected function async_function test_sv4_signature_without_public_key_rejected L356–399
+ test_sv5_unsigned_commit_accepted_when_signing_not_required function async_function test_sv5_unsigned_commit_accepted_when_signing_not_required L407–435
+ test_sv6_unsigned_commit_rejected_when_signing_required function async_function test_sv6_unsigned_commit_rejected_when_signing_required L443–471
+ test_sv7_mixed_batch_one_forged_rejects_all function async_function test_sv7_mixed_batch_one_forged_rejects_all L479–529
~ musehub/services/musehub_wire.py .py 3 symbols added, 4 symbols modified
+ decode_pubkey import import decode_pubkey L68–68
+ provenance_payload import import provenance_payload L69–69
+ verify_commit_ed25519 import import verify_commit_ed25519 L69–69
← Older Oldest on task/commit-sig-verification
All commits
Newer → Latest on task/commit-sig-verification

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:de914b09ef19fc604f6128006c0ac7cf64dfed9a601000897295587050979999 --body "your comment"