Key Derivation
Overview
Key derivation uses the OpenSSL EVP_KDF API. Five typed builders cover the most
common cases; raw KdfAlg + KdfCtx covers everything else.
HkdfBuilder— HKDF (RFC 5869)Pbkdf2Builder— PBKDF2 (PKCS #5 / RFC 8018)ScryptBuilder— scrypt (RFC 7914)SshkdfBuilder— SSH key derivation (RFC 4253 §7.2)KbkdfBuilder— KBKDF counter/feedback mode (NIST SP 800-108)KdfAlg+KdfCtx— raw access to anyEVP_KDFalgorithm
Builders hold borrowed slices with lifetimes — no data is copied until the final
derive() call. derive() writes directly into a caller-provided buffer.
Typed Builders
HkdfBuilder<'a>
#![allow(unused)]
fn main() {
pub struct HkdfBuilder<'a> { /* borrowed slices */ }
impl<'a> HkdfBuilder<'a> {
pub fn new(digest: &'a DigestAlg) -> Self; // no allocation
pub fn key(self, key: &'a [u8]) -> Self; // IKM (input key material)
pub fn salt(self, salt: &'a [u8]) -> Self; // optional
pub fn info(self, info: &'a [u8]) -> Self; // optional context info
pub fn mode(self, mode: HkdfMode) -> Self;
/// Derive into caller-provided buffer.
pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
/// Derive `len` bytes into a freshly allocated Vec.
pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}
HkdfMode
#![allow(unused)]
fn main() {
pub enum HkdfMode {
ExtractAndExpand, // RFC 5869 §2 default — extract then expand
ExtractOnly, // output is the PRK (pseudorandom key)
ExpandOnly, // input must already be a PRK
}
}
Pbkdf2Builder<'a>
#![allow(unused)]
fn main() {
pub struct Pbkdf2Builder<'a> { /* borrowed slices */ }
impl<'a> Pbkdf2Builder<'a> {
/// Password and salt are required at construction; digest selects the PRF.
/// Default iteration count: 600 000 (NIST SP 800-132 minimum for SHA-256).
pub fn new(digest: &'a DigestAlg, password: &'a [u8], salt: &'a [u8]) -> Self;
pub fn iterations(self, n: u32) -> Self;
pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}
ScryptBuilder<'a>
#![allow(unused)]
fn main() {
pub struct ScryptBuilder<'a> { /* borrowed slices */ }
pub struct ScryptParams {
pub n: u64, // CPU/memory cost; must be a power of 2
pub r: u32, // block size factor
pub p: u32, // parallelization factor
}
impl Default for ScryptParams {
fn default() -> Self {
ScryptParams { n: 16384, r: 8, p: 1 } // OWASP-recommended minimums
}
}
impl<'a> ScryptBuilder<'a> {
/// Password and salt are required; uses ScryptParams::default().
pub fn new(password: &'a [u8], salt: &'a [u8]) -> Self;
pub fn params(self, params: ScryptParams) -> Self;
pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}
SshkdfBuilder<'a>
SSH key derivation as specified in RFC 4253 §7.2. Derives the six key components (initial IVs, encryption keys, and integrity keys for each direction) from the shared secret, exchange hash, and session identifier produced by the SSH handshake.
#![allow(unused)]
fn main() {
pub struct SshkdfBuilder<'a> { /* borrowed slices */ }
impl<'a> SshkdfBuilder<'a> {
/// Create an SSH-KDF builder.
///
/// - digest — hash algorithm (e.g. SHA-256).
/// - key — shared secret K from the DH exchange.
/// - xcghash — exchange hash H.
/// - session_id — session identifier (equals the first H for the session).
/// - key_type — which key/IV component to derive (A–F).
pub fn new(
digest: &'a DigestAlg,
key: &'a [u8],
xcghash: &'a [u8],
session_id: &'a [u8],
key_type: SshkdfKeyType,
) -> Self;
/// Derive into caller-provided buffer.
pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
/// Derive `len` bytes into a freshly allocated Vec.
pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}
SshkdfKeyType
Each variant corresponds to one of the six letter codes from RFC 4253 §7.2:
#![allow(unused)]
fn main() {
pub enum SshkdfKeyType {
InitialIvClientToServer, // "A"
InitialIvServerToClient, // "B"
EncryptionKeyClientToServer, // "C"
EncryptionKeyServerToClient, // "D"
IntegrityKeyClientToServer, // "E"
IntegrityKeyServerToClient, // "F"
}
}
Derive all six components by calling SshkdfBuilder::new once per variant with
the same key, xcghash, and session_id inputs.
KbkdfBuilder<'a>
KBKDF (key-based key derivation function) as specified in NIST SP 800-108. Supports both counter mode and feedback mode with HMAC or CMAC as the PRF.
#![allow(unused)]
fn main() {
pub struct KbkdfBuilder<'a> { /* borrowed slices */ }
impl<'a> KbkdfBuilder<'a> {
/// Create a KBKDF builder.
///
/// - mode — counter or feedback.
/// - mac — MAC algorithm (fetch c"HMAC" or c"CMAC" via MacAlg::fetch).
/// - key — the key derivation key (KDK).
pub fn new(mode: KbkdfMode, mac: &'a MacAlg, key: &'a [u8]) -> Self;
/// Hash digest for HMAC-based derivation (required when mac is HMAC).
pub fn digest(self, digest: &'a DigestAlg) -> Self;
/// Label: identifies the purpose of the derived key.
pub fn label(self, label: &'a [u8]) -> Self;
/// Context: caller-specific data bound into the derivation.
pub fn context(self, context: &'a [u8]) -> Self;
/// Salt / feedback IV (feedback mode only).
pub fn salt(self, salt: &'a [u8]) -> Self;
/// Counter field length (default: Bits32).
pub fn counter_len(self, len: KbkdfCounterLen) -> Self;
/// Include the length field L in the PRF input (default: true).
pub fn use_l(self, enabled: bool) -> Self;
/// Include the zero-byte separator in the PRF input (default: true).
pub fn use_separator(self, enabled: bool) -> Self;
/// Derive into caller-provided buffer.
pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
/// Derive `len` bytes into a freshly allocated Vec.
pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}
KbkdfMode
#![allow(unused)]
fn main() {
pub enum KbkdfMode {
/// Counter mode — a monotonically incrementing counter is appended to each
/// PRF input block. This is the most common SP 800-108 construction.
Counter,
/// Feedback mode — the output of each PRF invocation is fed as input to
/// the next. An initial IV/salt is required for the first block.
Feedback,
}
}
KbkdfCounterLen
Controls the bit-width of the counter field in counter mode. Defaults to Bits32.
#![allow(unused)]
fn main() {
pub enum KbkdfCounterLen {
Bits8 = 8,
Bits16 = 16,
Bits24 = 24,
Bits32 = 32, // default
}
}
Pkcs12KdfBuilder<'a>
PKCS#12 (RFC 7292 Appendix B) legacy KDF. Needed only for interoperability with
PKCS#12 files encrypted with deprecated algorithms such as
PBEWithSHAAnd3-KeyTripleDES-CBC. New PKCS#12 files should use PBES2/PBKDF2 instead.
#![allow(unused)]
fn main() {
pub enum Pkcs12KdfId {
Key = 1, // cipher key bytes
Iv = 2, // cipher IV bytes
Mac = 3, // MAC key bytes
}
pub struct Pkcs12KdfBuilder<'a> { /* borrowed slices */ }
impl<'a> Pkcs12KdfBuilder<'a> {
/// Create a PKCS#12 KDF builder.
///
/// - `md` — hash algorithm (SHA-1 for legacy 3DES; SHA-256 for PBES2).
/// - `password` — UTF-8 passphrase bytes.
/// - `salt` — random salt (RFC 7292 recommends 8 bytes).
/// - `id` — which component to derive: Key, Iv, or Mac.
///
/// Default iteration count: 2048.
pub fn new(md: &'a DigestAlg, password: &'a [u8], salt: &'a [u8], id: Pkcs12KdfId) -> Self;
/// Override the iteration count.
pub fn iterations(self, n: u32) -> Self;
pub fn derive(self, out: &mut [u8]) -> Result<(), ErrorStack>;
pub fn derive_to_vec(self, len: usize) -> Result<Vec<u8>, ErrorStack>;
}
}
Raw KDF Access
For algorithms without a typed builder (TLS13-KDF, SSKDF, X963KDF, etc.):
#![allow(unused)]
fn main() {
pub struct KdfAlg { /* EVP_KDF* */ }
impl KdfAlg {
pub fn fetch(name: &CStr) -> Result<Self, ErrorStack>;
}
pub struct KdfCtx { /* EVP_KDF_CTX* */ }
impl KdfCtx {
pub fn new(alg: &KdfAlg) -> Result<Self, ErrorStack>;
/// Derive key material. Parameters are supplied at derive time.
pub fn derive(&mut self, out: &mut [u8], params: &Params<'_>) -> Result<(), ErrorStack>;
/// Update parameters on this context without destroying and recreating it.
///
/// Wraps `EVP_KDF_CTX_set_params`. Useful for changing HKDF mode (e.g.
/// extract-only vs. expand-only) or updating salt/key between derivations.
pub fn set_params(&mut self, params: &Params<'_>) -> Result<(), ErrorStack>;
/// Retrieve current parameter values from this context.
///
/// Wraps `EVP_KDF_CTX_get_params`. Build a `Params` with placeholder
/// values for the keys you want, call this, then read back with `params.get_*`.
pub fn get_params(&self, params: &mut Params<'_>) -> Result<(), ErrorStack>;
/// Return the output length of this KDF (if fixed), or `usize::MAX` if variable.
///
/// Wraps `EVP_KDF_CTX_get_kdf_size`. HKDF and most stream KDFs return
/// `usize::MAX`; fixed-output KDFs such as SSKDF with a set output length
/// return that fixed length.
pub fn size(&self) -> usize;
}
}
Algorithm Reference
| Builder / API | EVP_KDF name | Standard |
|---|---|---|
HkdfBuilder | c"HKDF" | RFC 5869 |
Pbkdf2Builder | c"PBKDF2" | PKCS #5 / RFC 8018 |
ScryptBuilder | c"SCRYPT" | RFC 7914 |
SshkdfBuilder | c"SSHKDF" | RFC 4253 §7.2 |
KbkdfBuilder | c"KBKDF" | NIST SP 800-108 |
Pkcs12KdfBuilder | c"PKCS12KDF" | RFC 7292 Appendix B |
KdfCtx direct | c"TLS13-KDF" | RFC 8446 |
KdfCtx direct | c"SSKDF" | NIST SP 800-56Cr2 |
KdfCtx direct | c"X963KDF" | ANSI X9.63 |
Examples
HKDF (Expand from existing PRK)
#![allow(unused)]
fn main() {
use native_ossl::kdf::{HkdfBuilder, HkdfMode};
use native_ossl::digest::DigestAlg;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let ikm = b"input key material";
let salt = b"random salt";
let info = b"context info";
let mut okm = [0u8; 32];
HkdfBuilder::new(&sha256)
.key(ikm)
.salt(salt)
.info(info)
.derive(&mut okm)?;
}
PBKDF2 (Password Hashing)
#![allow(unused)]
fn main() {
use native_ossl::kdf::Pbkdf2Builder;
use native_ossl::digest::DigestAlg;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let mut key = [0u8; 32];
Pbkdf2Builder::new(&sha256, b"my password", b"random salt")
.iterations(100_000)
.derive(&mut key)?;
}
scrypt (Memory-Hard Password Hashing)
#![allow(unused)]
fn main() {
use native_ossl::kdf::{ScryptBuilder, ScryptParams};
let mut key = [0u8; 32];
ScryptBuilder::new(b"my password", b"random salt")
.params(ScryptParams { n: 32768, r: 8, p: 1 })
.derive(&mut key)?;
}
SSH Key Derivation (RFC 4253)
Derive the client-to-server encryption key and IV after completing an SSH Diffie-Hellman key exchange:
use native_ossl::kdf::{SshkdfBuilder, SshkdfKeyType};
use native_ossl::digest::DigestAlg;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
// shared_secret, exchange_hash, and session_id come from the DH handshake.
let shared_secret = /* K from ECDH / DH group exchange */;
let exchange_hash = /* H = hash of handshake transcript */;
let session_id = /* first H for this session */;
// Derive the client-to-server IV (16 bytes for AES-128).
let mut iv = [0u8; 16];
SshkdfBuilder::new(
&sha256,
&shared_secret,
&exchange_hash,
&session_id,
SshkdfKeyType::InitialIvClientToServer,
).derive(&mut iv)?;
// Derive the client-to-server encryption key (16 bytes for AES-128).
let mut enc_key = [0u8; 16];
SshkdfBuilder::new(
&sha256,
&shared_secret,
&exchange_hash,
&session_id,
SshkdfKeyType::EncryptionKeyClientToServer,
).derive(&mut enc_key)?;
// Derive the client-to-server integrity key (32 bytes for HMAC-SHA-256).
let mut mac_key = [0u8; 32];
SshkdfBuilder::new(
&sha256,
&shared_secret,
&exchange_hash,
&session_id,
SshkdfKeyType::IntegrityKeyClientToServer,
).derive(&mut mac_key)?;
KBKDF Counter Mode (HMAC-SHA-256 PRF)
use native_ossl::kdf::{KbkdfBuilder, KbkdfMode, KbkdfCounterLen};
use native_ossl::mac::MacAlg;
use native_ossl::digest::DigestAlg;
let hmac = MacAlg::fetch(c"HMAC", None)?;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let master_key = b"my 32-byte key derivation key!!!";
let derived = KbkdfBuilder::new(KbkdfMode::Counter, &hmac, master_key)
.digest(&sha256)
.label(b"session key")
.context(b"connection id 42")
.counter_len(KbkdfCounterLen::Bits32)
.derive_to_vec(32)?;
KBKDF Feedback Mode (HMAC-SHA-256 PRF)
use native_ossl::kdf::{KbkdfBuilder, KbkdfMode};
use native_ossl::mac::MacAlg;
use native_ossl::digest::DigestAlg;
let hmac = MacAlg::fetch(c"HMAC", None)?;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;
let master_key = b"my 32-byte key derivation key!!!";
let iv = b"initial feedback value (16 bytes)";
let derived = KbkdfBuilder::new(KbkdfMode::Feedback, &hmac, master_key)
.digest(&sha256)
.label(b"session key")
.salt(iv) // feedback mode requires an IV/salt for the first block
.derive_to_vec(32)?;
Raw KdfCtx (TLS 1.3 KDF)
#![allow(unused)]
fn main() {
use native_ossl::kdf::{KdfAlg, KdfCtx};
use native_ossl::params::ParamBuilder;
let alg = KdfAlg::fetch(c"TLS13-KDF")?;
let mut ctx = KdfCtx::new(&alg)?;
let params = ParamBuilder::new()?
.push_utf8_string(c"digest", c"SHA2-256")?
.push_utf8_string(c"mode", c"EXTRACT_ONLY")?
.push_octet_slice(c"key", b"my ikm")?
.build()?;
let mut out = [0u8; 32];
ctx.derive(&mut out, ¶ms)?;
}
Design Notes
EVP_KDF, notEVP_PKEY_CTX— earlier versions of OpenSSL usedEVP_PKEY_CTXin derive mode for KDFs. OpenSSL 3.x provides the dedicatedEVP_KDFAPI, which has a cleaner parameter interface.- Re-parameterisation vs. rebuild —
KdfCtx::set_paramswrapsEVP_KDF_CTX_set_params, allowing algorithm-specific parameters (e.g. HKDF mode, salt, key) to be updated on an existing context without destroying and recreating it. This avoids the overhead of aKdfAlg::fetch+KdfCtx::newcycle and is the correct approach when performing multiple derivations with the same algorithm but different parameters. The typed builders (HkdfBuilder, etc.) always create a fresh context; useKdfCtxdirectly when re-parameterisation matters. get_paramsquery pattern —KdfCtx::get_paramsfollows the OpenSSL query-array pattern: build aParamswith placeholder values (e.g.push_uint(c"mode", 0)) for the keys you want, pass it by&mut, and read the filled-in values withparams.get_uint(c"mode")etc. Only keys present in the query array are filled; unknown keys are silently skipped by OpenSSL.- Builders are consumed by
derive— create a new builder for each derivation. This prevents accidental reuse of parameters from a previous call. - No context stored in builder —
KdfAlgandKdfCtxare created insidederive()and freed when it returns. There is no lifetime entanglement between the builder and the EVP context. ScryptParamsstruct — scrypt has three interdependent cost parameters. A struct prevents passing them in the wrong order.- PBKDF2 iteration count — the default of 600 000 follows NIST SP 800-132
(2023 revision) for SHA-256. Override with
.iterations()when compatibility with a specific value is required. SshkdfBuilderis one-shot per component — each of the six RFC 4253 key components requires a separate builder instance with the same inputs but a differentSshkdfKeyType. There is no loop API; construct each builder explicitly.KbkdfBuilder::contextmaps to the"data"parameter — OpenSSL’s KBKDF provider uses"data"for the SP 800-108 context field. The method is named.context()to match the specification’s terminology.KbkdfBuilder::digestis required for HMAC — when the MAC algorithm isHMAC, a digest must be set. ForCMACthe digest is ignored (the cipher is bound to theMacAlgat fetch time, not at derive time).