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 takekey=<hex>(local static private key; ephemeral and trust-on-first-use when omitted) andpinned=<hex>(SHA-256 fingerprint of the expected peer static key — the handshake aborts on mismatch)- one
submitobserved as one encrypted frame; a 10-second handshake deadline makes a non-Noise peer fail fast instead of hanging submit_sharereturns the aggregated signature once the threshold is met, elsenilCoordinatorServeris for tests and in-process quorums; production deployments front the coordinator throughconfium-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
Related
- Installation
- Error handling
- Policy
- Sinatra verifier quickstart
- RBS signatures (canonical method signatures).