CRUMB a card from devarno-cloud

Commitment Scheme & CommitmentTypes

aegis intermediate 5 min read

ELI5

A commitment is a locked ballot box: you drop your vote in, lock it, and hand over the locked box. Later you prove you voted correctly by opening the box. AEGIS uses four box types — for user identity, document id, timestamp, and access pattern — so none of those facts leak until deliberately opened.

Technical Deep Dive

Commitment and CommitmentManager live in src/commitments.rs. A commitment hides one of four attributes:

CommitmentTypeHidden attribute
UserCommitmentUser identity
DocumentCommitmentDocument identifier
TimestampCommitmentAccess timestamp
AccessCommitmentAccess pattern / level

Commitment Construction

sequenceDiagram
participant Caller
participant Commitment
participant blake3
Caller->>Commitment: Commitment::new(id, type, value)
Commitment->>blake3: hash(id + now_ms)
blake3-->>Commitment: nonce (32 bytes)
Commitment-->>Caller: Commitment { value, nonce, timestamp_ms }

The nonce is derived as blake3::hash(format!("{}{}", id, now).as_bytes()). This is deterministic given the same id and timestamp, which is a known limitation — callers requiring unpredictability should supply their own nonce via with_nonce(nonce).

Open (Verify) Phase

CommitmentManager::verify_commitment(id, secret, nonce) recomputes:

recomputed = blake3::hash(secret || nonce)

and checks recomputed == commitment.value. The commitment scheme is therefore Commit(secret) = blake3(secret || nonce).

Class Diagram

classDiagram
class Commitment {
+commitment_id: String
+commitment_type: CommitmentType
+value: Vec~u8~
+nonce: Vec~u8~
+timestamp_ms: u64
+is_valid() bool
+size_bytes() usize
}
class CommitmentManager {
-commitments: HashMap~String, Commitment~
+create_commitment(id, type, value) Result~Commitment~
+get_commitment(id) Option~Commitment~
+verify_commitment(id, secret, nonce) bool
+commitment_count() usize
+average_size() usize
}
class CommitmentType {
UserCommitment
DocumentCommitment
TimestampCommitment
AccessCommitment
}
CommitmentManager "1" --> "0..*" Commitment : stores
Commitment --> CommitmentType : typed by

size_bytes

size_bytes() = value.len() + nonce.len(). For a blake3 hash value (32 bytes) and blake3 nonce (32 bytes), a typical commitment is 64 bytes.

Key Terms

  • Commitment(value, nonce) pair hiding a secret attribute; value = blake3(secret || nonce)
  • CommitmentType → Four-variant enum tagging what attribute a commitment hides
  • nonce → 32-byte blake3 hash of (id, timestamp) by default; overridable via with_nonce
  • verify_commitment → Open phase: recomputes blake3(secret || nonce) and compares to stored value
  • hiding → Commitment property: value reveals nothing about the secret without the nonce

Q&A

Q: If two commitments are created with the same id in the same millisecond, do they share the same nonce? A: Yes. The nonce formula is blake3(id + now_ms), so identical (id, timestamp) pairs produce identical nonces. Callers requiring unique nonces must use with_nonce with a random value.

Q: create_commitment inserts with id as key — does a second call with the same id overwrite the first? A: Yes. HashMap::insert replaces the existing entry. The old commitment is dropped.

Q: Does the commitment scheme provide binding (can’t change the secret after committing)? A: Binding holds computationally under blake3 collision resistance. Hiding holds computationally if the nonce is uniformly random; the deterministic default nonce weakens hiding for identical (id, timestamp) inputs.

Examples

Create and open a user identity commitment:

let mut manager = CommitmentManager::new();
let secret = b"user-secret-key";
let nonce = blake3::hash(b"unique-nonce").as_bytes().to_vec();
let value = blake3::hash(&[secret.as_ref(), &nonce].concat()).as_bytes().to_vec();
manager.create_commitment("u1", CommitmentType::UserCommitment, value).unwrap();
// Open phase: verify
let valid = manager.verify_commitment("u1", secret, &nonce);
assert!(valid);

neighbors on the map