Skip to content

Latest commit

 

History

History
935 lines (712 loc) · 25.7 KB

File metadata and controls

935 lines (712 loc) · 25.7 KB

API Reference

Overview

The ML-DSA library provides a clean, template-based C++ API for key generation, signing, and verification. All functionality is contained in the dldsa:: namespace with zero heap allocation and no exceptions.

Quick API Summary

Typed API (Preferred)

namespace dldsa {

// Parameter sets
struct ML_DSA_44 { /* compile-time parameters */ };
struct ML_DSA_65 { /* compile-time parameters */ };
struct ML_DSA_87 { /* compile-time parameters */ };

// Strongly-typed key and signature objects
template<typename Params>
struct PublicKey {
    uint8_t data[Params::PK_SIZE];
    const uint8_t* bytes() const;
    static constexpr size_t size();
};

template<typename Params>
struct SecretKey {
    uint8_t data[Params::SK_SIZE];
    const uint8_t* bytes() const;
    static constexpr size_t size();
};

template<typename Params>
struct Signature {
    uint8_t data[Params::SIG_SIZE];
    size_t len;  // actual signature length after signing
    const uint8_t* bytes() const;
    size_t size() const;
};

// Result of signing operation
struct SignResult {
    int code;        // 0 = success, -1 = error, -3 = rejection loop limit
    size_t siglen;   // actual signature length (valid when code == 0)
};

// Main API (typed overloads)
template<typename Params>
struct MLDSA {
    static int keygen(PublicKey<Params>& pk, SecretKey<Params>& sk,
                      RngFunc rng, void* rng_ctx);
    
    static SignResult sign(Signature<Params>& sig,
                          const uint8_t* msg, size_t msglen,
                          const uint8_t* ctx, size_t ctxlen,
                          const SecretKey<Params>& sk,
                          RngFunc rng, void* rng_ctx);
    
    static int verify(const Signature<Params>& sig,
                      const uint8_t* msg, size_t msglen,
                      const uint8_t* ctx, size_t ctxlen,
                      const PublicKey<Params>& pk);
};

// Type aliases
using MLDSA44 = MLDSA<ML_DSA_44>;
using MLDSA65 = MLDSA<ML_DSA_65>;
using MLDSA87 = MLDSA<ML_DSA_87>;

}

Raw-Pointer API (Legacy, Deprecated)

The raw-pointer API is still available for interoperability and legacy code, but new code should use the typed API above. The raw-pointer functions are marked with [[deprecated]] and accept uint8_t* buffers directly:

// Deprecated overloads (don't use in new code)
int keygen(uint8_t* pk, uint8_t* sk, RngFunc rng, void* rng_ctx);
int sign(uint8_t* sig, size_t* siglen, ...);
int verify(const uint8_t* sig, size_t siglen, ...);

Top-Level API

Header: dldsa/sign.hpp

This header contains the primary cryptographic interface. All operations are template-based on a parameter set struct.

Parameter Sets

Three parameter sets are provided, corresponding to NIST security levels:

ML_DSA_44 — NIST Security Level 2 (~128-bit quantum-safe)

static constexpr int K = 4, L = 4, Eta = 2, Tau = 39;
static constexpr size_t PK_SIZE  = 1312;   // bytes
static constexpr size_t SK_SIZE  = 2560;   // bytes
static constexpr size_t SIG_SIZE = 2420;   // bytes

ML_DSA_65 — NIST Security Level 3 (~192-bit quantum-safe)

static constexpr int K = 6, L = 5, Eta = 4, Tau = 49;
static constexpr size_t PK_SIZE  = 1952;   // bytes
static constexpr size_t SK_SIZE  = 4032;   // bytes
static constexpr size_t SIG_SIZE = 3309;   // bytes

ML_DSA_87 — NIST Security Level 5 (~256-bit quantum-safe)

static constexpr int K = 8, L = 7, Eta = 2, Tau = 60;
static constexpr size_t PK_SIZE  = 2592;   // bytes
static constexpr size_t SK_SIZE  = 4896;   // bytes
static constexpr size_t SIG_SIZE = 4627;   // bytes

RNG Interface

Header: dldsa/rng.hpp

using RngFunc = void (*)(uint8_t* out, size_t len, void* ctx);

The RNG is a function pointer that:

  • Fills exactly len bytes into out with cryptographically secure random data
  • Receives an opaque context pointer ctx for state management
  • Must not fail silently; invalid implementations may compromise security

Example (Linux with getrandom):

#include <sys/random.h>

void os_rng(uint8_t* out, size_t len, void* ctx) {
    (void)ctx;  // unused
    getrandom(out, len, 0);  // fills 'len' bytes into 'out'
}

// Usage
MLDSA44::keygen(pk, sk, os_rng, nullptr);

Example (Custom state-based RNG):

struct RngState {
    uint64_t seed;
    // ... your RNG state
};

void my_rng(uint8_t* out, size_t len, void* ctx) {
    RngState* state = static_cast<RngState*>(ctx);
    // ... fill 'out' using your RNG algorithm
}

RngState rng_state{0xdeadbeefULL};
MLDSA44::keygen(pk, sk, my_rng, &rng_state);

Key Generation

Typed API (Preferred):

template<typename Params>
static int MLDSA<Params>::keygen(PublicKey<Params>& pk,
                                   SecretKey<Params>& sk,
                                   RngFunc rng, void* rng_ctx);

Parameters:

  • pk — Output public key object
  • sk — Output secret key object
  • rng — Function pointer to random number generator
  • rng_ctx — Opaque context passed to RNG (may be nullptr)

Return Value:

  • 0 — Success; pk and sk are now populated
  • -1 — Invalid argument (null pointer or invalid RNG)

Security Notes:

  • The RNG seed should be generated from a system entropy source (e.g., /dev/urandom, hardware RNG)
  • Each call to keygen must use a fresh, independent seed
  • The secret key should be stored securely and protected from unauthorized access

Example (Typed):

#include <iostream>

using namespace dldsa;

PublicKey44 pk;
SecretKey44 sk;

int result = MLDSA44::keygen(pk, sk, os_rng, nullptr);
if (result == 0) {
    // Key pair generated successfully
    std::cout << "Public key: " << pk.size() << " bytes\n";
}

Raw-Pointer API (Deprecated):

[[deprecated("Use the typed overload")]]
static int MLDSA<Params>::keygen(uint8_t* pk, uint8_t* sk, 
                                   RngFunc rng, void* rng_ctx);

Parameters:

  • pk — Output buffer for public key (must be exactly Params::PK_SIZE bytes)
  • sk — Output buffer for secret key (must be exactly Params::SK_SIZE bytes)
  • rng — Function pointer to random number generator
  • rng_ctx — Opaque context passed to RNG (may be nullptr)

Example (Deprecated):

uint8_t pk[MLDSA44::PK_SIZE];
uint8_t sk[MLDSA44::SK_SIZE];

int result = MLDSA44::keygen(pk, sk, os_rng, nullptr);

Signing

Typed API (Preferred):

template<typename Params>
static SignResult MLDSA<Params>::sign(Signature<Params>& sig,
                                       const uint8_t* msg, size_t msglen,
                                       const uint8_t* ctx, size_t ctxlen,
                                       const SecretKey<Params>& sk,
                                       RngFunc rng, void* rng_ctx);

Parameters:

  • sig — Output signature object (.len field set on success)
  • msg — Message to sign (pointer to first byte, may be nullptr if msglen == 0)
  • msglen — Message length in bytes (may be 0)
  • ctx — Context string (may be nullptr for empty context)
  • ctxlen — Context string length (0–255 bytes); ignored if ctx is nullptr
  • sk — Secret key (from keygen)
  • rng — Optional RNG for hedged signing (may be nullptr for deterministic)
  • rng_ctx — Opaque context for RNG

Return Value: Returns SignResult:

  • code == 0 — Success; sig.len is the actual signature length
  • code == -1 — Invalid argument (null signature, message, or key)
  • code == -3 — Signature generation failed (rejection loop limit exceeded; extremely rare ~2^-256)

Notes:

  • If rng == nullptr, signing is deterministic: same message + context → same signature
  • If rng != nullptr, signing includes randomness (hedged signing) for additional robustness
  • Context strings allow domain separation; different contexts produce different signatures
  • The context length is limited to 255 bytes per FIPS 204

Example (Deterministic, Typed):

#include <iostream>

using namespace dldsa;

uint8_t msg[] = "Hello, post-quantum world!";
Signature44 sig;
SecretKey44 sk;  // from keygen

auto result = MLDSA44::sign(
    sig,
    msg, sizeof(msg),
    nullptr, 0,        // no context
    sk,
    nullptr, nullptr   // deterministic (no RNG)
);

if (result.code == 0) {
    std::cout << "Signature length: " << result.siglen << " bytes\n";
}

Example (With Context, Typed):

using namespace dldsa;

const uint8_t ctx[] = "my-application-v1";
Signature44 sig;
SecretKey44 sk;

auto result = MLDSA44::sign(
    sig,
    msg, sizeof(msg),
    ctx, sizeof(ctx) - 1,  // context string
    sk,
    nullptr, nullptr
);

Example (Hedged Signing, Typed):

Signature44 sig;
SecretKey44 sk;

auto result = MLDSA44::sign(
    sig,
    msg, sizeof(msg),
    nullptr, 0,
    sk,
    os_rng, nullptr    // with RNG for randomized signing
);

Raw-Pointer API (Deprecated):

[[deprecated("Use the typed overload")]]
static int MLDSA<Params>::sign(uint8_t* sig, size_t* siglen,
                                 const uint8_t* msg, size_t msglen,
                                 const uint8_t* ctx, size_t ctxlen,
                                 const uint8_t* sk,
                                 RngFunc rng, void* rng_ctx);

Parameters:

  • sig — Output buffer for signature (must be Params::SIG_SIZE bytes)
  • siglen — Pointer to size_t; on success, set to actual signature length
  • msg — Message to sign
  • msglen — Message length
  • ctx — Context string (may be nullptr)
  • ctxlen — Context length (0–255)
  • sk — Secret key buffer (Params::SK_SIZE bytes)
  • rng — Optional RNG
  • rng_ctx — RNG context

Return Value:

  • 0 — Success
  • -1 — Invalid argument
  • -3 — Rejection loop limit exceeded

Example (Deprecated):

uint8_t sig[MLDSA44::SIG_SIZE];
size_t siglen = 0;

int result = MLDSA44::sign(
    sig, &siglen,
    msg, sizeof(msg),
    nullptr, 0,
    sk,
    nullptr, nullptr
);

Verification

Typed API (Preferred):

template<typename Params>
static int MLDSA<Params>::verify(const Signature<Params>& sig,
                                   const uint8_t* msg, size_t msglen,
                                   const uint8_t* ctx, size_t ctxlen,
                                   const PublicKey<Params>& pk);

Parameters:

  • sig — Signature object (with .len field set by a previous sign() call)
  • msg — Message that was signed
  • msglen — Message length in bytes
  • ctx — Context string used during signing (may be nullptr)
  • ctxlen — Context string length (0–255); ignored if ctx is nullptr
  • pk — Public key (from corresponding keygen)

Return Value:

  • 0 — Success; signature is valid
  • -1 — Invalid argument (null pointer)
  • -2 — Verification failed; signature does not match

Notes:

  • The context string must match exactly the context used during signing (case-sensitive)
  • The signature length in sig.len must match the expected size; otherwise, return -2
  • Verification is deterministic and can be done by anyone
  • Does not use fuzzy matching; signature bits must match exactly

Example (Typed):

#include <iostream>

using namespace dldsa;

uint8_t msg[] = "Hello, post-quantum world!";
const uint8_t ctx[] = "my-application-v1";
Signature44 sig;        // from sign()
PublicKey44 pk;         // from keygen()

int result = MLDSA44::verify(
    sig,
    msg, sizeof(msg),
    ctx, sizeof(ctx) - 1,
    pk
);

if (result == 0) {
    std::cout << "✓ Signature is valid!\n";
} else if (result == -2) {
    std::cout << "✗ Signature is invalid!\n";
} else {
    std::cerr << "Error: " << result << '\n';
}

Raw-Pointer API (Deprecated):

[[deprecated("Use the typed overload")]]
static int MLDSA<Params>::verify(const uint8_t* sig, size_t siglen,
                                   const uint8_t* msg, size_t msglen,
                                   const uint8_t* ctx, size_t ctxlen,
                                   const uint8_t* pk);

Parameters:

  • sig — Signature buffer (must be exactly Params::SIG_SIZE bytes)
  • siglen — Signature length (must equal Params::SIG_SIZE)
  • msg — Message that was signed
  • msglen — Message length
  • ctx — Context string (may be nullptr)
  • ctxlen — Context length (0–255)
  • pk — Public key buffer (Params::PK_SIZE bytes)

Example (Deprecated):

int result = MLDSA44::verify(
    sig, MLDSA44::SIG_SIZE,
    msg, sizeof(msg),
    ctx, sizeof(ctx) - 1,
    pk
);

Key and Signature Types

Header: dldsa/keys.hpp

The library provides strongly-typed, stack-allocated wrappers for keys and signatures:

PublicKey

template<typename Params>
struct PublicKey {
    static constexpr size_t SIZE = Params::PK_SIZE;
    uint8_t data[SIZE];
    
    uint8_t*       bytes();         // Access raw bytes
    const uint8_t* bytes() const;
    static constexpr size_t size(); // Returns SIZE
};

Convenience aliases: PublicKey44, PublicKey65, PublicKey87

SecretKey

template<typename Params>
struct SecretKey {
    static constexpr size_t SIZE = Params::SK_SIZE;
    uint8_t data[SIZE];
    
    uint8_t*       bytes();         // Access raw bytes
    const uint8_t* bytes() const;
    static constexpr size_t size(); // Returns SIZE
};

Convenience aliases: SecretKey44, SecretKey65, SecretKey87

Signature

template<typename Params>
struct Signature {
    static constexpr size_t MAX_SIZE = Params::SIG_SIZE;
    uint8_t data[MAX_SIZE];
    size_t  len;  // Actual signature length (0 until sign() completes)
    
    uint8_t*       bytes();         // Access raw bytes
    const uint8_t* bytes() const;
    size_t size() const;            // Returns len
};

Note: The len field is automatically set by sign() on success.

Convenience aliases: Signature44, Signature65, Signature87

SignResult

struct SignResult {
    int    code;    // 0 = success, -1 = invalid arg, -3 = rejection limit exceeded
    size_t siglen;  // Actual signature length (valid only when code == 0)
};

Returned by the typed sign() overload to communicate both success/failure and the signature length.


Type Definitions

Header: dldsa/types.hpp

Core types and constants used throughout the library:

// Global constants
static constexpr int32_t Q = 8380417;  // Modulus
static constexpr int     N = 256;      // Polynomial degree
static constexpr int     D = 13;       // Rounding bit count

static constexpr size_t SEED_BYTES = 32;  // Seed size (ρ, K, ξ)
static constexpr size_t TR_BYTES   = 64;  // Public key hash
static constexpr size_t RND_BYTES  = 32;  // Hedged signing randomness
static constexpr size_t MU_BYTES   = 64;  // Message hash

// Polynomial type
struct Poly {
    int32_t coeffs[256];  // 256 coefficients in range [-(q-1), q-1]
};

// Polynomial vector type
template<int Size>
struct PolyVec {
    Poly vec[Size];  // Vector of 'Size' polynomials
};

Low-Level Polynomial Operations

Header: dldsa/poly.hpp

These are internal helper functions, exposed for advanced use and testing:

void poly_add(Poly& a, const Poly& b);              // a += b
void poly_sub(Poly& a, const Poly& b);              // a -= b
void poly_ntt(Poly& a);                             // In-place forward NTT
void poly_invntt_tomont(Poly& a);                   // In-place inverse NTT + Montgomery
void poly_pointwise_montgomery(Poly& c, 
                               const Poly& a, 
                               const Poly& b);      // c = a * b (NTT domain)
void poly_reduce(Poly& a);                          // Barrett reduction
void poly_caddq(Poly& a);                           // Conditional add q
void poly_freeze(Poly& a);                          // Full reduction to [0, q)
void poly_shiftl(Poly& a);                          // Multiply by 2^D
bool poly_chknorm(const Poly& a, int32_t bound);    // Check |a[i]| < bound

These functions are not part of the main API and are subject to change. Use them only if you are implementing custom extensions or testing.


Error Codes

All main functions return int with the following meanings:

Code Meaning Context
0 Success All operations
-1 Invalid argument Null pointer, bad buffer size, out-of-range context length
-2 Verification failed verify() only; signature does not match
-3 Rejection loop limit exceeded sign() only; extremely rare; indicates implementation issue

Note: For sign() and verify(), passing nullptr for optional RNG/context is valid and returns 0 on success, not an error.


Parameter Set Differences

Choosing the right parameter set depends on your security requirements:

Aspect ML-DSA-44 ML-DSA-65 ML-DSA-87
Security NIST L2 NIST L3 NIST L5
Quantum Safety ~128 bits ~192 bits ~256 bits
Key Size 1.3 KB 1.9 KB 2.6 KB (public)
Signature Size 2.4 KB 3.3 KB 4.6 KB
Speed Fastest Medium Slowest
Use Case Cost-sensitive IoT General-purpose High-security apps

Namespace and Naming Conventions

  • All public API is in the dldsa:: namespace
  • Type names use PascalCase (e.g., Poly, PolyVec, MLDSA)
  • Function names use snake_case (e.g., keygen, poly_add)
  • Constants use UPPER_CASE (e.g., Q, N, SEED_BYTES)
  • Template parameters use PascalCase (e.g., Params, Size)

Memory Layout

Public Key

[ρ (32 bytes)] [T₁ packed (K × 320 bytes)]

Where K is the parameter set's module dimension.

Secret Key

[ρ (32)] [K (32)] [tr (64)] [s₁ packed (L × 96 or 128)] [s₂ packed (K × 96 or 128)] [t₀ packed (K × 416)]

Signature

[c̃ (32 or 48 or 64)] [z packed (L × 576 or 640)] [h (ω + L bytes)]

Where ω is the rejection threshold (80, 55, or 75 depending on parameter set).


Stack Usage

With default settings (no on-demand matrix):

  • keygen: ~50 KB
  • sign: ~60 KB
  • verify: ~70 KB (includes full matrix storage)

With DLDSA_ONDEMAND_MATRIX=ON:

  • Reduces matrix storage overhead
  • verify: ~30 KB (no full matrix in memory)
  • Computation slightly slower due to matrix regeneration

Examples

See QUICKSTART.md for practical code examples and usage patterns.


OpenSSL-Compatible Key I/O

Header: dldsa/ossl_io.hpp

This header provides serialization and deserialization of keys in OpenSSL 3.5+ compatible formats. Keys can be exported to DER (binary) or PEM (text) formats for interoperability with OpenSSL tooling and other implementations.

Supported Formats

Public Keys:

  • SubjectPublicKeyInfo DER (binary)
  • SubjectPublicKeyInfo PEM (text, -----BEGIN PUBLIC KEY-----)

Private Keys:

  • PKCS#8 seed-only DER/PEM — compact format matching OpenSSL's genpkey default output
  • PKCS#8 expanded-key DER/PEM — full secret key bytes (when seed is not available)

OIDs

Each parameter set has a unique OID (Object Identifier) per draft-ietf-lamps-dilithium-certificates:

Parameter Set OID DER Hex
ML-DSA-44 2.16.840.1.101.3.4.3.17 06 09 60 86 48 01 65 03 04 03 11
ML-DSA-65 2.16.840.1.101.3.4.3.18 06 09 60 86 48 01 65 03 04 03 12
ML-DSA-87 2.16.840.1.101.3.4.3.19 06 09 60 86 48 01 65 03 04 03 13

Template: OsslIO<Params>

template<typename Params>
struct OsslIO {
    // Compile-time size constants
    static constexpr size_t PK_DER_SIZE;
    static constexpr size_t SK_SEED_DER_SIZE;  // 54 bytes (all param sets)
    static constexpr size_t SK_EXP_DER_SIZE;
    static constexpr size_t PK_PEM_SIZE;
    static constexpr size_t SK_SEED_PEM_SIZE;
    static constexpr size_t SK_EXP_PEM_SIZE;
    
    // Write (serialize)
    static int pk_to_der(uint8_t* buf, size_t buflen,
                         const PublicKey<Params>& pk);
    static int pk_to_pem(char* buf, size_t buflen,
                         const PublicKey<Params>& pk);
    
    static int sk_to_der(uint8_t* buf, size_t buflen,
                         const uint8_t seed[SEED_BYTES]);
    static int sk_to_pem(char* buf, size_t buflen,
                         const uint8_t seed[SEED_BYTES]);
    
    static int sk_expanded_to_der(uint8_t* buf, size_t buflen,
                                   const SecretKey<Params>& sk);
    static int sk_expanded_to_pem(char* buf, size_t buflen,
                                   const SecretKey<Params>& sk);
    
    // Read (deserialize)
    static int pk_from_der(PublicKey<Params>& pk,
                           const uint8_t* der, size_t derlen);
    static int pk_from_pem(PublicKey<Params>& pk,
                           const char* pem, size_t pemlen);
    
    static int sk_from_der(PublicKey<Params>& pk,
                           SecretKey<Params>& sk,
                           const uint8_t* der, size_t derlen);
    static int sk_from_pem(PublicKey<Params>& pk,
                           SecretKey<Params>& sk,
                           const char* pem, size_t pemlen);
};

// Convenience aliases
using OsslIO44 = OsslIO<ML_DSA_44>;
using OsslIO65 = OsslIO<ML_DSA_65>;
using OsslIO87 = OsslIO<ML_DSA_87>;

Serialization (Write)

Writing Public Keys

To DER (binary):

using namespace dldsa;

PublicKey44 pk;  // from keygen()
uint8_t der[OsslIO44::PK_DER_SIZE];

int bytes_written = OsslIO44::pk_to_der(der, sizeof(der), pk);
if (bytes_written < 0) {
    // Error: invalid input
}
// bytes_written == OsslIO44::PK_DER_SIZE

To PEM (text):

char pem[OsslIO44::PK_PEM_SIZE];

int chars_written = OsslIO44::pk_to_pem(pem, sizeof(pem), pk);
if (chars_written < 0) {
    // Error
}
// pem is now null-terminated and contains:
// -----BEGIN PUBLIC KEY-----
// [base64-encoded DER]
// -----END PUBLIC KEY-----

Writing Private Keys

Seed-only (recommended):

When you have the original seed (ξ), use the seed-only format — this matches OpenSSL's default output and is the most compact:

uint8_t seed[SEED_BYTES];  // from RNG during keygen
uint8_t der[OsslIO44::SK_SEED_DER_SIZE];  // exactly 54 bytes

int bytes = OsslIO44::sk_to_der(der, sizeof(der), seed);
if (bytes != static_cast<int>(OsslIO44::SK_SEED_DER_SIZE)) {
    // Error
}

Expanded key (alternative):

If the seed is not available (e.g., only the SecretKey<Params> object exists):

SecretKey44 sk;  // from keygen()
uint8_t der[OsslIO44::SK_EXP_DER_SIZE];

int bytes = OsslIO44::sk_expanded_to_der(der, sizeof(der), sk);
if (bytes != static_cast<int>(OsslIO44::SK_EXP_DER_SIZE)) {
    // Error
}

To PEM:

char pem[OsslIO44::SK_SEED_PEM_SIZE];
OsslIO44::sk_to_pem(pem, sizeof(pem), seed);
// Contains: -----BEGIN PRIVATE KEY-----

Deserialization (Read)

Reading Public Keys

PublicKey44 pk;

// From DER
int ret = OsslIO44::pk_from_der(pk, der, derlen);
if (ret != 0) {
    // Error: -1 = invalid input, wrong OID, wrong size
}

// From PEM
ret = OsslIO44::pk_from_pem(pk, pem_string, pem_length);
if (ret != 0) {
    // Error
}

// pk.data now contains the public key bytes

Reading Private Keys

PublicKey44 pk;
SecretKey44 sk;

// From DER — automatically handles seed-only, expanded, or both formats
int ret = OsslIO44::sk_from_der(pk, sk, der, derlen);
if (ret != 0) {
    // Error: -1 = invalid input, wrong OID, wrong version
}

// If the input was seed-only:
//   - pk is populated (keygen replayed with the seed)
//   - sk is populated
// If the input was expanded-only:
//   - sk is populated
//   - pk left unchanged (cannot derive from expanded key alone)

// From PEM
ret = OsslIO44::sk_from_pem(pk, sk, pem_string, pem_length);

Important: When reading a seed-only private key, the library internally replays keygen with the recovered seed. This produces both the public and secret keys without requiring the seed to be stored separately.

Error Codes

All functions return:

  • 0 or positive: Success; for write functions, returns number of bytes/chars written
  • -1: Invalid input (null pointer, buffer too small, wrong OID, malformed DER/PEM)

Interoperability with OpenSSL

Generate a key with OpenSSL and load it:

# Generate ML-DSA-44 key
openssl genpkey -algorithm ML-DSA-44 -out priv.pem

# Extract public key
openssl pkey -in priv.pem -pubout -out pub.pem

Then load in your C++ application:

#include <fstream>
#include <iostream>
#include "dldsa/ossl_io.hpp"

using namespace dldsa;

// Read PEM file
std::ifstream f("priv.pem");
char pem[OsslIO44::SK_SEED_PEM_SIZE];
f.read(pem, sizeof(pem));
std::streamsize pem_len = f.gcount();

// Load key
PublicKey44 pk;
SecretKey44 sk;
int ret = OsslIO44::sk_from_pem(pk, sk, pem, static_cast<size_t>(pem_len));
if (ret != 0) {
    std::cerr << "Failed to load private key\n";
    return 1;
}

// Ready to sign
Signature44 sig;
MLDSA44::sign(sig, msg, msglen, nullptr, 0, sk, nullptr, nullptr);

Memory and Constraints

  • Zero heap allocation: All buffers are caller-provided on the stack
  • No exceptions: All errors return -1
  • Buffer sizes: Compile-time constants; use sizeof or the constant directly
  • Maximum sizes (stack):
    • PEM public key: ~2 KB
    • PEM private key: ~1 KB (seed-only), ~10 KB (expanded)
    • DER is binary, typically 40–70% smaller than PEM

Building and Linking

Include the headers:

#include "dldsa/sign.hpp"           // Core signing API
#include "dldsa/ossl_io.hpp"        // Optional: OpenSSL-compatible I/O

Link against the library:

cmake -B build -S .
cmake --build build
g++ myapp.cpp -o myapp -I./include -L./build -ldldsa

Or use CMake in your project:

add_subdirectory(path/to/dl-dsa)
add_executable(myapp myapp.cpp)
target_link_libraries(myapp dldsa)