API guide
Kdf108 has one shape throughout:
- Static classes do the work:
Sp800108,Sp80056C,EcSchemes,FfcSchemes, andKeyConfirmation. They are pure functions with no state and no I/O. - Services (
ISp800108Kdf,ISp80056CKdf,IEcKeyAgreement,IFfcKeyAgreement,IKeyConfirmation) mirror them method for method and add logging. Register them withservices.AddKdf108()or construct them directly. - Secrets go in as spans and come out as new arrays. You own every array you get back; clear
it with
Array.Clearwhen you are done. Nothing in the library needs disposing. - Lengths are always
BitLength, never a bare number of bytes or bits. - Algorithms are small immutable values:
Prf,KeyExpansion,OneStepFunction,Extraction, andKeyConfirmationMac. Invalid combinations are rejected when you create them. - Every failure is a
Kdf108Exception:KdfParameterExceptionfor a parameter the standard does not allow,InvalidKeyExceptionfor a key or domain that fails validation, andKeyAgreementExceptionfor a forbidden shared secret.
SP 800-108: keys from a key
Sp800108.Derive(key, prf, label, context, length) is counter mode with a 32-bit counter and
the conventional fixed input Label || 0x00 || Context || [L]32. It is the right choice for
most applications.
For anything else, describe the expansion with KeyExpansion:
var length = BitLength.Create(512);
byte[] fixedInput = Sp800108.FixedInput("session"u8, "client-7"u8, length); // Label || 0x00 || Context || [L]32
byte[] counter = Sp800108.Derive(kdk, Prf.AesCmac(256), KeyExpansion.Counter(fixedInput, counterBits: 16), length);
byte[] feedback = Sp800108.Derive(kdk, Prf.Hmac(NistHashAlgorithm.Sha384), KeyExpansion.Feedback(fixedInput, iv), length);
byte[] pipeline = Sp800108.Derive(kdk, Prf.Hmac(NistHashAlgorithm.Sha3_256), KeyExpansion.DoublePipeline(fixedInput, useCounter: false), length);
byte[] kmac = Sp800108.DeriveKmac(kdk, KmacVariant.Kmac256, label: "session"u8, context: "client-7"u8, length);
| Mode | Factory | Counter positions |
|---|---|---|
| Counter (§4.1) | KeyExpansion.Counter, KeyExpansion.CounterInMiddle |
before or after the fixed input, or in the middle |
| Feedback (§4.2) | KeyExpansion.Feedback |
after K(i-1) (the §4.2 form), before it, or after the fixed input; or no counter |
| Double pipeline (§4.3) | KeyExpansion.DoublePipeline |
after A(i) (the §4.3 form), before it, or after the fixed input; or no counter |
| KMAC (§4.4) | Sp800108.DeriveKmac |
none (one KMAC call) |
The counter width r is 8, 16, 24, or 32 bits. PRFs are HMAC with any approved hash (SHA-1, SHA-2, SHA-512/t, SHA-3) or AES-CMAC with a 128-, 192-, or 256-bit key. TDEA is not available: SP 800-131A disallows it.
SP 800-56A: agreeing on a shared secret
Start from a domain: EcDomain.Named(EcCurve.P256) for one of the twelve NIST curves, or
FfcDomain.Named(FfcSafePrimeGroup.Ffdhe3072) for a safe-prime group. Keys come in four types per
family (static or ephemeral, public key or key pair), so a scheme cannot be given the wrong kind.
var p384 = EcDomain.Named(EcCurve.P384);
var theirKey = EcStaticPublicKey.Import(p384, publicKey); // full §5.6.2.3.3 validation
var myPair = EcStaticKeyPair.Import(p384, privateKey, publicKey); // also checks the halves match
var fips186 = FfcDomain.ImportFips186(p, q, g, FfcDomainAssurance.TrustedAuthority("Our CA policy 4.2"));
Importing a public key runs full validation (§5.6.2.3). KeyPair.Import also checks that the
private and public halves belong together. FIPS 186-type finite-field parameters need assurance:
either replay of their FIPS 186-4 generation evidence (FfcDomainAssurance.Fips186Evidence) or
a named trusted authority.
EcSchemes and FfcSchemes have the same methods:
| Method | SP 800-56A scheme (ECC / FFC) | Z |
|---|---|---|
Ephemeral |
Ephemeral Unified / dhEphem | CDH(ephemeral, ephemeral) |
Static |
Static Unified / dhStatic | CDH(static, static) |
OneFlowAsPartyU, OneFlowAsPartyV |
One-Pass DH / dhOneFlow | CDH(U ephemeral, V static) |
Hybrid |
Full Unified / dhHybrid1 | Ze || Zs |
HybridOneFlowAsPartyU, ...AsPartyV |
One-Pass Unified / dhHybridOneFlow | Ze || Zs |
Mqv2 |
Full MQV / MQV2 | MQV |
Mqv1AsPartyU, Mqv1AsPartyV |
One-Pass MQV / MQV1 | MQV |
Party U is the initiator and V the responder. Static keys authenticate the parties; the purely ephemeral scheme needs authentication from somewhere else, such as signatures.
SP 800-56C: keys from a shared secret
Sp80056C.OneStep(z, function, fixedInfo, length, strength) hashes counter || Z || FixedInfo
with a hash, HMAC, or KMAC (OneStepFunction). The two-step method extracts a key-derivation key
with HMAC or AES-CMAC and expands it with SP 800-108. To derive several keys from one Z, extract
once and expand once per key with distinct fixed input:
byte[] z = FfcSchemes.Mqv2(myStatic, myEphemeral, theirStatic, theirEphemeral);
// Extract once, then expand one key per purpose with distinct fixed input.
var extraction = Extraction.Hmac(NistHashAlgorithm.Sha256, salt: "protocol v1 salt"u8);
byte[] kdk = Sp80056C.Extract(z, extraction, SecurityStrength.Bits128);
var length = BitLength.Create(256);
byte[] encryptionKey = Sp800108.Derive(kdk, extraction.ExpansionPrf, "encrypt"u8, fixedInfo, length);
byte[] macKey = Sp800108.Derive(kdk, extraction.ExpansionPrf, "mac"u8, fixedInfo, length);
Array.Clear(z);
Array.Clear(kdk);
Extraction.ExpansionPrf is the PRF the standard requires for expansion: the same HMAC, or
AES-128-CMAC after AES-CMAC extraction. The security strength you pass is checked against the
function you chose.
Key confirmation
After both sides derive keying material, its first bits can serve as MacKey to prove that both derived the same thing (§5.9):
var mac = KeyConfirmationMac.Hmac(NistHashAlgorithm.Sha256);
var tagLength = BitLength.Create(128);
// Bob (party V) proves he derived the same MacKey. Alice (party U) checks the tag.
var fromBob = new KeyConfirmationContext(KeyConfirmationMode.Bilateral, provider: Party.V,
partyUId: "alice"u8, partyVId: "bob"u8, alicePublic, bobPublic);
byte[] tag = KeyConfirmation.GenerateTag(macKey, fromBob, mac, tagLength, SecurityStrength.Bits128);
bool confirmed = KeyConfirmation.VerifyTag(tag, macKey, fromBob, mac, tagLength, SecurityStrength.Bits128);
The MAC is HMAC, AES-CMAC, or KMAC (with customization string "KC"). Tags are 64 bits up to the MAC's output length and are compared in constant time.
Errors
try
{
byte[] key = Sp800108.Derive(keyDerivationKey, Prf.AesCmac(128), "label"u8, "context"u8, BitLength.Create(128));
}
catch (InvalidKeyException e) when (e.Failure == KeyFailure.InvalidPublicKey)
{
reason = "the peer sent an invalid public key";
}
catch (Kdf108Exception e)
{
reason = e.Message; // every library failure derives from Kdf108Exception
}
Dependency injection and logging
builder.Services.AddKdf108(); // ISp800108Kdf, ISp80056CKdf, IEcKeyAgreement, IFfcKeyAgreement, IKeyConfirmation
public sealed class SessionKeys(ISp800108Kdf kdf)
{
public byte[] ForTenant(ReadOnlySpan<byte> keyDerivationKey, int tenant) =>
kdf.Derive(keyDerivationKey, Prf.Hmac(NistHashAlgorithm.Sha256),
label: "session"u8, context: Encoding.UTF8.GetBytes($"tenant:{tenant}"), BitLength.Create(256));
}
Services log each operation at Debug (algorithm and output length) and each rejection at Warning (the reason), with stable event IDs: 1 for a completed operation, 2 for a rejection, 3 for a key-confirmation tag mismatch. Keys, shared secrets, and derived output are never logged.