KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

SPEC-RELEASE-THRESHOLD — BLS12-381 threshold signatures for kit releases

Status: Draft. This repository does not implement this specification — see §6. The document fixes the wire bytes and the verifier path for a future implementation.

This spec is normative for the wire bytes a threshold-signed release ships and the verifier path that consumes them. It is not a replacement for SPEC-ATTESTATIONS.md — DSSE envelope shape, in-toto v1 Statement shape, JCS canonicalization, and attestation_id construction are all inherited unchanged. This document only describes the signer/verifier pair that produces a single signature standing in for M maintainers.


1. Motivation — "release party"

kit's release process today (docs/RELEASE.md) is cut by one human running git tag -s vX.Y.Z. The cosign keyless signatures come from GitHub Actions OIDC; the release-tag signature is one person.

For a project that wants N maintainers to co-sign a release without each one having to publish a separate signature, BLS12-381 threshold signatures over the commonware-cryptography crate provide:

The "release party" framing: a release is cut by M-of-N maintainers each posting their partial signature to a coordination channel; an aggregator combines them; the resulting BLS signature lands on the GitHub Release alongside the existing cosign keyless signature. No single maintainer can produce a valid release signature alone.

This spec defines the signer/verifier pair (§3-§4) and the keystore backend for share storage (§6; SPEC-KEYSTORE.md). The dealer/aggregator CLI and CI integration that make the ceremony usable end-to-end (§5) are the one piece not yet built.

1.1 Non-goals


2. Trust model

2.1 Trusted dealer

A single party (the project lead, in practice) runs kit_attest::bls_threshold_trusted_dealer(rng, n) and hands each maintainer their Share, plus the cohort's public Sharing<MinSig> (which every holder also keeps a copy of for partial-verification).

Trust assumptions of the trusted dealer:

  1. The dealer is honest at the moment of dealing — they do not retain the unsplit secret after distribution and do not collude with any M-1 maintainers.
  2. Share distribution to maintainers happens over a confidential channel (HSM-backed envelope, encrypted handoff over Signal, etc.).
  3. The fault model is N3f1 (commonware's Faults trait): quorum = ceil(2n/3). At n=4 this gives 3-of-4; at n=7, 5-of-7.

The dealer assumption is explicitly time-bounded: it holds for the window between key generation and the first DKG resharing. Public commitment to the cohort public key (for example published in the project README and pinned in trust-roots TOML) closes the window: any future deal that doesn't match the published key is rejected.

2.2 DKG

commonware-cryptography::bls12381::dkg provides the distributed-key-generation protocol. No single party ever sees the unsplit secret. The DKG ceremony reuses every piece of the signer/verifier machinery (ThresholdSigner, aggregate, verify) because the only thing that changes is how shares are produced.

The DKG plan is the subject of a separate spec; this document only guarantees that the on-disk share format the keystore backend defines is compatible with both ceremonies.


3. Wire shape

3.1 Partial signature (in-flight, holder → aggregator)

A PartialSignature<MinSig> per commonware-cryptography:

+---------------------+------------------------------+
| index: Participant  | value: V::Signature (G1)     |
| (4 bytes, u32 BE)   | (48 bytes, G1 compressed)    |
+---------------------+------------------------------+

Total: 52 bytes on the wire. kit_attest::bls_threshold ships this via the commonware_codec::Encode impl on PartialSignature; the release-party ceremony CLI wraps it in a thin JSON envelope for human-friendly coordination-channel transport.

3.2 Aggregated signature (on-disk, in DSSE envelope)

A V::Signature per commonware-cryptography, with the MinSig variant:

+----------------------------------+
| signature: G1 compressed         |
| (48 bytes — SIGNATURE_SIZE)      |
+----------------------------------+

48 bytes total. This is what lands in the DSSE envelope's signatures[i].sig field, base64-encoded per DSSE §3.

3.3 Aggregated public key

A V::Public per commonware-cryptography:

+----------------------------------+
| public_key: G2 compressed        |
| (96 bytes — PUBLIC_KEY_SIZE)     |
+----------------------------------+

96 bytes. The DSSE keyid carries this as bls12381-thr:<192 hex chars> (205 chars total: 13-char prefix + 192 hex). The verifier registry maps this keyid to the public key bytes for the BLS verify step.

3.4 Algorithm wire identifier

The kit-rpc Algorithm proto enum has a fixed integer for this algorithm:

ALGORITHM_BLS12381_THRESHOLD = 5;

The integer is load-bearing: the buffa codegen pins it. The variant appears in SignerFrame.SignResponse.algorithm when a future external signer produces a partial; today the adapter is in-process and does not emit a SignerFrame, but the integer is already reserved so an external BLS signer subprocess can use it.

3.5 Variant choice

MinSig (signature in G1, public key in G2) over MinPk (the reverse). Rationale: each release ships one aggregated signature in the SLSA bundle; the public key is registered once in the verifier trust root and amortized across every release. This minimizes the signature side (48 bytes vs. 96).

3.6 Ciphersuite

The signature scheme is the IETF BLS-signature construction over BLS12-381 with a proof-of-possession scheme, using the MinSig variant selected in §3.5:

BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_POP_

This identifies, per draft-irtf-cfrg-bls-signature: the curve (BLS12-381), the signature group (G1, that is, MinSig), the hash-to-curve suite (XMD:SHA-256_SSWU_RO_, an RFC 9380 expand-message-XMD construction with SHA-256 and the Simplified Shallue–van de Woestijne– Ulas map), and the proof-of-possession scheme (POP_, which requires each participating key to have a separately-verified proof of possession before it may contribute to an aggregate — closing the rogue-key attack that plain BLS aggregation is vulnerable to without one). This is a real, standard IETF ciphersuite; it is not a kit- or commonware-specific construction, and any BLS12-381 implementation conforming to that draft and RFC 9380 can be checked against it.


4. Verifier algorithm

Given:

The verifier:

  1. Reject if aggregate_pubkey.len() != 96.
  2. Decode aggregate_pubkey as a G2 point (commonware V::Public::decode); reject on decode failure.
  3. Reject if signature.len() != 48.
  4. Decode signature as a G1 point; reject on decode failure.
  5. Run ops::verify_message::<MinSig>(&pk, None, pae, &sig) — the single BLS verify of pae against the hash-to-curve of §3.6. The internal computation is one optimal-ate pairing check.

Failure of any step is fatal: the verifier MUST NOT accept the signature. Pass/fail is binary; there is no per-share verdict at the aggregated layer (that's a feature, not a bug — the aggregate is the unit of consensus).

kit_attest::bls_threshold_verify exposes this as a single function. TrustRoot::Bls12381ThresholdPubKey is wired into verify_envelope's registry dispatch (kit-attest/src/verify.rs, feature-gated behind bls-threshold), so callers verifying a full DSSE envelope get this check automatically without calling bls_threshold_verify directly.


5. Ceremony walkthrough

No CLI implements this ceremony. This section describes the flow that an implementation follows.

5.1 Genesis (one-time)

release-party deal --maintainers alice,bob,carol,dave \
                   --threshold-mode N3f1 \
                   --out cohort.json

Output:

5.2 Per-release (every vX.Y.Z)

  1. The release coordinator (could be any maintainer) builds the release artifacts and computes the DSSE PAE for the in-toto v1 Statement claiming the release tag as subject.
  2. Coordinator posts the PAE bytes to a coordination channel (Signal group, GitHub Actions issue comment, OpenBao audit-log channel — pick one, document it).
  3. Each maintainer pulls the PAE, runs release-party sign --share ~/.kit/release-share.json --pae <bytes>, which:
    • Loads the share via the keystore backend.
    • Computes the partial signature via ThresholdSigner::sign(pae).
    • Emits the 52-byte partial as a base64-wrapped JSON object the maintainer posts back to the channel.
  4. Coordinator collects M partials (M = bls_threshold_for(n)), runs release-party aggregate --partials partial-*.json --cohort cohort.json, which calls bls_threshold_aggregate and emits a 48-byte signature.
  5. Coordinator wraps the signature in a DSSE envelope with keyid = bls12381-thr:<hex> and pushes the envelope to the GitHub Release alongside the cosign keyless signature.

5.3 Rotation

Not yet implemented (§2.2). The high-level shape: a quorum of current holders runs the keysharing protocol to mint a new share set; the public key does not change, so existing trust-roots TOML pins keep working. Maintainer set churn is invisible at the verifier layer — by design.


6. Implementation status

No code in this repository signs, aggregates or verifies a threshold signature. kit-attest has no BLS algorithm variant. kit-keystore stores no shares. A trust-roots file entry of kind bls12381-thr is ignored, so a keyid of that kind reports as unknown. The algorithm identifier ALGORITHM_BLS12381_THRESHOLD = 5 stays reserved in common.proto.


7. Open questions


8. Invariants

Invariant Enforced by
No single maintainer can produce a valid release signature M-of-N threshold with quorum = ceil(2n/3) under the N3f1 fault model (§1, §2.1)
Verifiers check exactly one signature against exactly one public key, with a binary verdict single verify_message::<MinSig> pairing check; any step failure is fatal, no per-share verdict at the aggregated layer (§4)
Malformed key or signature bytes never reach the pairing fixed sizes (96-byte G2 key, 48-byte G1 signature) and point-decode rejection, verifier steps 1–4 (§3.2, §3.3, §4)
A release signature covers exactly one DSSE PAE the verify of pae in §4 step 5; the PAE binds the payload type
The DSSE keyid resolves to the intended cohort key, not an attacker-supplied one trust-root registry maps bls12381-thr:<hex> to pinned public-key bytes (§3.3, §4)
A re-deal that doesn't match the published cohort key is rejected public commitment to the cohort key (README and trust-roots TOML pin), bounding the trusted-dealer window (§2.1)
Maintainer rotation never invalidates existing verifier pins resharing keeps the aggregated public key unchanged (§1, §5.3)
A stored 1-of-N share cannot be passed off as M-of-N (or swapped across cohorts) BlsShareRecord AAD binds the share to cohort key, holder index, threshold, and total (§7)
Envelope shape, Statement shape, and attestation_id never diverge from ordinary attestations inherited unchanged from SPEC-ATTESTATIONS; this spec defines only the signer/verifier pair (scope statement, preamble)
The BLS aggregate never stands in for build provenance or commit signatures cosign keyless and Ed25519 commit signing remain separate, co-shipped surfaces (§1.1)

Dealer honesty at the moment of dealing (§2.1 item 1) is a trust assumption, not an enforced invariant; it becomes structural only with DKG (§2.2), which is specified separately.