Universal API
The Universal Cryptographic API provides algorithm-agnostic interfaces that work transparently across all classical and post-quantum algorithms. Applications written against this API can switch between ML-KEM, ML-DSA, SLH-DSA, and future algorithms without code changes — the algorithm selection is driven by configurable policies rather than hard-coded function calls.
Overview #
Traditional cryptographic APIs tightly couple applications to specific algorithms. Switching from RSA to ECDSA, or from ECDH to ML-KEM, requires rewriting cryptographic code paths, updating key management logic, and retesting. The Universal API eliminates this coupling by abstracting all cryptographic operations behind unified interfaces with opaque key handles and policy-driven algorithm selection.
The core abstraction is simple: instead of calling kyber_keygen() or dilithium_sign(), applications call qc_key_generate() and qc_sign() with a policy object that specifies security requirements. The SDK's Algorithm Adaptation Engine selects the optimal algorithm based on the policy, available hardware capabilities, and compliance constraints.
// Traditional approach — algorithm-specific, brittle
pqc_kyber_keypair(&pk, &sk);
pqc_kyber_encrypt(&ct, msg, &pk);
// Universal API approach — algorithm-agnostic, crypto-agile
qc_key_handle_t key;
qc_key_generate(&key, QC_ALG_POLICY_DEFAULT, QC_SECURITY_LEVEL_HIGH);
qc_encrypt(&ciphertext, plaintext, key, QC_MODE_DEFAULT);
Design Principles #
Algorithm Independence: No function signature contains an algorithm name. Operations are expressed as abstract cryptographic goals (key exchange, digital signature, encryption) and the policy engine resolves these to concrete algorithms at runtime.
Opaque Handles: All cryptographic objects (keys, ciphertexts, signatures) are represented as opaque handles. Applications never manipulate raw key bytes directly — the SDK manages memory allocation, secure storage, and zeroization internally.
Policy-Driven Selection: Algorithm selection is driven by declarative policies that express security requirements, performance constraints, compliance needs, and deployment context. Policies can be defined in code, configuration files, or centralized policy servers.
Zero-Copy Where Possible: The API is designed to minimize data copying between application memory and hardware. DMA-friendly buffer alignment is handled transparently when hardware acceleration is available.
Thread Safety: All Universal API functions are thread-safe. Multiple threads can perform concurrent operations on the same device using separate key handles. The SDK manages internal locking and hardware queue multiplexing.
Opaque Handle Model #
Every cryptographic object in the Universal API is represented by an opaque handle — a pointer-sized value that references internal SDK state. Handles encapsulate the algorithm-specific key material, metadata, hardware references, and security properties.
// All handle types are opaque — no internal fields are exposed
typedef struct qc_key_handle_s* qc_key_handle_t;
typedef struct qc_ciphertext_s* qc_ciphertext_t;
typedef struct qc_signature_s* qc_signature_t;
typedef struct qc_shared_secret_s* qc_shared_secret_t;
// Handle lifecycle
qc_key_handle_t key;
qc_key_generate(&key, policy, level); // Create
qc_encrypt(&ct, pt, key, mode); // Use
qc_key_info(key, &info); // Inspect
qc_key_export(key, &buf, QC_FMT_PEM); // Export (if policy permits)
qc_key_destroy(&key); // Destroy (zeroizes memory)
Handles provide several security advantages over raw byte arrays. Key material never appears in application address space — it stays within SDK-managed (or hardware-managed) secure memory. Handles are validated on every API call, preventing use-after-free and dangling-pointer attacks. And destruction is guaranteed to zeroize all associated memory, including any hardware key slots.
Handles can be serialized for storage or transmission using qc_key_export() and qc_key_import(), supporting PEM, DER, and PKCS#8 formats. Export may be restricted by policy — keys generated with QC_KEY_NON_EXTRACTABLE cannot be exported from the device.
Unified Key Generation #
The qc_key_generate() function generates a key pair appropriate for the requested operation type and security level. The algorithm is selected by the policy engine based on the current configuration.
// Generate a key for key exchange (KEM)
qc_key_handle_t kem_key;
qc_result_t rc = qc_key_generate(&kem_key,
QC_OP_KEY_EXCHANGE, // Operation type
QC_SECURITY_LEVEL_HIGH, // NIST Level 3
QC_KEY_HW_BOUND); // Key never leaves hardware
// Generate a key for digital signatures
qc_key_handle_t sig_key;
rc = qc_key_generate(&sig_key,
QC_OP_DIGITAL_SIGNATURE, // Operation type
QC_SECURITY_LEVEL_MEDIUM, // NIST Level 2
QC_KEY_EXTRACTABLE); // Can export public key
// Query what algorithm was selected
qc_key_info_t info;
qc_key_info(kem_key, &info);
printf("Algorithm: %s\n", info.algorithm_name); // e.g., "ML-KEM-768"
printf("Key size: %zu bytes\n", info.public_key_size);
Key generation flags control storage, extractability, and lifecycle properties. QC_KEY_HW_BOUND ensures the private key never leaves the QUAC 100 hardware — even the SDK cannot read it. QC_KEY_EPHEMERAL marks keys for single-use and automatic destruction after one operation.
KEM Operations #
Key Encapsulation Mechanism (KEM) operations establish shared secrets between two parties. The Universal API uses the standard encapsulate/decapsulate model that works across ML-KEM (Kyber), Classic McEliece, BIKE, HQC, and future KEM algorithms.
// Sender: encapsulate using recipient's public key
qc_ciphertext_t ct;
qc_shared_secret_t ss;
rc = qc_encapsulate(&ct, &ss, recipient_public_key);
// Transport ct to recipient...
// Recipient: decapsulate using their private key
qc_shared_secret_t recovered_ss;
rc = qc_decapsulate(&recovered_ss, &ct, recipient_private_key);
// Both parties now share the same secret
// Use for symmetric encryption, key derivation, etc.
uint8_t secret_bytes[32];
qc_shared_secret_export(ss, secret_bytes, 32);
// Cleanup — zeroizes secret material
qc_shared_secret_destroy(&ss);
qc_shared_secret_destroy(&recovered_ss);
qc_ciphertext_destroy(&ct);
The shared secret is always 32 bytes regardless of the underlying KEM algorithm. This uniformity simplifies key derivation and downstream symmetric cryptography.
Signature Operations #
Digital signature operations follow the conventional sign/verify pattern with algorithm-agnostic interfaces that support ML-DSA (Dilithium), SLH-DSA (SPHINCS+), and hybrid combinations.
// Sign a message
qc_signature_t sig;
rc = qc_sign(&sig, message, message_len, signing_key, QC_HASH_DEFAULT);
// Verify a signature
bool valid;
rc = qc_verify(&valid, message, message_len, &sig, verification_key);
if (rc == QC_SUCCESS && valid) {
// Signature is valid
}
// Streaming signature for large messages
qc_sign_ctx_t ctx;
qc_sign_init(&ctx, signing_key, QC_HASH_DEFAULT);
qc_sign_update(&ctx, chunk1, chunk1_len);
qc_sign_update(&ctx, chunk2, chunk2_len);
qc_sign_final(&ctx, &sig);
The streaming interface (init/update/final) is essential for signing large files or network streams without buffering the entire message in memory. Internally, the SDK hashes the message using the algorithm's required hash function (SHA-3 for ML-DSA, SHAKE for SLH-DSA) and submits only the digest to the hardware for signature computation.
QC_HASH_DEFAULT selects the hash function mandated by the underlying algorithm's NIST specification. Applications can override this for algorithms that support hash flexibility, but the default is strongly recommended for compliance.
Algorithm Policy Engine #
The Algorithm Policy Engine is the intelligence layer that maps abstract security requirements to concrete algorithm selections. Policies can be defined programmatically, loaded from configuration files, or retrieved from centralized policy servers.
// Programmatic policy definition
qc_security_policy_t policy = {
.classification_level = QC_CLASS_SECRET,
.threat_model = QC_THREAT_NATION_STATE,
.compliance_frameworks = QC_COMPLIANCE_FIPS_140_3 | QC_COMPLIANCE_CC_EAL4,
.key_escrow_required = true,
.audit_level = QC_AUDIT_DETAILED,
.side_channel_protection = QC_SC_PROTECTION_FULL,
.max_latency_us = 100,
.prefer_hardware = true
};
// Apply policy globally
qc_set_global_policy(&policy);
// Or per-operation
qc_key_generate(&key, QC_OP_DIGITAL_SIGNATURE,
QC_SECURITY_LEVEL_HIGH, QC_KEY_POLICY(&policy));
Configuration file policies (quac_policy.json):
{
"default_policy": {
"kem": { "algorithm": "auto", "min_security_level": 3 },
"signature": { "algorithm": "auto", "min_security_level": 2 },
"compliance": ["FIPS-140-3"],
"hybrid_mode": "required"
}
}
The engine evaluates policies using multi-criteria decision making that considers security level requirements, hardware capability (which algorithms the device accelerates), compliance constraints (FIPS-approved algorithms only), performance bounds, and organizational preferences. When multiple algorithms satisfy all constraints, the engine selects the highest-performing option.
Security Level Mapping #
The Universal API uses NIST security levels as its primary abstraction for cryptographic strength. Applications request a security level and the policy engine maps it to appropriate parameter sets.
| Security Level | NIST Equivalent | ML-KEM | ML-DSA | SLH-DSA | Classical Equivalent |
|---|---|---|---|---|---|
QC_SECURITY_LEVEL_LOW | Level 1 | ML-KEM-512 | ML-DSA-44 | SLH-DSA-128f | AES-128 |
QC_SECURITY_LEVEL_MEDIUM | Level 3 | ML-KEM-768 | ML-DSA-65 | SLH-DSA-192f | AES-192 |
QC_SECURITY_LEVEL_HIGH | Level 5 | ML-KEM-1024 | ML-DSA-87 | SLH-DSA-256f | AES-256 |
The f (fast) variants of SLH-DSA are selected by default for their smaller signatures. Applications needing smaller public keys can specify QC_PREFER_SMALL_KEYS to select the s (small) variants instead.
Dynamic Plugin Architecture #
The Universal API supports runtime loading of algorithm implementations through a plugin system. Plugins are shared libraries that implement a standardized interface and advertise their capabilities to the policy engine.
// Plugin registration (inside a plugin shared library)
QC_REGISTER_PLUGIN(my_algorithm, "1.0.0", QC_ALG_TYPE_KEM, {
.key_generate = my_key_generate,
.encapsulate = my_encapsulate,
.decapsulate = my_decapsulate,
.capabilities = QC_CAP_HARDWARE_ACCEL | QC_CAP_SIDE_CHANNEL_RESISTANT
});
QC_DECLARE_CAPABILITIES({
.security_levels = QC_SECURITY_LEVEL_MEDIUM | QC_SECURITY_LEVEL_HIGH,
.key_sizes = {1024, 2048, 4096},
.performance_class = QC_PERF_CLASS_HIGH_THROUGHPUT
});
Plugins enable several critical capabilities: adding support for new algorithms without SDK updates, integrating proprietary or experimental algorithms, replacing reference implementations with optimized versions, and hot-swapping backends without service interruption in long-running applications.
The SDK ships with built-in plugins for all NIST-standardized PQC algorithms. Additional plugins for classical algorithms (RSA, ECDSA, ECDH) enable hybrid operation and backward compatibility.
Capability Discovery #
Applications can query the system's cryptographic capabilities at runtime to make informed decisions about algorithm selection and feature availability.
// Enumerate all available algorithms
qc_algorithm_info_t* algos;
size_t count;
qc_enumerate_algorithms(&algos, &count);
for (size_t i = 0; i < count; i++) {
printf("%-20s type=%-12s hw=%s fips=%s\n",
algos[i].name, algos[i].type_name,
algos[i].hardware_accelerated ? "yes" : "no",
algos[i].fips_approved ? "yes" : "no");
}
qc_free(algos);
// Check specific capability
bool can_kyber = qc_has_capability(QC_CAP_ML_KEM);
bool can_hw = qc_has_capability(QC_CAP_HARDWARE_ACCEL);
// Performance profiling
qc_perf_profile_t perf;
qc_profile_algorithm(QC_ALG_KYBER_768, &perf);
printf("Keygen: %u ops/sec, Encaps: %u ops/sec\n",
perf.keygen_ops_per_sec, perf.encapsulate_ops_per_sec);
Key Lifecycle Management #
The Universal API provides comprehensive key lifecycle management covering generation, storage, rotation, archival, and destruction. Keys can be stored in software keystore files, hardware-protected slots on the QUAC 100, or external HSMs via PKCS#11.
// Generate with expiry and usage limits
qc_key_policy_t key_policy = {
.extractable = false,
.max_operations = 1000000,
.expiry_time = time(NULL) + (365 * 24 * 3600), // 1 year
.permitted_ops = QC_OP_SIGN | QC_OP_VERIFY,
.storage = QC_STORAGE_HARDWARE
};
qc_key_generate_with_policy(&key, &key_policy);
// Key rotation
qc_key_handle_t new_key;
qc_key_rotate(old_key, &new_key); // Generates new key, archives old
// Secure destruction
qc_key_destroy(&key); // Zeroizes all copies, including hardware slots
All key destruction is performed using constant-time memory zeroization that cannot be optimized away by the compiler. For hardware-bound keys, destruction also triggers the QUAC 100's internal key slot erasure sequence.
Migration Assistance #
The SDK includes migration utilities that help transition existing applications from algorithm-specific APIs (OpenSSL EVP, libsodium, Botan) to the Universal API. The migration helpers provide compatibility shims, automatic code adaptation suggestions, and validation tools to verify that migrated code produces identical results.
// Compatibility shim — wraps Universal API in OpenSSL-like interface
#include <quac100/compat/openssl.h>
// EVP_PKEY* operations transparently use Universal API underneath
EVP_PKEY* pkey = QC_EVP_PKEY_keygen(QC_OP_DIGITAL_SIGNATURE);
// Verify migration correctness
qc_migration_test_t test;
qc_migration_validate("openssl_app", &test);
printf("Compatibility: %s (%d/%d tests passed)\n",
test.compatible ? "PASS" : "FAIL",
test.passed, test.total);
Was this page helpful? Send feedback