-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HKDF performance #52
Comments
It's mostly Benchmark class (click to open)public class HkdfBenchmark
{
private static byte[] rawSharedSecret = new byte[32];
private static SharedSecret importedSharedSecret = SharedSecret.Import(rawSharedSecret);
private static byte[] salt = new byte[32];
private static byte[] info = new byte[32];
[Params(16, 512)]
public int OutputLength;
[ParamsSource(nameof(Hkdfs))]
public KeyDerivationAlgorithm Hkdf;
public static KeyDerivationAlgorithm[] Hkdfs => new KeyDerivationAlgorithm[]
{
KeyDerivationAlgorithm.HkdfSha256,
KeyDerivationAlgorithm.HkdfSha512,
};
[Benchmark]
public void BenchmarkHkdfWithSecretImport()
{
var sharedSecret = SharedSecret.Import(rawSharedSecret);
Hkdf.DeriveBytes(sharedSecret, salt, info, OutputLength);
}
[Benchmark]
public void BenchmarkHkdfWithoutSecretImport()
{
Hkdf.DeriveBytes(importedSharedSecret, salt, info, OutputLength);
}
} Benchmark results:
I don't know, maybe it's worth taking a look not only at For example, on my PC the NSec's implementation of SHA-512 is faster that the .NET 5's implementation for small inputs: Benchmark class for SHA-512 (click to open)public class Sha512Benchmark
{
[ParamsSource(nameof(Inputs))]
public byte[] Input;
public IEnumerable<byte[]> Inputs => new[]
{
new byte[16],
new byte[64],
new byte[256],
new byte[1024],
new byte[4096],
new byte[16384],
new byte[65536]
};
[Benchmark]
public void BenchmarkNetSha512()
{
using var sha = SHA512.Create();
sha.ComputeHash(Input);
}
[Benchmark]
public void BenchmarkNSecSha512()
{
var sha = new Sha512();
sha.Hash(Input);
}
} Benchmark results (click to open)
So, I'd naturally expect NSec's HMAC-SHA-512 be faster than .NET 5's for small inputs, but it turns out not to be the case. This makes me think that there's probably a potential for optimisation in NSec's HMAC (both the Benchmark class for HMAC-SHA-512 (click to open)public class HmacSha512Benchmark
{
private static byte[] rawKey = new byte[64];
private static Key importedKey = Key.Import(MacAlgorithm.HmacSha512, rawKey, KeyBlobFormat.RawSymmetricKey);
[ParamsSource(nameof(Messages))]
public byte[] Message;
public IEnumerable<byte[]> Messages => new[]
{
new byte[16],
new byte[64],
new byte[256],
new byte[1024],
new byte[4096],
new byte[16384],
new byte[65536]
};
[Benchmark]
public void BenchmarkNetHmacSha512()
{
using var hmac = new HMACSHA512(rawKey);
hmac.ComputeHash(Message);
}
[Benchmark]
public void BenchmarkNSecHmacSha512WithKeyImport()
{
var hmac = MacAlgorithm.HmacSha512;
hmac.Mac(Key.Import(hmac, rawKey, KeyBlobFormat.RawSymmetricKey), Message);
}
[Benchmark]
public void BenchmarkNSecHmacSha512WithoutKeyImport()
{
var hmac = MacAlgorithm.HmacSha512;
hmac.Mac(importedKey, Message);
}
} Benchmark results (click to open)
So, in total it makes four candidates for optimisation:
Not sure though that all four really need it and even can be optimised. Maybe some of them, like |
Wow, that's quite a detailed analysis. Thanks a lot!
That leaves NSec's HKDF implementation itself. At first glance, it doesn't look too different from what HKDF.Standard does or from libsodium's upcoming implementation. I'll have to dig a bit more into this sometime. Overall, I'm a bit wary of implementing cryptographic primitives myself. Even though it's "just" HKDF and probably not very difficult to get right, I think it would be better to defer to established, well-tested implementations. So, maybe, the "solution" to the performance problem here is just to use .NET's HKDF where available and/or libsodium's HKDF when that's released... |
Happy to help!
It must be the ability to initialize HMAC with a key once and compute multiple MACs that gives .NET's HMAC-based implementations of HKDF noticeable performance boost. NSec's HKDF reinitializes HMAC on every iteration during the Expand stage, and it looks like currently there's no way around this - libsodium's documentation specifically requires so:
For example, the derivation of 4096-bit key (as in the HKDF.Standard benchmark) via SHA-512 requires 8 MAC computations with the same key during the Expand stage. Initializing HMAC once instead of eight times more than doubles the speed of this group of operations: Benchmark classpublic class BatchHmacSha512Benchmark
{
private static byte[] rawKey = new byte[64];
private static Key importedKey = Key.Import(MacAlgorithm.HmacSha512, rawKey, KeyBlobFormat.RawSymmetricKey);
private static byte[] message = new byte[97];
[Benchmark]
public void BenchmarkNSecHmacSha512_8Inits_8Macs()
{
for (int i = 0; i < 8; i++)
MacAlgorithm.HmacSha512.Mac(importedKey, message);
}
[Benchmark]
public void BenchmarkNetHmacSha512_8Inits_8Macs()
{
for (int i = 0; i < 8; i++)
{
using var hmac = new HMACSHA512(rawKey);
hmac.ComputeHash(message);
}
}
[Benchmark]
public void BenchmarkNetHmacSha512_1Init_8Macs()
{
using var hmac = new HMACSHA512(rawKey);
for (int i = 0; i < 8; i++)
hmac.ComputeHash(message);
}
}
So, it's the key import and MAC computation operations that contribute mostly to the execution time, maybe there are other optimizations possible (e.g., moving Derivation of 4096-bit key using SHA-512 (as in the HKDF.Standard benchmark)
Derivation of 128-bit key using SHA-512 (as in the HKDF.Standard benchmark)
|
I've added new overloads to the HKDF class and factored out the HMAC initialization in a new branch.
libsodium mutates the state in place, so, after updating or finalizing it, the original state is gone and needs to be recreated in the next iteration. I didn't think that initialization would take so much time, so I just reinitialized the original state in every iteration. But, of course, it's also possible to keep a copy of the original state and just initialize the state from that.
As far as I know, after checking the scopes of variables, the C# compiler just hoists all variables to the top of the method. So this shouldn't make a difference.
Thanks again for the detailed benchmarks! |
Great result!
You are welcome :) |
HKDF.Standard claims to be 2.6 - 6.7 times faster than NSec.
@andreimilto Any quick thoughts on why NSec is so much slower?
https://github.com/andreimilto/HKDF.Standard/blob/67f5f1078b4be61746047a57842d2fa1ea042377/src/HkdfStandard.Benchmark/KeyDerivationBenchmark.cs#L121-L127
Is it the
SharedSecret.Import
or theDeriveBytes
call?The text was updated successfully, but these errors were encountered: