1
0
mirror of https://github.com/wolfSSL/wolfssl.git synced 2026-10-24 04:09:07 +03:00
Files
2026-10-02 09:32:25 -06:00

392 lines
20 KiB
C#

/* KdfTests.cs
*
* Copyright (C) 2006-2026 wolfSSL Inc.
*
* This file is part of wolfSSL.
*
* wolfSSL is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* wolfSSL is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
*/
using System;
using System.Buffers.Binary;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace wolfSSL.CSharp.Fips.Test
{
internal static class KdfTests
{
private static readonly FipsHashType[] TlsHashes =
{ FipsHashType.Sha256, FipsHashType.Sha384, FipsHashType.Sha512 };
public static void Run()
{
T.Section("KDFs");
/* The salt is the HMAC key of HKDF-Extract; the module applies the
* 112-bit HMAC minimum to it. RFC 5869 test case 1 (13-byte salt)
* is therefore refused. An empty salt is allowed (zeros of the
* hash length are used). */
T.Run("HKDF salt of 1-13 bytes is refused (HMAC_MIN_KEYLEN_E)", () =>
{
byte[] ikm = Enumerable.Repeat((byte)0x0b, 22).ToArray();
for (int s = 1; s <= 13; s++)
{
byte[] salt = Enumerable.Range(1, s).Select(i => (byte)i).ToArray();
T.Throws(FipsError.HMAC_MIN_KEYLEN_E,
() => FipsKdf.HkdfExtract(FipsHashType.Sha256, salt, ikm), s + "-byte salt");
}
T.Equal(32, FipsKdf.HkdfExtract(FipsHashType.Sha256, new byte[14], ikm).Length, "14-byte salt");
});
T.Run("HKDF RFC 5869 test case 3 (SHA-256, empty salt), matches .NET", () =>
{
byte[] ikm = Enumerable.Repeat((byte)0x0b, 22).ToArray();
byte[] prk = FipsKdf.HkdfExtract(FipsHashType.Sha256, null, ikm);
byte[] okm = FipsKdf.Hkdf(FipsHashType.Sha256, ikm, null, null, 42);
T.Bytes(T.Hex("19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04"), prk, "PRK");
T.Bytes(T.Hex("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"),
okm, "OKM");
T.Bytes(HKDF.DeriveKey(HashAlgorithmName.SHA256, ikm, 42), okm, ".NET OKM");
T.Bytes(FipsKdf.HkdfExpand(FipsHashType.Sha256, prk, null, 42), okm, "expand");
T.Bytes(okm, FipsKdf.Hkdf(FipsHashType.Sha256, ikm, Array.Empty<byte>(), null, 42), "empty salt = no salt");
});
foreach (FipsHashType fh in TlsHashes)
{
FipsHashType h = fh;
T.Run("HKDF " + h + " matches .NET HKDF", () =>
{
HashAlgorithmName nh = NetHash(h);
using var rng = new FipsRng();
for (int i = 0; i < 20; i++)
{
/* salt empty or >= 14 bytes (see the HMAC minimum test) */
byte[] ikm = rng.Generate(16 + i), salt = rng.Generate(i == 0 ? 0 : 13 + i),
info = rng.Generate(i % 7);
int len = 1 + i * 13;
T.Bytes(HKDF.DeriveKey(nh, ikm, len, salt, info),
FipsKdf.Hkdf(h, ikm, salt, info, len), "iteration " + i);
T.Bytes(HKDF.Extract(nh, ikm, salt), FipsKdf.HkdfExtract(h, salt, ikm), "extract " + i);
}
});
}
foreach (FipsHashType fh in new[] { FipsHashType.Sha1, FipsHashType.Sha256, FipsHashType.Sha512 })
{
FipsHashType h = fh;
T.Run("SSH KDF " + h + " matches RFC 4253 reference", () =>
{
using var rng = new FipsRng();
byte[] hh = rng.Generate(FipsHash.DigestSizeOf(h)), sid = rng.Generate(FipsHash.DigestSizeOf(h));
for (int i = 0; i < 12; i++)
{
byte[] k = rng.Generate(32 + i);
k[0] = (byte)(i % 2 == 0 ? 0x80 | k[0] : (k[0] & 0x7f) | 1); /* both mpint pad cases */
char id = (char)('A' + i % 6);
int len = 16 + i * 11;
T.Bytes(SshReference(h, id, k, hh, sid, len), FipsKdf.SshKdf(h, id, k, hh, sid, len),
"key " + id + " len " + len);
}
});
}
/* Regression: an empty IKM must equal HashLen zero bytes and must
* not reach the module as ikmLen 0 (the v5.2.x module then writes
* HashLen bytes into the buffer). */
T.Run("TLS 1.3 extract: empty IKM equals HashLen zeros (SHA-256/384)", () =>
{
foreach (FipsHashType h in new[] { FipsHashType.Sha256, FipsHashType.Sha384 })
{
int n = FipsHash.DigestSizeOf(h);
for (int i = 0; i < 50; i++)
{
T.Bytes(HKDF.Extract(NetHash(h), new byte[n], Array.Empty<byte>()),
FipsKdf.Tls13Extract(h, null, Array.Empty<byte>()), h + " empty IKM vs .NET HKDF");
}
byte[] ikm = { 1, 2, 3 };
FipsKdf.Tls13Extract(h, new byte[n], ikm);
T.Bytes(new byte[] { 1, 2, 3 }, ikm, h + " caller IKM unchanged");
}
});
T.Run("TLS 1.3 KDFs refuse hashes other than SHA-256 and SHA-384", () =>
{
bool threw = false;
try { FipsKdf.Tls13Extract(FipsHashType.Sha512, null, new byte[64]); } catch (NotSupportedException) { threw = true; }
T.True(threw, "SHA-512 accepted");
});
T.Run("TLS 1.3 expand-label: HkdfLabel limited to the module buffer and 255-byte fields", () =>
{
int max = FipsKdf.Tls13LabelMax;
Console.WriteLine(" module HkdfLabel buffer: " + max + " bytes");
byte[] secret = new byte[32];
int ctxAtMax = max - 4 - 6 - 7; /* "tls13 " + "derived" */
T.Equal(32, FipsKdf.Tls13ExpandLabel(FipsHashType.Sha256, secret, "derived", new byte[ctxAtMax], 32).Length,
"at the limit");
foreach (var (label, ctx, what) in new[] {
("derived", ctxAtMax + 1, "one byte over"),
(new string('a', 250), 0, "protocol + label over 255"),
("x", 256, "context over 255"),
("", 0, "empty label"),
(new string('a', 128), 32, "128-byte label") })
{
bool threw = false;
try { FipsKdf.Tls13ExpandLabel(FipsHashType.Sha256, secret, label, new byte[ctx], 32); }
catch (ArgumentException) { threw = true; }
T.True(threw, what + " accepted");
}
});
/* the module keeps redundant leading zeros in the mpint (RFC 4251
* 5 requires minimal form), so K must be passed minimal */
T.Run("SSH KDF refuses K with leading zero bytes or zero", () =>
{
byte[] hh = new byte[32], sid = new byte[32];
foreach (byte[] k in new[] { new byte[] { 0, 5 }, new byte[] { 0, 0, 0x80 }, new byte[32], new byte[1], new byte[0] })
{
bool threw = false;
try { FipsKdf.SshKdf(FipsHashType.Sha256, 'C', k, hh, sid, 16); } catch (ArgumentException) { threw = true; }
T.True(threw, Convert.ToHexString(k) + " accepted");
}
});
T.Run("P_hash (TLS 1.2 PRF core) matches an HMAC reference (SHA-256/384/512)", () =>
{
using var rng = new FipsRng();
foreach (FipsHashType h in new[] { FipsHashType.Sha256, FipsHashType.Sha384, FipsHashType.Sha512 })
{
for (int i = 0; i < 5; i++)
{
byte[] secret = rng.Generate(48), seed = rng.Generate(13 + i);
int len = 20 + i * 37;
T.Bytes(PHashReference(h, secret, seed, len), FipsKdf.PHash(h, secret, seed, len), h + " len " + len);
}
}
});
/* Output checks: label || seed
* concatenation, EMS label, key block seed order (server random
* first) and the MAC id mapping for each hash. */
T.Run("TLS 1.2 PRF, EMS and key block match the RFC 5246 / 7627 reference", () =>
{
using var rng = new FipsRng();
foreach (FipsHashType h in new[] { FipsHashType.Sha256, FipsHashType.Sha384, FipsHashType.Sha512 })
{
byte[] pms = rng.Generate(48), sessionHash = rng.Generate(FipsHash.DigestSizeOf(h));
byte[] cr = rng.Generate(32), sr = rng.Generate(32);
byte[] ms = FipsKdf.Tls12ExtendedMasterSecret(h, pms, sessionHash);
T.Bytes(PrfReference(h, pms, "extended master secret", sessionHash, 48), ms, h + " EMS");
T.Bytes(PrfReference(h, ms, "key expansion", sr.Concat(cr).ToArray(), 104),
FipsKdf.Tls12KeyBlock(h, ms, cr, sr, 104), h + " key block (server || client)");
byte[] vd = rng.Generate(FipsHash.DigestSizeOf(h));
T.Bytes(PrfReference(h, ms, "client finished", vd, 12),
FipsKdf.Tls12Prf(h, ms, "client finished", vd, 12), h + " finished");
}
});
T.Run("TLS 1.2 non-EMS \"master secret\" derivation is refused (IG D.Q)", () =>
{
bool threw = false;
try { FipsKdf.Tls12Prf(FipsHashType.Sha256, new byte[48], "master secret", new byte[64], 48); }
catch (ArgumentException) { threw = true; }
T.True(threw, "non-EMS master secret derived");
/* the module PRF sees label || seed, so every split of
* "master secret" between them is the same derivation */
byte[] randoms = new byte[64];
foreach (int cut in new[] { 0, 1, 6, 7, 12 })
{
string lab = "master secret".Substring(0, cut);
byte[] seed = Encoding.ASCII.GetBytes("master secret".Substring(cut)).Concat(randoms).ToArray();
threw = false;
try { FipsKdf.Tls12Prf(FipsHashType.Sha256, new byte[48], lab, seed, 48); }
catch (ArgumentException) { threw = true; }
T.True(threw, "split \"" + lab + "\" + seed accepted");
}
/* the EMS label only through Tls12ExtendedMasterSecret, which checks the
* session hash: whole or split, it is refused here */
foreach (int cut in new[] { 22, 9, 0 })
{
string lab = "extended master secret".Substring(0, cut);
byte[] seed = Encoding.ASCII.GetBytes("extended master secret".Substring(cut)).Concat(randoms).ToArray();
threw = false;
try { FipsKdf.Tls12Prf(FipsHashType.Sha256, new byte[48], lab, seed, 48); }
catch (ArgumentException) { threw = true; }
T.True(threw, "EMS label \"" + lab + "\" + seed accepted by Tls12Prf");
}
/* labels that only share a prefix, or seeds that differ, are fine */
FipsKdf.Tls12Prf(FipsHashType.Sha256, new byte[48], "master", Encoding.ASCII.GetBytes(" key").Concat(randoms).ToArray(), 48);
FipsKdf.Tls12Prf(FipsHashType.Sha256, new byte[48], "", randoms, 48);
});
T.Run("TLS 1.3 Expand-Label matches .NET HKDF-Expand over an RFC 8446 HkdfLabel", () =>
{
using var rng = new FipsRng();
foreach (FipsHashType h in new[] { FipsHashType.Sha256, FipsHashType.Sha384 })
{
int hl = FipsHash.DigestSizeOf(h);
foreach (var (label, ctxLen, outLen) in new[] { ("derived", hl, hl), ("key", 0, 16), ("iv", 0, 12),
("c hs traffic", hl, hl), ("finished", 0, hl) })
{
byte[] secret = rng.Generate(hl), ctx = rng.Generate(ctxLen);
byte[] lab = Encoding.ASCII.GetBytes("tls13 " + label);
byte[] info = new byte[] { (byte)(outLen >> 8), (byte)outLen, (byte)lab.Length }
.Concat(lab).Append((byte)ctx.Length).Concat(ctx).ToArray();
T.Bytes(HKDF.Expand(NetHash(h), secret, outLen, info),
FipsKdf.Tls13ExpandLabel(h, secret, label, ctx, outLen), h + " " + label);
}
}
});
T.Run("KDF calls restore the private key read gate", () =>
{
FipsModule.SetPrivateKeyReadEnable(false);
FipsKdf.Hkdf(FipsHashType.Sha256, new byte[32], null, null, 32);
FipsKdf.Tls12ExtendedMasterSecret(FipsHashType.Sha256, new byte[48], new byte[32]);
T.True(!FipsModule.PrivateKeyReadEnabled, "gate left enabled");
});
T.Run("TLS 1.2 helpers name null arguments by their public names", () =>
{
var cases = new (string Param, Action Call)[] {
("clientRandom", () => FipsKdf.Tls12KeyBlock(FipsHashType.Sha256, new byte[48], null!, new byte[32], 40)),
("masterSecret", () => FipsKdf.Tls12KeyBlock(FipsHashType.Sha256, null!, new byte[32], new byte[32], 40)),
("preMasterSecret", () => FipsKdf.Tls12ExtendedMasterSecret(FipsHashType.Sha256, null!, new byte[32])),
("sessionHash", () => FipsKdf.Tls12ExtendedMasterSecret(FipsHashType.Sha256, new byte[48], null!)),
};
foreach (var (param, call) in cases)
{
string? got = null;
try { call(); } catch (ArgumentNullException e) { got = e.ParamName; }
T.Equal(param, got, "ParamName");
}
});
T.Run("EMS session hash must be a digest of the PRF hash (RFC 7627 4)", () =>
{
foreach (var (h, len) in new[] { (FipsHashType.Sha256, 64), (FipsHashType.Sha256, 48), (FipsHashType.Sha384, 32) })
{
bool threw = false;
try { FipsKdf.Tls12ExtendedMasterSecret(h, new byte[48], new byte[len]); }
catch (ArgumentException) { threw = true; }
T.True(threw, h + " with a " + len + "-byte session hash accepted");
}
});
T.Run("SSH KDF partial output matches the reference", () =>
{
byte[] k = { 5, 6, 7 }, hh = new byte[32], sid = new byte[32];
foreach (int len in new[] { 1, 31, 33, 45 })
{
T.Bytes(SshReference(FipsHashType.Sha256, 'B', new byte[] { 5, 6, 7 }, hh, sid, len),
FipsKdf.SshKdf(FipsHashType.Sha256, 'B', k, hh, sid, len), len + " bytes");
}
});
T.Run("general HKDF is not public (no CAVP validation)", () =>
{
foreach (string m in new[] { "Hkdf", "HkdfExtract", "HkdfExpand", "PHash" })
{
var mi = typeof(FipsKdf).GetMethod(m, System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
T.True(mi != null && !mi.IsPublic, m + " is public");
}
});
T.Run("KDF labels must be ASCII (no lossy '?' substitution)", () =>
{
bool threw = false;
try { FipsKdf.Tls13ExpandLabel(FipsHashType.Sha256, new byte[32], "cl\u00e9", new byte[32], 32); }
catch (ArgumentException) { threw = true; }
T.True(threw, "non-ASCII TLS 1.3 label accepted");
threw = false;
try { FipsKdf.Tls12Prf(FipsHashType.Sha256, new byte[48], "cl\u00e9", new byte[64], 48); }
catch (ArgumentException) { threw = true; }
T.True(threw, "non-ASCII TLS 1.2 label accepted");
T.True(!FipsModule.PrivateKeyReadEnabled, "gate left enabled");
});
T.Run("TLS PRF rejects non-TLS hashes", () =>
{
bool threw = false;
try { FipsKdf.Tls12Prf(FipsHashType.Sha3_256, new byte[48], "x", new byte[1], 16); }
catch (NotSupportedException) { threw = true; }
T.True(threw, "SHA3 accepted");
});
}
/* RFC 4253 7.2: K1 = HASH(mpint(K) || H || X || session_id),
* Kn = HASH(mpint(K) || H || K1 || ... || Kn-1). Test reference only. */
private static byte[] SshReference(FipsHashType h, char id, byte[] k, byte[] hh, byte[] sid, int len)
{
byte[] kk = k.SkipWhile(b => b == 0).ToArray();
if ((kk[0] & 0x80) != 0)
{
kk = new byte[] { 0 }.Concat(kk).ToArray();
}
byte[] mpLen = new byte[4];
BinaryPrimitives.WriteInt32BigEndian(mpLen, kk.Length); /* RFC 4251 mpint: big-endian length */
byte[] mp = mpLen.Concat(kk).ToArray();
Func<byte[], byte[]> hash = h switch
{
FipsHashType.Sha1 => SHA1.HashData,
FipsHashType.Sha256 => SHA256.HashData,
_ => SHA512.HashData
};
byte[] outp = hash(mp.Concat(hh).Append((byte)id).Concat(sid).ToArray());
while (outp.Length < len)
{
outp = outp.Concat(hash(mp.Concat(hh).Concat(outp).ToArray())).ToArray();
}
return outp.Take(len).ToArray();
}
/* RFC 5246 5: P_hash(secret, seed) = HMAC(secret, A(1) + seed) || ...,
* A(0) = seed, A(i) = HMAC(secret, A(i-1)). Test reference only. */
private static byte[] PHashReference(FipsHashType h, byte[] secret, byte[] seed, int len)
{
Func<byte[], byte[], byte[]> hmac = h switch
{
FipsHashType.Sha256 => HMACSHA256.HashData,
FipsHashType.Sha384 => HMACSHA384.HashData,
_ => HMACSHA512.HashData
};
var outp = new System.Collections.Generic.List<byte>();
byte[] a = seed;
while (outp.Count < len)
{
a = hmac(secret, a);
outp.AddRange(hmac(secret, a.Concat(seed).ToArray()));
}
return outp.Take(len).ToArray();
}
/* PRF(secret, label, seed) = P_hash(secret, label || seed). */
private static byte[] PrfReference(FipsHashType h, byte[] secret, string label, byte[] seed, int len) =>
PHashReference(h, secret, Encoding.ASCII.GetBytes(label).Concat(seed).ToArray(), len);
private static HashAlgorithmName NetHash(FipsHashType h) => h switch
{
FipsHashType.Sha256 => HashAlgorithmName.SHA256,
FipsHashType.Sha384 => HashAlgorithmName.SHA384,
_ => HashAlgorithmName.SHA512
};
}
}