API reference

This page is the curated orientation to every public class and module in the confium gem. For per-method signatures, see the RBS signatures which are the source of truth.

Module tree

Confium
├── VERSION, core_version, Native   # version strings + extension status
├── SecureBytes                    # zeroize-on-clear byte wrapper
├── Policy                         # jurisdictional + FIPS hooks
├── Error                          # root of typed error hierarchy
│   ├── ParseError
│   ├── ValidationError
│   ├── VerificationError
│   ├── ThresholdError
│   ├── CryptoError
│   ├── NotFoundError
│   ├── IndexError
│   ├── UnresolvedSignerError
│   └── PolicyViolationError
├── PKI
│   ├── Certificate
│   ├── CSR
│   ├── PathValidator
│   ├── CMS
│   │   └── SignedData
│   └── XMLDSig
├── Composite
│   └── Signature
├── Transparency
│   ├── MerkleTree
│   └── InclusionProof
├── Attributes
│   └── Predicate                  # attribute-based threshold DSL
├── TC
│   ├── FrostP256                  # P-256 Shamir + ECDSA
│   ├── ElGamalP256                # threshold ElGamal
│   ├── Cmp20                      # CMP20 threshold ECDSA (in-process)
│   └── Gg18                       # GG18 threshold ECDSA (in-process)
├── Identity
│   └── Actor                      # manufacturer, lab, director, etc.
└── Config
    └── Manifest                   # deployment manifest (TOML)

Confium (top-level)

Confium::VERSION        # => "0.3.2"
Confium.core_version    # => confium-core version the extension was built against
Confium::Native.version # => extension version
Confium::Native.loaded? # => true

Confium::SecureBytes

A byte wrapper that zeroizes its content on garbage collection or explicit clear. Use it for any secret material (private keys, shares, nonces).

secret = Confium::SecureBytes.wrap(binary_string)
secret.bytes    # => String (binary encoding)
secret.bytes!   # => same, but clears the wrapper's copy
secret.length   # => Integer
secret.clear    # zeroizes immediately
secret.cleared? # => true

Confium::PKI::Certificate

cert = Confium::PKI::Certificate.from_pem(File.read("cert.pem"))
cert.not_before           # => Time
cert.not_after            # => Time
cert.fingerprint_sha256   # => String (hex)
cert.serial_hex           # => String
cert.public_key_bytes     # => String (binary)
cert.valid_at?(iso8601)   # => true | false
cert.to_der / cert.to_pem

Confium::PKI::PathValidator

result = Confium::PKI::PathValidator.validate(leaf_cert, nil, root_cert)
# (leaf, intermediates, root [, time_iso8601]) — positional
result.valid?        # => true | false
result.check_count   # => Integer
result.checks_json   # => String (per-check detail)

Confium::Composite::Signature

Composite multi-algorithm signature for PQ migration. See the PQ migration guide.

kp = Confium::Composite.generate_ed25519_keypair
component = Confium::Composite.sign_ed25519(kp["private_key"], message)

sig = Confium::Composite::Signature.new([component])
sig.component_count # => Integer
sig.algorithms      # => ["Ed25519", ...]

result = sig.verify(message)
result.all_verified?  # => true | false
result.per_component  # => { 0 => { "algorithm" => "Ed25519", "verified" => true } }

JSON transport

# Producer: hex-encodes the binary fields for the wire.
json = Confium::Composite::Signature.components_to_json(components)

# Consumer: accepts a JSON string or parsed Array/{"components": [...]}.
sig = Confium::Composite::Signature.from_json(json)
sig.verify(message)

Confium::Transparency::MerkleTree

Append-only Merkle tree (RFC 6962).

tree = Confium::Transparency::MerkleTree.new
seq = tree.append(hash_bytes_32)   # 32-byte binary String
tree.root             # => String (32 bytes, binary)
tree.size             # => integer

proof = tree.inclusion_proof(seq)
proof.sequence        # => integer
proof.steps           # => proof step list
proof.verify(tree.root)  # => true | false
proof.verify_with_leaf(leaf_hash, root)  # => true | false

tree.verify_consistency(old_root, new_root, old_size, new_size, proof)
# => true | raises

Confium::Transparency::OTS — OpenTimestamps anchoring

Real wire-protocol client (no stub, no silent nils — network failures raise IOError):

# Default calendar pool
proof = Confium::Transparency::OTS.stamp(tree.root)   # 32-byte digest
summary = Confium::Transparency::OTS.verify(proof)
# => { "pending" => ["https://alice.btc.calendar.opentimestamps.org"],
#      "bitcoin" => [], "litecoin" => [], "anchored" => false }
proof = Confium::Transparency::OTS.upgrade(proof)     # poll for confirmation

# Custom pool / explicit client
client = Confium::Transparency::OTS::Client.new(["http://my.calendar"])
proof = client.stamp(tree.root)
proof.to_bytes    # the raw .ots file (binary String)
proof.digest      # the anchored digest

The network round trip releases the GVL, so a slow calendar never freezes the VM. Ots::Proof.new(digest, bytes) reconstructs a proof from stored .ots bytes; malformed proofs raise Confium::Transparency::OTS::ParseError.

Confium::Attributes::Predicate

Attribute-based threshold predicate DSL — function-call form over signer attributes.

predicate = Confium::Attributes.parse(
  'and(min_count("role:director", 3), min_distinct("region", 3))'
)

alice = Confium::Attributes::Signer.new
alice.add("role:director", "yes")
alice.add("region", "europe")

predicate.satisfied_by?([alice, bob, carol])  # => true | false

DSL: min_count("attr", n), min_distinct("attr", n), any("attr"), all("attr"), none("attr"), and(...), or(...), not(p) — nesting up to 32 levels.

Confium::TC::FrostP256

P-256 Shamir secret sharing + single-party threshold ECDSA. Used for testing, demos, and as the underlying Shamir primitive for the other TC modules.

kp = Confium::TC::FrostP256.generate_keypair
shares = Confium::TC::FrostP256.split_secret(kp["private_key"], 3, 5)
sig = Confium::TC::FrostP256.sign(kp["private_key"], "message")
# sig["der"], sig["fixed"]

Confium::TC::ElGamalP256

Threshold ElGamal KEM over P-256. Encapsulate produces a fresh shared secret against a public key; any T-of-N custodians can jointly recover it via partial_decrypt + aggregate_partials.

enc = Confium::TC::ElGamalP256.encapsulate(public_key_bytes)
enc["ciphertext"]   # => {"c1" => bytes, "c2" => bytes}
enc["shared_secret"]# => 32 bytes

partial = Confium::TC::ElGamalP256.partial_decrypt(party_idx, share_bytes, enc["ciphertext"])
# partial["party_index"], partial["bytes"]

recovered = Confium::TC::ElGamalP256.aggregate_partials([p1, p2], threshold, enc["ciphertext"])
# recovered == enc["shared_secret"]

Confium::TC::Cmp20 / Confium::TC::Gg18

In-process threshold-ECDSA drivers. CMP20 (Canetti–Makriyannis–Peled 2020) is preferred for new code; GG18 (Gennaro–Goldfeder 2018) is provided for interop with existing deployments.

kg = Confium::TC::Cmp20.keygen(2, 3)
kg["shares"]      # => Array<String> (3 share blobs, 71 bytes each)
kg["public_key"]  # => String (33 bytes SEC1 compressed)

sig = Confium::TC::Cmp20.sign(kg["shares"].first(2), 2, "message")
# sig is a 64-byte binary string (r || s)

# Verify externally with OpenSSL:
require "openssl"
asn1 = OpenSSL::ASN1::Sequence([
  OpenSSL::ASN1::Sequence([
    OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
    OpenSSL::ASN1::ObjectId("prime256v1"),
  ]),
  OpenSSL::ASN1::BitString(kg["public_key"]),
])
pkey = OpenSSL::PKey::EC.new(asn1.to_der)
digest = OpenSSL::Digest::SHA256.digest("message")
r = OpenSSL::BN.new(sig[0, 32], 2)
s = OpenSSL::BN.new(sig[32, 32], 2)
der = OpenSSL::ASN1::Sequence([
  OpenSSL::ASN1::Integer(r),
  OpenSSL::ASN1::Integer(s),
]).to_der
pkey.dsa_verify_asn1(digest, der)   # => true

Both modules share the same shape: keygen(threshold, party_count) returns {"shares" => [...], "public_key" => bytes}, and sign(shares, threshold, message) returns a 64-byte (r, s) signature.

Errors

Failures raise Confium::ThresholdError (subclass of Confium::Error) with structured details accessors:

begin
  Confium::TC::Cmp20.sign(two_shares, 3, "msg")
rescue Confium::ThresholdError => e
  e.have_count   # => 2 (shares supplied)
  e.need_count   # => 3 (threshold required)
end

Security model

The in-process drivers wrap the upstream Rust crate’s real Feldman VSS, Lagrange interpolation, and threshold-ECDSA combine, running under a single trusted process (the coordinator sees all share blobs at combine time). The MtA sub-protocol is the real Paillier construction with GG18/GG20 Appendix A zero-knowledge proofs — exposed below — not a stub. True per-party signing rounds (each signer computes on its share without revealing it) are the Confium::TC::Session surface.

Confium::TC::Cmp20::Mta — the proved MtA sub-protocol

The multiplicative-to-additive conversion threshold ECDSA runs between every signer pair: for nonce k_i and key share x_j, party i ends with alpha and party j with beta such that alpha − beta = k_i · x_j — neither side learns the other’s secret. Every ciphertext carries a zero-knowledge range proof; the responder refuses unproven input and the initiator refuses unbound responses (Confium::TC::Cmp20::MtaError on any forgery). All integers cross the Ruby boundary as hex strings and messages are plain Hashes, so rounds can be JSON-encoded onto a transport.

Mta = Confium::TC::Cmp20::Mta
Q   = Mta::P256_ORDER

# Per-party setup: a Paillier keypair (prime_bits >= 1024 in
# production; N must exceed q^5) and a Strong-RSA commitment key
# whose VERIFIER is the other party.
keypair = Mta.generate_keypair(1536)
ck_i    = Mta.generate_commitment_key(512)
ck_j    = Mta.generate_commitment_key(512)

# One call runs the whole proved exchange under a coordinator:
alpha, beta = Mta.full(
  keypair["public"], keypair["private"], ck_i, ck_j, Q, k_i_hex, x_j_hex
)

# Or drive the three passes explicitly — sound across processes:
# the responder never needs private material.
msg1 = Mta.party_i_init(keypair["public"], ck_j, Q, k_i_hex)
msg2, beta = Mta.party_j_respond(
  keypair["public"], ck_i, ck_j, Q, msg1, x_j_hex
)
alpha = Mta.party_i_finish(
  keypair["public"], keypair["private"], ck_i, Q,
  msg1["ciphertext"], msg2
)

Key direction. The exchange runs under the initiator’s Paillier key end to end: party i encrypts k_i under its own public key, party j responds using only public material (it can never open the initiator’s ciphertext), and only party i can decrypt the response. The share contract forces this — alpha − beta = k_i·x_j exactly, so whoever holds both recovers the peer’s secret — so running the three passes in separate processes is sound: alpha stays with the initiator, beta with the responder. full is the in-process convenience for a coordinator that legitimately holds the initiator’s keypair.

Confium::TC::Session — per-party threshold protocol sessions

One session per signer process, driving the real FROST-ed25519 rounds. Each party feeds round_step the messages received from peers and sends on its own outgoing — the long-term share never leaves the process. This is the per-party half of multi-host signing; NetworkCoordinator is the transport half.

# DKG: every party derives the same group public key.
dkg = %w[p0 p1 p2].each_index.map do |i|
  Confium::TC::Session.new("FROST-ed25519-dkg",
                           parties: %w[p0 p1 p2],
                           threshold: 2, this_party_idx: i)
end
# ... run rounds, broadcasting each round's outgoing messages ...

blob = dkg[0].result  # length-prefixed pubkey || share (72 bytes)

# Signing: the session's local_share is the party's FULL DKG output
# blob (it embeds the group public key FROST needs for the challenge
# and cannot derive from the bare scalar).
sess = Confium::TC::Session.new("FROST-ed25519",
                                parties: %w[p0 p1 p2],
                                threshold: 2, this_party_idx: 0,
                                local_share: blob,
                                message: "payload")
# ... run rounds; every signing party produces the SAME 64-byte
# RFC-8032 signature, verifiable under the group public key.

Accessors: scheme_name, threshold, party_count, this_party_idx, round, complete?. Messages are Hashes with string keys (from, to, round, payload).

Confium::TC::Coordinator — threshold signing coordination

coordinator = Confium::TC::Coordinator.new(quorum_id: "root")
sid = coordinator.create_session(message: data, threshold: 3,
                                 scheme: "CMP20-ECDSA-P256")
coordinator.submit_commitment(sid, signer_id, commitment_bytes)
coordinator.submit_share(sid, signer_id, share_bytes)
signature = coordinator.aggregate(sid)  # 64-byte (r, s), OpenSSL-verifiable

aggregate runs the real CMP20/GG18 combine; below-threshold aggregation raises Confium::ThresholdError with have_count / need_count. The session semantics live in Confium::TC::SigningSession (state machine, per-signer dedup, the combine); Coordinator is the in-process adapter and NetworkCoordinator the TCP/NDJSON adapter over the same sessions. One signer submitting twice never counts twice toward the threshold (duplicate submissions raise Confium::ValidationError), and an unknown session id raises Confium::NotFoundError.

Confium::TC::NetworkCoordinator / SignerClient — multi-host signing

Same coordinator behind TCP, for signers on separate machines (NDJSON protocol, hex-encoded binary fields):

service = Confium::TC::NetworkCoordinator.new(quorum_id: "root").start
client = Confium::TC::SignerClient.new(port: service.port)
sid = client.create_session(message: data, threshold: 3)
client.submit_share(sid, "signer-1", share_blob)
signature = client.aggregate(sid)
service.stop

Transport is plain TCP for loopback/private networks; for Noise-XX encrypted sessions use Confium::Transport below.

Confium::Transport — Noise-XX encrypted coordinator sessions

SignerClient and CoordinatorServer over any registered transport scheme, addressed by URL:

# Trusted network: plain TCP
url = "tcp://127.0.0.1:7800"

# Untrusted network: Noise_XX 25519_ChaChaPoly_BLAKE2s with a stable
# local identity and the coordinator's fingerprint pinned
url = "noise://signing.internal:7800?key=#\{key_hex\}&pinned=#\{fp_hex\}"

server = Confium::Transport::CoordinatorServer.new(url)  # binds at construction
client = Confium::Transport::SignerClient.new(url)

client.register("signer-1", quorum_id)
sid = client.create_session(quorum_id, "CMP20-ECDSA-P256", message, 3, 5)
client.submit_commitment(sid, "signer-1", commitment_bytes)
signature = client.submit_share(sid, "signer-1", share_bytes)  # may aggregate
  • SignerClient.new(url) connects and completes the Noise handshake at construction; noise:// URLs take key=<hex> (local static private key; ephemeral and trust-on-first-use when omitted) and pinned=<hex> (SHA-256 fingerprint of the expected peer static key — the handshake aborts on mismatch)
  • one submit observed as one encrypted frame; a 10-second handshake deadline makes a non-Noise peer fail fast instead of hanging
  • submit_share returns the aggregated signature once the threshold is met, else nil
  • CoordinatorServer is for tests and in-process quorums; production deployments front the coordinator through confium-daemon

Confium::Audit::OtlpSink — OpenTelemetry export

Confium::Audit.sink = Confium::Audit::OtlpSink.new(
  endpoint: "http://localhost:4318/v1/logs",
  headers: { "Authorization" => "Bearer #\{token\}" },
  service_name: "confium-issuer"
)

Every audit record ships to an OTLP collector as a log record (operation/result/algorithm as attributes, success→INFO / failure→ERROR). Delivery failures drop the record with a $stderr notice — telemetry must never break signing.

Confium::Config::Manifest

Deployment manifest (TOML) describing the deployment, tiers, and quorums.

manifest = Confium::Config::Manifest.from_toml(toml_string)
manifest.deployment_name  # => String
manifest.operator         # => String
manifest.tier_count       # => Integer
manifest.tier_name_at(i)  # => String (IndexError when out of range)
manifest.quorum_count     # => Integer
manifest.validate         # => Array<String> — problems, empty when valid
manifest.valid?           # => Boolean

Confium::Store

Keystore access over the store layer, including the remote-sign path (the sign-with-handle contract):

Confium::Store.backends          # => ["memory", "filesystem", ...]
ks = Confium::Store::Keystore.new("memory")
ks.sign(key_id, algorithm, message)  # => binary String

#sign works with remote-holding backends (cloud KMS via source builds that opt into their cargo features); local backends raise a typed Confium::Error (details[:what] explains the capability gap). Errors from the store carry details[:operation].

Confium::Policy

Jurisdictional algorithm/key-size rules and FIPS mode toggle.

Confium::Policy.jurisdiction = :eu        # or :us, :cnml
Confium::Policy.fips_mode = true
Confium::Policy.check!('ecdsa_p256', key_bits: 256)  # => true / raises
Confium::Policy.reset!

See the policy page for details.

Confium::OpenPGP

RFC 9580 armor (encode/decode) is pure Ruby and always available:

Confium::OpenPGP.armor(bytes, Confium::OpenPGP::SIGNATURE)  # => armored String
Confium::OpenPGP.dearmor(armored)                           # => binary String

Signature verification is backed by librnp and ships behind the opt-in pgp cargo feature — the vendored librnp drags in a full Botan/json-c C/C++ build that platform gems deliberately exclude. Check availability at runtime with Confium::OpenPGP::PGP_AVAILABLE (false on platform gems); without the feature the verify methods raise with rebuild instructions instead of pretending:

result = Confium::OpenPGP.verify_detached(message, signature, public_keys)
# => { "any_valid" => true,
#      "signature_count" => 1,
#      "signatures" => [{ "valid" => true, "status" => "Valid",
#                         "key_id" => "5BD652AF...", "creation_time" => 1756...,
#                         "expiration_time" => 0, "hash" => "SHA512" }] }

result = Confium::OpenPGP.verify(clearsigned_data, public_keys)  # inline/clearsigned

public_keys is a String or Array of Strings (armored or binary public-key material) imported into the verification keyring. A cryptographically failed check is the answer, not an exception: any_valid is false with no signature entries. Input that is not an OpenPGP message at all raises Confium::ParseError. Build the extension yourself to enable it:

RB_SYS_CARGO_FEATURES=pgp bundle exec rake compile