7. Zero-Knowledge Cryptography

7.6 CISO Access Control Unit & Zero-Plaintext Inboxes

chinwag-acu policy verify --zero-plaintext

Technical specification of crates/chinwag-acu: BLS12-381 pairing-based split-key encryption, CISO fat-client instant revocation over iroh, zero-plaintext-at-rest inboxes with CISO-gated forensic message archiving over S3/R2 Parquet + DuckDB, and zero-re-encryption agent handoffs derived from DASTE (ePrint 2026/1141).

7.6 CISO Access Control Unit & Zero-Plaintext Inboxes

chinwag-acu policy verify --zero-plaintext

Your CISO holds the kill switch. Inboxes store zero plaintext at rest. Agent handoffs never decrypt and re-encrypt. Implemented in crates/chinwag-acu (#![forbid(unsafe_code)]) over BLS12-381 bilinear pairings (e: G1 × G2 → GT), with dynamic ciphertext-only policy evolution derived from the pairing-based DASTE construction (IACR ePrint 2026/1141).

When autonomous coding, review, and QA agents coordinate across a shared Git repository, enterprise CISOs require three hard cryptographic guarantees:

  1. Customer Key Custody & Instant Revocation: Chinwag Cloud must never hold decryption keys, and the CISO must be able to cut off a compromised laptop or rogue agent in 0 ms, even for encrypted payloads already sitting on the bus or workstation.
  2. Zero Plaintext At Rest in Inboxes + CISO-Gated Forensic Archive: No agent or human inbox may store decrypted messages at rest on disk, yet the CISO must retain the ability to authorize forensic decryption of historical agent messages during an incident or compliance audit, even after an agent's key has been revoked and wiped.
  3. Zero-Re-Encryption Multi-Agent Handoffs: When an agent fails at Step 11 and warm-forks (chinwag fork-run) to a second agent, granting the new agent access to the encrypted state must not require decrypting and re-encrypting the payload.

chinwag-acu (crates/chinwag-acu) delivers all three guarantees in a single BLS12-381 pairing envelope.


1. The Canonical 2-of-3 Policy: ACU AND (Coding Agent OR Archive Unit)

Instead of encrypting to a single recipient or storing decrypted messages in an agent's local inbox, every CI/CD root-cause payload and every sent/received agent or human message is encrypted under a canonical 2-of-3 Linear Secret Sharing Scheme (LSSS) policy anchored on the CISO's Access Control Unit (ACU):

Policy = ACU (CISO Fat Client) AND (Recipient (Coding Agent / Human) OR Archive (Message Archive Unit))

Every agent and human automatically CC's the organization's Message Archive Unit (archive:vault) inside the same ciphertext header (3 LSSS rows over a single encrypted payload C_0):

Access PathParticipating Seats (2-of-3)Operational Purpose
Live Execution PathACU + Coding Agent (acu:98939834 + branch:8687872)The agent wakes, requests a 576-byte partial pairing share D_ACU in GT from the CISO's ACU over the iroh P2P bus, combines D_ACU + D_Agent in RAM as its own local combiner, and never writes plaintext to its inbox on disk.
Forensic Archive PathACU + Archive Unit (acu:98939834 + archive:vault)The Archive Unit stores every message encrypted at rest and cannot decrypt anything on its own. If an investigation requires inspecting historical messages, the CISO authorizes release via the ACU, and ACU + Archive Unit co-decrypts the message with a permanent entry in the immutable audit log.
Collusion Attempt (Blocked)Coding Agent + Archive Unit (without ACU)Mathematically impossible (PolicyNotSatisfied). Because the LSSS rows for Coding Agent ([1, 2]) and Archive Unit ([1, 2]) share the same right-hand OR sub-branch, even an attacker who steals both the developer laptop and the Message Archive storage cannot reconstruct (1, 0) without the CISO's ACU share ([1, 1]).

2. Zero-Config CI/CD Policy Resolution via Git (chinwag-config.git)

The chinwag CI/CD runner never calls an external policy database to figure out who should receive a failed build payload. Your Git repository (chinwag-config.git) is the policy directory:

  1. Inspect PR / Commit Provenance: When a CI build runs on a commit or pull request from branch:8687872, the runner extracts the inner Git commit Ed25519 signature and the outer envelope Ed25519 signature.
  2. Validate Against .chinwag/allowed_signers: The runner looks up branch:8687872, acu:98939834, and archive:vault in .chinwag/allowed_signers (mounted from chinwag-config.git). Each record binds the seat's Ed25519 verification key alongside its BLS12-381 pairing public key (pk1 = g1^sk in G1, pk2 = g2^sk in G2), verified for bilinear pairing consistency e(pk1, g2) == e(g1, pk2).
  3. Fail Closed Before Encryption: If either Ed25519 signature or pairing consistency check fails, the runner aborts immediately.
  4. Auto-Synthesize the 2-of-3 Ciphertext: When 20 CI shards fail, the runner collapses them into one root-cause payload, encrypts it via acu_encrypt under acu:98939834 AND (branch:8687872 OR archive:vault), and writes the single encrypted envelope to chinwag-bus.git.

3. CISO Fat Client (chinwag Desktop) Over the iroh P2P Bus

Chinwag Cloud never holds the ACU private key (sk_ACU). Instead, the CISO runs the ACU inside their own chinwag fat client (the native desktop app or an on-prem security node) connected to the fleet over Chinwag's outbound-only iroh QUIC P2P mesh (chinwag-mesh):

┌────────────────────────────────────────────────────────────────────────────┐
│ 1. CI Runner encrypts failed build under: ACU ∧ (Agent_8687872 ∨ Archive)  │
└─────────────────────────────────────┬──────────────────────────────────────┘
                                      │ Outbound :443 (chinwag-bus.git)
                                      ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ 2. Agent_8687872 wakes locally -> dials CISO Fat Client over iroh P2P mesh │
│    Sends signed AcuShareRequest (Row_ACU only; C_0 & payload stay local!)  │
└─────────────────────────────────────┬──────────────────────────────────────┘
                                      │ iroh QUIC Stream (ALPN: /chinwag/acu/1)
                                      ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ 3. CISO Fat Client (acu:98939834)                                          │
│    • Verifies Agent_8687872 Ed25519 signature & live revocation status     │
│    • Appends hash-chained AuditLogEntry to Immutable CISO Audit Log        │
│    • Computes D_ACU = e(C_{ACU,1}, g2) · e(g1, C_{ACU,2})^(-sk_ACU) in GT  │
│    • Streams 576-byte signed AcuShareResponse back to Agent_8687872        │
└─────────────────────────────────────┬──────────────────────────────────────┘
                                      │
                                      ▼
┌────────────────────────────────────────────────────────────────────────────┐
│ 4. Agent_8687872 acts as its own Local Combiner in RAM:                    │
│    • Verifies D_ACU via acu_verify_partial_share                           │
│    • Computes own half D_Agent = acu_partial_decrypt(Row_Agent, sk_Agent)  │
│    • Calls acu_combine(ct, &[D_ACU, D_Agent]) -> unblinds C_0 in RAM!      │
└────────────────────────────────────────────────────────────────────────────┘
  • Zero Plaintext Exposure to the ACU: The CISO's ACU only ever receives its own LSSS row (C_{ACU,1}, C_{ACU,2}), never C_0 or the encrypted CI payload.
  • 0 ms Instant Revocation: The moment the CISO revokes branch:8687872 in the fat client, the ACU refuses to emit D_ACU, rendering every encrypted message or checkpoint held by that agent mathematically undecryptable.

4. Ciphertext-Only Dynamic Policy Evolution (Derived from DASTE)

In standard threshold or attribute-based encryption, changing who can decrypt an existing ciphertext requires somebody holding the secret key to decrypt the payload and re-encrypt it. In an ephemeral CI/CD and multi-agent pipeline, the CI runner that originally encrypted the failure is already gone, and the relay is untrusted.

chinwag-acu adapts the dynamic policy evolution mechanics of the pairing-based DASTE scheme (Decentralized Ad-Hoc Access-Structured Threshold Encryption with Dynamic Policy Evolution, Pradhan & Dutta, ICICS 2026, IACR ePrint 2026/1141) to BLS12-381. Because the LSSS secret shares lambda_j = <M_j, v> sit in the exact prime-order exponent of G1 alongside public basis commitments V_m = g1^(v_m), chinwag-acu updates access policies directly on the ciphertext header without ever decrypting C_0:

Policy Evolution Operationchinwag_acu Rust FunctionWhat It Does on the Ciphertext Header
1. Zero-Decryption Agent Handoff (insert_key / insert_sibling_key)insert_sibling_key(&mut ct, "branch:8687872", &pk_agent2, rng)When Agent 1 hands off or warm-forks at Step 11 (chinwag fork-run) to Agent 2 (branch:77441122), computes g1^(lambda_new) = Sum_m V_m * M_{new,m} in G1 and appends a new row for Agent 2. Policy becomes ACU AND (Agent 1 OR Agent 2 OR Archive) with zero decryption or re-encryption.
2. In-Place Threshold Escalation (escalate_threshold)escalate_threshold(&mut ct, &rows, &dir, delta_t, rng)When a task touches a high-security file scope (crates/chinwag-crypto/**), extends basis_commitments with delta_t fresh column commitments g1^(u_m) and shifts affected rows by Vandermonde powers x_j^m * u_m, escalating a 2-of-2 gate to 3-of-3 (ACU + Agent + Human Architect) in place.
3. Subtree Revocation + Ciphertext Rerandomization (revoke_and_rerandomize)revoke_and_rerandomize(&mut ct, &["branch:8687872"], &dir, rng)Prunes all LSSS rows for branch:8687872, samples a fresh blinding vector delta_v = (delta_s, ...), rotates C_0 += e(g1 * delta_s, g2), updates V_m and surviving rows (ACU and Archive), and increments policy_epoch. Any partial share D_old cached by the revoked agent at epoch 1 is now algebraically useless, while ACU + Archive can still decrypt at epoch 2 for forensic investigations.

5. What Stores the Encrypted Message Archive? (DuckDB Over Cloudflare R2 S3-Compatible Parquet)

You never have to stand up or manage MongoDB, Postgres, or a 24/7 database cluster for the Message Archive Unit (archive:vault). Chinwag uses append-only Parquet files on Cloudflare R2 (via R2's S3-compatible API with $0 egress fees) queried by DuckDB embedded inside the CISO's chinwag fat client:

ComponentTechnologyHow It Works
1. Encrypted Archive StorageCloudflare R2 Parquet Files (S3 API <account_id>.r2.cloudflarestorage.com + WORM Object Lock)Every archived message is written as a Parquet row with cleartext routing columns (timestamp_utc, sender_seat, recipient_seat, task_id, room, policy_epoch, ed25519_sig) and an encrypted payload column (acu_ciphertext, encrypted under ACU AND (Agent OR Archive)).
2. Serverless Investigation Query EngineEmbedded DuckDB (httpfs, threads=2, memory_limit='1GB')Runs off the main GUI thread via tokio::task::spawn_blocking (Arc<Mutex<Connection>>). Queries read_parquet('s3://.../archive.parquet', hive_partitioning = true) over R2 HTTPS Range requests and populates an in-RAM TEMP TABLE active_channel_cache (50 messages per active room) so flipping between channels takes 0 ms and only scrolling past cached history fetches older chunks from R2.
3. In-RAM Forensic Decryptionacu_combine (sk_ACU + sk_Archive)Only the matching acu_ciphertext BLOB rows returned by DuckDB are verified via ed25519_sig, combined with the CISO's ACU share, and decrypted in RAM inside the fat client, logging GrantedArchiveInvestigation to the immutable audit log.

6. Rust API Reference (crates/chinwag-acu)

use chinwag_acu::{
    acu_combine, acu_encrypt, acu_partial_decrypt, acu_verify_partial_share,
    insert_sibling_key, revoke_and_rerandomize, AcuCiphertext, AcuError,
    AccessTree, EncryptedCiPayload, LsssMatrix, SeatSecretKey,
};

// 1. Canonical 2-of-3 Policy: ACU AND (Coding Agent OR Message Archive)
let tree = EncryptedCiPayload::build_acu_recipient_or_archive_policy(
    "acu:98939834",
    "branch:8687872",
    "archive:vault",
);
let lsss = LsssMatrix::from_tree(&tree)?;

// 2. Core Pairing Operations (BLS12-381)
let ct: AcuCiphertext = acu_encrypt(&session_key_gt, &lsss, &directory, &mut rng)?;
let share_acu = acu_partial_decrypt(&ct.rows[0], &sk_acu)?;
assert!(acu_verify_partial_share(&ct.rows[0], &pk_acu, &share_acu)?);
let share_agent = acu_partial_decrypt(&ct.rows[1], &sk_agent1)?;
let recovered_gt = acu_combine(&ct, &[share_acu, share_agent])?;

// 3. Zero-Decryption Handoff to Agent 2 (branch:77441122)
insert_sibling_key(&mut ct, "branch:8687872", &pk_agent2, &mut rng)?;

// 4. Revoke Agent 1 & Rerandomize Ciphertext (Archive + ACU remains decryptable!)
revoke_and_rerandomize(&mut ct, &["branch:8687872"], &directory, &mut rng)?;

Next Section: Zero-Tailscale E2EE Mesh & Open Extension SDK