Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,21 @@
{
// Create the AES provider
SymmetricAlgorithm symmetricAlgorithm = Aes.Create();
symmetricAlgorithm.Mode = CipherMode.ECB; // CodeQL [SM02199] Approved necessary usage of AES-ECB for implementing AES-KW
symmetricAlgorithm.Padding = PaddingMode.None;
symmetricAlgorithm.KeySize = keyBytes.Length * 8;
symmetricAlgorithm.Key = keyBytes;

#if !NET10_0_OR_GREATER
// For .NET 9 and below, we manually implement AES-KW using ECB mode
// On .NET 10+, we use native EncryptKeyWrapPadded/DecryptKeyWrapPadded which implement RFC 5649
// RFC 5649 is backward compatible with RFC 3394 when input is a multiple of 8 bytes
symmetricAlgorithm.Mode = CipherMode.ECB; // CodeQL [SM02199] Approved necessary usage of AES-ECB for implementing AES-KW
Comment thread
saurabhsathe-ms marked this conversation as resolved.
Dismissed
symmetricAlgorithm.Padding = PaddingMode.None;

// Set the AES IV to Zeroes
var aesIv = new byte[symmetricAlgorithm.BlockSize >> 3];
Utility.Zero(aesIv);
symmetricAlgorithm.IV = aesIv;
#endif

return symmetricAlgorithm;
}
Expand Down Expand Up @@ -227,6 +233,22 @@

private byte[] UnwrapKeyPrivate(byte[] inputBuffer, int inputOffset, int inputCount)
{
#if NET10_0_OR_GREATER
// Use native AES Key Unwrap (RFC 5649) available in .NET 10+
// RFC 5649 is backward compatible with RFC 3394 when input is a multiple of 8 bytes
byte[] wrappedKey;
if (inputOffset == 0 && inputCount == inputBuffer.Length)
{
wrappedKey = inputBuffer;
}
else
{
wrappedKey = new byte[inputCount];
Array.Copy(inputBuffer, inputOffset, wrappedKey, 0, inputCount);
}

return ((Aes)_symmetricAlgorithm.Value).DecryptKeyWrapPadded(wrappedKey);
#else
/*
1) Initialize variables.

Expand Down Expand Up @@ -319,6 +341,7 @@
{
throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogMessages.IDX10665));
}
#endif
}

private void ValidateKeySize(byte[] key, string algorithm)
Expand Down Expand Up @@ -382,6 +405,22 @@

private byte[] WrapKeyPrivate(byte[] inputBuffer, int inputOffset, int inputCount)
{
#if NET10_0_OR_GREATER
// Use native AES Key Wrap (RFC 5649) available in .NET 10+
// RFC 5649 is backward compatible with RFC 3394 when input is a multiple of 8 bytes
byte[] keyToWrap;
if (inputOffset == 0 && inputCount == inputBuffer.Length)
{
keyToWrap = inputBuffer;
}
else
{
keyToWrap = new byte[inputCount];
Array.Copy(inputBuffer, inputOffset, keyToWrap, 0, inputCount);
}

return ((Aes)_symmetricAlgorithm.Value).EncryptKeyWrapPadded(keyToWrap);
#else
/*
1) Initialize variables.

Expand Down Expand Up @@ -464,6 +503,7 @@
}

return keyBytes;
#endif
}
}
}
135 changes: 135 additions & 0 deletions test/Microsoft.IdentityModel.Tokens.Tests/KeyWrapProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,141 @@ private static void AddUnwrapParameterCheckTheoryData(string testId, string algo
WrappedKey = wrappedKey
});
}

#if NET10_0_OR_GREATER
/// <summary>
/// Test to verify .NET 10 native AES Key Wrap APIs produce results compatible with the manual implementation.
/// This ensures backward compatibility when using EncryptKeyWrapPadded/DecryptKeyWrapPadded (RFC 5649)
/// for inputs that are multiples of 8 bytes (RFC 3394 compatible).
/// </summary>
[Theory, MemberData(nameof(Net10BackwardCompatibilityTheoryData), DisableDiscoveryEnumeration = true)]
public void Net10_WrapUnwrap_BackwardCompatibility(KeyWrapTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.Net10_WrapUnwrap_BackwardCompatibility", theoryData);
try
{
// Create a provider using .NET 10 native implementation
var provider = CryptoProviderFactory.Default.CreateKeyWrapProvider(theoryData.WrapKey, theoryData.WrapAlgorithm);

// Wrap the key
var wrappedKey = provider.WrapKey(theoryData.KeyToWrap);

// Verify wrapped key is not null and has expected length
Assert.NotNull(wrappedKey);
Assert.True(wrappedKey.Length > theoryData.KeyToWrap.Length, "Wrapped key should be longer than original");
Assert.True(wrappedKey.Length % 8 == 0, "Wrapped key length should be a multiple of 8 bytes");

// Unwrap the key
byte[] unwrappedKey = provider.UnwrapKey(wrappedKey);

// Verify unwrapped key matches original
Assert.True(Utility.AreEqual(unwrappedKey, theoryData.KeyToWrap), "Unwrapped key should match original key");

theoryData.ExpectedException.ProcessNoException(context);
}
catch (Exception ex)
{
theoryData.ExpectedException.ProcessException(ex, context);
}

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<KeyWrapTheoryData> Net10BackwardCompatibilityTheoryData()
{
var theoryData = new TheoryData<KeyWrapTheoryData>();

// Test with various key sizes and data sizes (all multiples of 8 bytes for RFC 3394 compatibility)
AddNet10CompatibilityTest("Aes128_16ByteData", SecurityAlgorithms.Aes128KW, Default.SymmetricEncryptionKey128, 16, theoryData);
AddNet10CompatibilityTest("Aes128_24ByteData", SecurityAlgorithms.Aes128KW, Default.SymmetricEncryptionKey128, 24, theoryData);
AddNet10CompatibilityTest("Aes128_32ByteData", SecurityAlgorithms.Aes128KW, Default.SymmetricEncryptionKey128, 32, theoryData);
AddNet10CompatibilityTest("Aes256_16ByteData", SecurityAlgorithms.Aes256KW, Default.SymmetricEncryptionKey256, 16, theoryData);
AddNet10CompatibilityTest("Aes256_24ByteData", SecurityAlgorithms.Aes256KW, Default.SymmetricEncryptionKey256, 24, theoryData);
AddNet10CompatibilityTest("Aes256_32ByteData", SecurityAlgorithms.Aes256KW, Default.SymmetricEncryptionKey256, 32, theoryData);
AddNet10CompatibilityTest("Aes256_40ByteData", SecurityAlgorithms.Aes256KW, Default.SymmetricEncryptionKey256, 40, theoryData);

return theoryData;
}

private static void AddNet10CompatibilityTest(string testId, string algorithm, SecurityKey key, int dataSize, TheoryData<KeyWrapTheoryData> theoryData)
{
byte[] keyToWrap = new byte[dataSize];
new Random(42).NextBytes(keyToWrap); // Use fixed seed for reproducibility

theoryData.Add(new KeyWrapTheoryData
{
KeyToWrap = keyToWrap,
WrapAlgorithm = algorithm,
WrapKey = key,
TestId = $"Net10_BackwardCompatibility_{testId}"
});
}

/// <summary>
/// Test to verify that wrapped keys from .NET 10 can be unwrapped correctly.
/// This validates the native EncryptKeyWrapPadded/DecryptKeyWrapPadded implementation.
/// </summary>
[Theory, MemberData(nameof(Net10RoundTripTheoryData), DisableDiscoveryEnumeration = true)]
public void Net10_WrapUnwrap_RoundTrip(KeyWrapTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.Net10_WrapUnwrap_RoundTrip", theoryData);
try
{
var provider = CryptoProviderFactory.Default.CreateKeyWrapProvider(theoryData.WrapKey, theoryData.WrapAlgorithm);

// Perform multiple round trips to ensure consistency
for (int i = 0; i < 3; i++)
{
var wrappedKey = provider.WrapKey(theoryData.KeyToWrap);
byte[] unwrappedKey = provider.UnwrapKey(wrappedKey);

Assert.True(Utility.AreEqual(unwrappedKey, theoryData.KeyToWrap),
$"Round trip {i + 1}: Unwrapped key should match original key");
}

theoryData.ExpectedException.ProcessNoException(context);
}
catch (Exception ex)
{
theoryData.ExpectedException.ProcessException(ex, context);
}

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<KeyWrapTheoryData> Net10RoundTripTheoryData()
{
var theoryData = new TheoryData<KeyWrapTheoryData>();

// Test different algorithms and key sizes
theoryData.Add(new KeyWrapTheoryData
{
KeyToWrap = Guid.NewGuid().ToByteArray(),
WrapAlgorithm = SecurityAlgorithms.Aes128KW,
WrapKey = Default.SymmetricEncryptionKey128,
TestId = "Net10_RoundTrip_Aes128KW"
});

theoryData.Add(new KeyWrapTheoryData
{
KeyToWrap = Guid.NewGuid().ToByteArray(),
WrapAlgorithm = SecurityAlgorithms.Aes256KW,
WrapKey = Default.SymmetricEncryptionKey256,
TestId = "Net10_RoundTrip_Aes256KW"
});

// Test with JsonWebKey
theoryData.Add(new KeyWrapTheoryData
{
KeyToWrap = Guid.NewGuid().ToByteArray(),
WrapAlgorithm = SecurityAlgorithms.Aes128KW,
WrapKey = KeyingMaterial.JsonWebKeySymmetric128,
TestId = "Net10_RoundTrip_JsonWebKey_Aes128KW"
});

return theoryData;
}
#endif
}
}

Expand Down
Loading