Add unit tests for CngAuthenticatedEncryptorBase, PBKDF2, and SP800_108-CTR-HMACSHA512.

This commit is contained in:
Levi B 2014-10-16 10:32:16 -07:00
parent 796acc0e34
commit cd33cbfc8f
10 changed files with 293 additions and 55 deletions

View File

@ -219,7 +219,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Managed
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
DeriveKeysWithContextHeader(
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,
@ -285,14 +285,6 @@ namespace Microsoft.AspNet.Security.DataProtection.Managed
}
}
private static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
{
byte[] combinedContext = new byte[checked(contextHeader.Length + context.Count)];
Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length);
Buffer.BlockCopy(context.Array, context.Offset, combinedContext, contextHeader.Length, context.Count);
ManagedSP800_108_CTR_HMACSHA512.DeriveKeys(kdk, label, new ArraySegment<byte>(combinedContext), prfFactory, output);
}
public void Dispose()
{
_keyDerivationKey.Dispose();
@ -336,7 +328,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Managed
try
{
_keyDerivationKey.WriteSecretIntoBuffer(new ArraySegment<byte>(decryptedKdk));
DeriveKeysWithContextHeader(
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(
kdk: decryptedKdk,
label: additionalAuthenticatedData,
contextHeader: _contextHeader,

View File

@ -53,5 +53,13 @@ namespace Microsoft.AspNet.Security.DataProtection.SP800_108
}
}
}
public static void DeriveKeysWithContextHeader(byte[] kdk, ArraySegment<byte> label, byte[] contextHeader, ArraySegment<byte> context, Func<byte[], HashAlgorithm> prfFactory, ArraySegment<byte> output)
{
byte[] combinedContext = new byte[checked(contextHeader.Length + context.Count)];
Buffer.BlockCopy(contextHeader, 0, combinedContext, 0, contextHeader.Length);
Buffer.BlockCopy(context.Array, context.Offset, combinedContext, contextHeader.Length, context.Count);
DeriveKeys(kdk, label, new ArraySegment<byte>(combinedContext), prfFactory, output);
}
}
}

View File

@ -2,8 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Security.DataProtection.SafeHandles;
using Microsoft.Win32.SafeHandles;
using Microsoft.AspNet.Security.DataProtection.Cng;
namespace Microsoft.AspNet.Security.DataProtection.SP800_108
{
@ -16,8 +15,6 @@ namespace Microsoft.AspNet.Security.DataProtection.SP800_108
/// </remarks>
internal unsafe static class SP800_108_CTR_HMACSHA512Util
{
private static readonly bool _isWin8OrLater = GetIsRunningWin8OrLater();
// Creates a provider with an empty key.
public static ISP800_108_CTR_HMACSHA512Provider CreateEmptyProvider()
{
@ -28,9 +25,14 @@ namespace Microsoft.AspNet.Security.DataProtection.SP800_108
// Creates a provider from the given key.
public static ISP800_108_CTR_HMACSHA512Provider CreateProvider(byte* pbKdk, uint cbKdk)
{
return (_isWin8OrLater)
? (ISP800_108_CTR_HMACSHA512Provider)new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk)
: (ISP800_108_CTR_HMACSHA512Provider)new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk);
if (OSVersionUtil.IsBCryptOnWin8OrLaterAvailable())
{
return new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk);
}
else
{
return new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk);
}
}
// Creates a provider from the given secret.
@ -57,37 +59,5 @@ namespace Microsoft.AspNet.Security.DataProtection.SP800_108
}
}
}
private static bool GetIsRunningWin8OrLater()
{
// In priority order, our three implementations are Win8, Win7, and "other".
const string BCRYPT_LIB = "bcrypt.dll";
SafeLibraryHandle bcryptLibHandle = null;
try
{
bcryptLibHandle = SafeLibraryHandle.Open(BCRYPT_LIB);
}
catch
{
// BCrypt not available? We'll fall back to managed code paths.
}
if (bcryptLibHandle != null)
{
using (bcryptLibHandle)
{
if (bcryptLibHandle.DoesProcExist("BCryptKeyDerivation"))
{
// We're running on Win8+.
return true;
}
}
}
// Not running on Win8+
return false;
}
}
}

View File

@ -90,7 +90,7 @@ namespace Microsoft.AspNet.Security.DataProtection.SP800_108
{
hashHandle.HashData(pbKdk, cbKdk, pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES);
}
return CachedAlgorithmHandles.SP800_108_CTR_HMAC.GenerateSymmetricKey(pbKdk, cbKdk);
return CachedAlgorithmHandles.SP800_108_CTR_HMAC.GenerateSymmetricKey(pbHashedKey, SHA512_DIGEST_SIZE_IN_BYTES);
}
finally
{

View File

@ -38,8 +38,6 @@ namespace Microsoft.AspNet.Security.DataProtection.SafeHandles
public BCryptHashHandle CreateHmac(byte* pbKey, uint cbKey)
{
Debug.Assert(pbKey != null);
Debug.Assert(cbKey != 0);
return CreateHashImpl(pbKey, cbKey);
}

View File

@ -0,0 +1,109 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Security.DataProtection.Cng;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test.Cng
{
public unsafe class CngAuthenticatedEncryptorBaseTests
{
[Fact]
public void Decrypt_ForwardsArraySegment()
{
// Arrange
var ciphertext = new ArraySegment<byte>(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, 3, 2);
var aad = new ArraySegment<byte>(new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }, 1, 4);
var encryptorMock = new Mock<MockableEncryptor>();
encryptorMock
.Setup(o => o.DecryptHook(It.IsAny<IntPtr>(), 2, It.IsAny<IntPtr>(), 4))
.Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) =>
{
// ensure that pointers started at the right place
Assert.Equal((byte)0x03, *(byte*)pbCiphertext);
Assert.Equal((byte)0x11, *(byte*)pbAdditionalAuthenticatedData);
return new byte[] { 0x20, 0x21, 0x22 };
});
// Act
var retVal = encryptorMock.Object.Decrypt(ciphertext, aad);
// Assert
Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal);
}
[Fact]
public void Decrypt_HandlesEmptyAADPointerFixup()
{
// Arrange
var ciphertext = new ArraySegment<byte>(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, 3, 2);
var aad = new ArraySegment<byte>(new byte[0]);
var encryptorMock = new Mock<MockableEncryptor>();
encryptorMock
.Setup(o => o.DecryptHook(It.IsAny<IntPtr>(), 2, It.IsAny<IntPtr>(), 0))
.Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) =>
{
// ensure that pointers started at the right place
Assert.Equal((byte)0x03, *(byte*)pbCiphertext);
Assert.NotEqual(IntPtr.Zero, pbAdditionalAuthenticatedData); // CNG will complain if this pointer is zero
return new byte[] { 0x20, 0x21, 0x22 };
});
// Act
var retVal = encryptorMock.Object.Decrypt(ciphertext, aad);
// Assert
Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal);
}
[Fact]
public void Decrypt_HandlesEmptyCiphertextPointerFixup()
{
// Arrange
var ciphertext = new ArraySegment<byte>(new byte[0]);
var aad = new ArraySegment<byte>(new byte[] { 0x10, 0x11, 0x12, 0x13, 0x14 }, 1, 4);
var encryptorMock = new Mock<MockableEncryptor>();
encryptorMock
.Setup(o => o.DecryptHook(It.IsAny<IntPtr>(), 0, It.IsAny<IntPtr>(), 4))
.Returns((IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData) =>
{
// ensure that pointers started at the right place
Assert.NotEqual(IntPtr.Zero, pbCiphertext); // CNG will complain if this pointer is zero
Assert.Equal((byte)0x11, *(byte*)pbAdditionalAuthenticatedData);
return new byte[] { 0x20, 0x21, 0x22 };
});
// Act
var retVal = encryptorMock.Object.Decrypt(ciphertext, aad);
// Assert
Assert.Equal(new byte[] { 0x20, 0x21, 0x22 }, retVal);
}
internal abstract class MockableEncryptor : CngAuthenticatedEncryptorBase
{
public override void Dispose()
{
}
public abstract byte[] DecryptHook(IntPtr pbCiphertext, uint cbCiphertext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData);
protected override sealed unsafe byte[] DecryptImpl(byte* pbCiphertext, uint cbCiphertext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
{
return DecryptHook((IntPtr)pbCiphertext, cbCiphertext, (IntPtr)pbAdditionalAuthenticatedData, cbAdditionalAuthenticatedData);
}
public abstract byte[] EncryptHook(IntPtr pbPlaintext, uint cbPlaintext, IntPtr pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer);
protected override sealed unsafe byte[] EncryptImpl(byte* pbPlaintext, uint cbPlaintext, byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData, uint cbPreBuffer, uint cbPostBuffer)
{
return EncryptHook((IntPtr)pbPlaintext, cbPlaintext, (IntPtr)pbAdditionalAuthenticatedData, cbAdditionalAuthenticatedData, cbPreBuffer, cbPostBuffer);
}
}
}
}

View File

@ -22,8 +22,5 @@
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,66 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using Microsoft.AspNet.Security.DataProtection.PBKDF2;
using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test.PBKDF2
{
public class Pbkdf2Tests
{
// The 'numBytesRequested' parameters below are chosen to exercise code paths where
// this value straddles the digest length of the PRF. We only use 5 iterations so
// that our unit tests are fast.
[Theory]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 - 1, "efmxNcKD/U1urTEDGvsThlPnHA==")]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 + 0, "efmxNcKD/U1urTEDGvsThlPnHDI=")]
[InlineData("my-password", KeyDerivationPrf.Sha1, 5, 160 / 8 + 1, "efmxNcKD/U1urTEDGvsThlPnHDLk")]
[InlineData("my-password", KeyDerivationPrf.Sha256, 5, 256 / 8 - 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRA==")]
[InlineData("my-password", KeyDerivationPrf.Sha256, 5, 256 / 8 + 0, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLo=")]
[InlineData("my-password", KeyDerivationPrf.Sha256, 5, 256 / 8 + 1, "JRNz8bPKS02EG1vf7eWjA64IeeI+TI8gBEwb1oVvRLpk")]
[InlineData("my-password", KeyDerivationPrf.Sha512, 5, 512 / 8 - 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm9")]
[InlineData("my-password", KeyDerivationPrf.Sha512, 5, 512 / 8 + 0, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Q==")]
[InlineData("my-password", KeyDerivationPrf.Sha512, 5, 512 / 8 + 1, "ZTallQJrFn0279xIzaiA1XqatVTGei+ZjKngA7bIMtKMDUw6YJeGUQpFG8iGTgN+ri3LNDktNbzwfcSyZmm90Wk=")]
public void RunTest_Normal(string password, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedValueAsBase64)
{
// Arrange
byte[] salt = new byte[256];
for (int i = 0; i < salt.Length; i++)
{
salt[i] = (byte)i;
}
// Act & assert - fully managed, Win7, and Win8
TestProvider<ManagedPbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
TestProvider<Win7Pbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
TestProvider<Win8Pbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedValueAsBase64);
}
[Fact]
public void RunTest_WithLongPassword()
{
// Arrange
string password = new String('x', 50000); // 50,000 char password
byte[] salt = Encoding.UTF8.GetBytes("salt");
const string expectedDerivedKeyBase64 = "Sc+V/c3fiZq5Z5qH3iavAiojTsW97FAp2eBNmCQAwCNzA8hfhFFYyQLIMK65qPnBFHOHXQPwAxNQNhaEAH9hzfiaNBSRJpF9V4rpl02d5ZpI6cZbsQFF7TJW7XJzQVpYoPDgJlg0xVmYLhn1E9qMtUVUuXsBjOOdd7K1M+ZI00c=";
const KeyDerivationPrf prf = KeyDerivationPrf.Sha256;
const int iterationCount = 5;
const int numBytesRequested = 128;
// Act & assert - fully managed, Win7, and Win8
TestProvider<ManagedPbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedDerivedKeyBase64);
TestProvider<Win7Pbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedDerivedKeyBase64);
TestProvider<Win8Pbkdf2Provider>(password, salt, prf, iterationCount, numBytesRequested, expectedDerivedKeyBase64);
}
private static void TestProvider<TProvider>(string password, byte[] salt, KeyDerivationPrf prf, int iterationCount, int numBytesRequested, string expectedDerivedKeyAsBase64)
where TProvider : IPbkdf2Provider, new()
{
byte[] derivedKey = new TProvider().DeriveKey(password, salt, prf, iterationCount, numBytesRequested);
Assert.Equal(numBytesRequested, derivedKey.Length);
Assert.Equal(expectedDerivedKeyAsBase64, Convert.ToBase64String(derivedKey));
}
}
}

View File

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.CompilerServices;
// for unit testing
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

View File

@ -0,0 +1,90 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Security.DataProtection.SP800_108;
using Xunit;
namespace Microsoft.AspNet.Security.DataProtection.Test.SP800_108
{
public unsafe class SP800_108Tests
{
private delegate ISP800_108_CTR_HMACSHA512Provider ProviderFactory(byte* pbKdk, uint cbKdk);
// The 'numBytesRequested' parameters below are chosen to exercise code paths where
// this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
[Theory]
[InlineData(512 / 8 - 1, "V47WmHzPSkdC2vkLAomIjCzZlDOAetll3yJLcSvon7LJFjJpEN+KnSNp+gIpeydKMsENkflbrIZ/3s6GkEaH")]
[InlineData(512 / 8 + 0, "mVaFM4deXLl610CmnCteNzxgbM/VkmKznAlPauHcDBn0le06uOjAKLHx0LfoU2/Ttq9nd78Y6Nk6wArmdwJgJg==")]
[InlineData(512 / 8 + 1, "GaHPeqdUxriFpjRtkYQYWr5/iqneD/+hPhVJQt4rXblxSpB1UUqGqL00DMU/FJkX0iMCfqUjQXtXyfks+p++Ev4=")]
public void DeriveKeyWithContextHeader_Normal(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
{
// Arrange
byte[] kdk = Encoding.UTF8.GetBytes("kdk");
byte[] label = Encoding.UTF8.GetBytes("label");
byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
byte[] context = Encoding.UTF8.GetBytes("context");
// Act & assert - managed, Win7, Win8
TestManagedKeyDerivation(kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
TestCngKeyDerivation((pbKdk, cbKdk) => new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
TestCngKeyDerivation((pbKdk, cbKdk) => new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
}
// The 'numBytesRequested' parameters below are chosen to exercise code paths where
// this value straddles the digest length of the PRF (which is hardcoded to HMACSHA512).
[Theory]
[InlineData(512 / 8 - 1, "rt2hM6kkQ8hAXmkHx0TU4o3Q+S7fie6b3S1LAq107k++P9v8uSYA2G+WX3pJf9ZkpYrTKD7WUIoLkgA1R9lk")]
[InlineData(512 / 8 + 0, "RKiXmHSrWq5gkiRSyNZWNJrMR0jDyYHJMt9odOayRAE5wLSX2caINpQmfzTH7voJQi3tbn5MmD//dcspghfBiw==")]
[InlineData(512 / 8 + 1, "KedXO0zAIZ3AfnPqY1NnXxpC3HDHIxefG4bwD3g6nWYEc5+q7pjbam71Yqj0zgHMNC9Z7BX3wS1/tajFocRWZUk=")]
public void DeriveKeyWithContextHeader_LongKey(int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
{
// Arrange
byte[] kdk = new byte[50000]; // CNG can't normally handle a 50,000 byte KDK, but we coerce it into working :)
for (int i = 0; i < kdk.Length; i++)
{
kdk[i] = (byte)i;
}
byte[] label = Encoding.UTF8.GetBytes("label");
byte[] contextHeader = Encoding.UTF8.GetBytes("contextHeader");
byte[] context = Encoding.UTF8.GetBytes("context");
// Act & assert - managed, Win7, Win8
TestManagedKeyDerivation(kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
TestCngKeyDerivation((pbKdk, cbKdk) => new Win7SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
TestCngKeyDerivation((pbKdk, cbKdk) => new Win8SP800_108_CTR_HMACSHA512Provider(pbKdk, cbKdk), kdk, label, contextHeader, context, numDerivedBytes, expectedDerivedSubkeyAsBase64);
}
private static void TestCngKeyDerivation(ProviderFactory factory, byte[] kdk, byte[] label, byte[] contextHeader, byte[] context, int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
{
byte[] derivedSubkey = new byte[numDerivedBytes];
fixed (byte* pbKdk = kdk)
fixed (byte* pbLabel = label)
fixed (byte* pbContext = context)
fixed (byte* pbDerivedSubkey = derivedSubkey)
{
ISP800_108_CTR_HMACSHA512Provider provider = factory(pbKdk, (uint)kdk.Length);
provider.DeriveKeyWithContextHeader(pbLabel, (uint)label.Length, contextHeader, pbContext, (uint)context.Length, pbDerivedSubkey, (uint)derivedSubkey.Length);
}
Assert.Equal(expectedDerivedSubkeyAsBase64, Convert.ToBase64String(derivedSubkey));
}
private static void TestManagedKeyDerivation(byte[] kdk, byte[] label, byte[] contextHeader, byte[] context, int numDerivedBytes, string expectedDerivedSubkeyAsBase64)
{
var labelSegment = new ArraySegment<byte>(new byte[label.Length + 10], 3, label.Length);
Buffer.BlockCopy(label, 0, labelSegment.Array, labelSegment.Offset, labelSegment.Count);
var contextSegment = new ArraySegment<byte>(new byte[context.Length + 10], 5, context.Length);
Buffer.BlockCopy(context, 0, contextSegment.Array, contextSegment.Offset, contextSegment.Count);
var derivedSubkeySegment = new ArraySegment<byte>(new byte[numDerivedBytes + 10], 4, numDerivedBytes);
ManagedSP800_108_CTR_HMACSHA512.DeriveKeysWithContextHeader(kdk, labelSegment, contextHeader, contextSegment,
bytes => new HMACSHA512(bytes), derivedSubkeySegment);
Assert.Equal(expectedDerivedSubkeyAsBase64, Convert.ToBase64String(derivedSubkeySegment.AsStandaloneArray()));
}
}
}