From 8845be6bb43071d91608bbb8d58fef0a47b7c241 Mon Sep 17 00:00:00 2001 From: Frans van Dorsselaer <17404029+dorssel@users.noreply.github.com> Date: Fri, 3 Jan 2025 16:56:06 +0100 Subject: [PATCH] Add memory state manager --- Examples/ConsoleApp/Program.cs | 23 +-- UnitTests/UnitTests/MemoryStateManager.cs | 4 +- .../UnitTests/XmssMemoryStateManagerTests.cs | 185 ++++++++++++++++++ Xmss/XmssMemoryStateManager.cs | 134 +++++++++++++ 4 files changed, 331 insertions(+), 15 deletions(-) create mode 100644 UnitTests/UnitTests/XmssMemoryStateManagerTests.cs create mode 100644 Xmss/XmssMemoryStateManager.cs diff --git a/Examples/ConsoleApp/Program.cs b/Examples/ConsoleApp/Program.cs index 0bac5f9..a3182da 100644 --- a/Examples/ConsoleApp/Program.cs +++ b/Examples/ConsoleApp/Program.cs @@ -26,13 +26,16 @@ static async Task Main() #pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code Console.WriteLine($"Created by CryptoConfig('XMSS'): {alg is not null}"); } + + var message = new byte[] { 1, 2, 3 }; + byte[] signature; + string publicKeyPem; + { Console.WriteLine("Generating new key..."); - var stateManager = new XmssFileStateManager(@"C:\test"); - stateManager.DeleteAll(); using var xmss = new Xmss(); - xmss.GeneratePrivateKey(stateManager, XmssParameterSet.XMSS_SHA2_10_256, false); + xmss.GeneratePrivateKey(new XmssEphemeralStateManager(), XmssParameterSet.XMSS_SHA2_10_256, false); { // this is special for XMSS, a long-running (cancelable) process @@ -63,19 +66,13 @@ await xmss.CalculatePublicKeyAsync(progress => } } Console.WriteLine(); + + publicKeyPem = xmss.ExportSubjectPublicKeyInfoPem(); Console.WriteLine("Public Key:"); - Console.WriteLine(xmss.ExportSubjectPublicKeyInfoPem()); - } - var message = new byte[] { 1, 2, 3 }; - byte[] signature; - string publicKeyPem; - { - Console.WriteLine("Signing a message..."); + Console.WriteLine(publicKeyPem); - using var xmss = new Xmss(); - xmss.ImportPrivateKey(new XmssFileStateManager(@"C:\test")); + Console.WriteLine("Signing a message..."); signature = xmss.Sign(message); - publicKeyPem = xmss.ExportSubjectPublicKeyInfoPem(); } { using var xmss = new Xmss(); diff --git a/UnitTests/UnitTests/MemoryStateManager.cs b/UnitTests/UnitTests/MemoryStateManager.cs index 2e07803..72dfa1a 100644 --- a/UnitTests/UnitTests/MemoryStateManager.cs +++ b/UnitTests/UnitTests/MemoryStateManager.cs @@ -53,7 +53,7 @@ public void StoreStatefulPart(ReadOnlySpan expected, ReadOnlySpan da if (State[XmssKeyPart.PrivateStateful] is not byte[] oldData) { - throw new InvalidOperationException("Part does not exists."); + throw new InvalidOperationException("Part does not exist."); } if (!expected.SequenceEqual(oldData)) { @@ -68,7 +68,7 @@ public void Load(XmssKeyPart part, Span destination) if (State[part] is not byte[] data) { - throw new ArgumentException("Part does not exist.", nameof(part)); + throw new InvalidOperationException("Part does not exist."); } if (data.Length != destination.Length) { diff --git a/UnitTests/UnitTests/XmssMemoryStateManagerTests.cs b/UnitTests/UnitTests/XmssMemoryStateManagerTests.cs new file mode 100644 index 0000000..91550f0 --- /dev/null +++ b/UnitTests/UnitTests/XmssMemoryStateManagerTests.cs @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2025 Frans van Dorsselaer +// +// SPDX-License-Identifier: MIT + +using Dorssel.Security.Cryptography; + +namespace UnitTests; + +[TestClass] +sealed class XmssMemoryStateManagerTests +{ + [TestMethod] + public void Constructor() + { + using var stateManager = new XmssMemoryStateManager(); + } + + [TestMethod] + public void Store() + { + using var stateManager = new XmssMemoryStateManager(); + + stateManager.Store(XmssKeyPart.Public, [1]); + } + + [TestMethod] + public void Store_Exists() + { + using var stateManager = new XmssMemoryStateManager(); + + stateManager.Store(XmssKeyPart.Public, [1]); + + Assert.ThrowsException(() => + { + stateManager.Store(XmssKeyPart.Public, [2]); + }); + } + + [TestMethod] + public void StoreStoreStatefulPart() + { + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.PrivateStateful, [1]); + + stateManager.StoreStatefulPart([1], [2]); + } + + [TestMethod] + public void StoreStoreStatefulPart_ExpectedAndDataMismatch() + { + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.PrivateStateful, [1]); + + Assert.ThrowsException(() => + { + stateManager.StoreStatefulPart([1], [2, 3]); + }); + } + + [TestMethod] + public void StoreStoreStatefulPart_FileNotExists() + { + using var stateManager = new XmssMemoryStateManager(); + + Assert.ThrowsException(() => + { + stateManager.StoreStatefulPart([1], [2]); + }); + } + + [TestMethod] + public void StoreStoreStatefulPart_FileSizeMismatch() + { + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.PrivateStateful, [1]); + + Assert.ThrowsException(() => + { + stateManager.StoreStatefulPart([1, 2], [3, 4]); + }); + } + + [TestMethod] + public void StoreStoreStatefulPart_FileContentMismatch() + { + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.PrivateStateful, [1]); + + Assert.ThrowsException(() => + { + stateManager.StoreStatefulPart([2], [3]); + }); + } + + [TestMethod] + public void Load() + { + var data = new byte[] { 1, 2, 3 }; + + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.Public, data); + + var read = new byte[data.Length]; + stateManager.Load(XmssKeyPart.Public, read); + + CollectionAssert.AreEqual(data, read); + } + + [TestMethod] + public void Load_WrongSize() + { + var data = new byte[] { 1, 2, 3 }; + + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.Public, data); + + var read = new byte[data.Length - 1]; + Assert.ThrowsException(() => + { + stateManager.Load(XmssKeyPart.Public, read); + }); + } + + [TestMethod] + public void Load_PartNotExists() + { + using var stateManager = new XmssMemoryStateManager(); + + Assert.ThrowsException(() => + { + stateManager.Load(XmssKeyPart.Public, new byte[1]); + }); + } + + [TestMethod] + public void Load_UnknownPart() + { + var data = new byte[] { 1, 2, 3 }; + + using var stateManager = new XmssMemoryStateManager(); + + Assert.ThrowsException(() => + { + stateManager.Load(Enum.GetValues().Max() + 1, new byte[1]); + }); + } + + [TestMethod] + public void DeletePublicPart() + { + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.Public, [1]); + + stateManager.DeletePublicPart(); + + + } + + [TestMethod] + public void DeletePublicPart_PartNotExists() + { + using var stateManager = new XmssMemoryStateManager(); + + stateManager.DeletePublicPart(); + } + + [TestMethod] + public void DeleteAll() + { + using var stateManager = new XmssMemoryStateManager(); + stateManager.Store(XmssKeyPart.PrivateStateless, [1]); + stateManager.Store(XmssKeyPart.PrivateStateful, [2]); + stateManager.Store(XmssKeyPart.Public, [3]); + + stateManager.DeleteAll(); + } + + [TestMethod] + public void DeleteAll_PartsNotExist() + { + using var stateManager = new XmssMemoryStateManager(); + + stateManager.DeleteAll(); + } +} diff --git a/Xmss/XmssMemoryStateManager.cs b/Xmss/XmssMemoryStateManager.cs new file mode 100644 index 0000000..01e8eaa --- /dev/null +++ b/Xmss/XmssMemoryStateManager.cs @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2025 Frans van Dorsselaer +// +// SPDX-License-Identifier: MIT + +using System.Diagnostics; +using System.Security.Cryptography; + +namespace Dorssel.Security.Cryptography; + +/// +/// TODO +/// +public sealed class XmssMemoryStateManager() + : IXmssStateManager, IDisposable +{ + readonly Dictionary State = new() + { + { XmssKeyPart.PrivateStateless, null }, + { XmssKeyPart.PrivateStateful, null }, + { XmssKeyPart.Public, null }, + }; + + [StackTraceHidden] + void ThrowIfInvalidPart(XmssKeyPart part) + { + if (!State.ContainsKey(part)) + { + throw new ArgumentOutOfRangeException(nameof(part)); + } + } + + /// + public void Store(XmssKeyPart part, ReadOnlySpan data) + { + lock (State) + { + ThrowIfInvalidPart(part); + + ObjectDisposedException.ThrowIf(IsDisposed, this); + + if (State[part] is not null) + { + throw new InvalidOperationException("Part already exists."); + } + State[part] = data.ToArray(); + } + } + + /// + public void StoreStatefulPart(ReadOnlySpan expected, ReadOnlySpan data) + { + if (data.Length != expected.Length) + { + throw new ArgumentException("Expected data and new data must have the same size."); + } + + lock (State) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + + if (State[XmssKeyPart.PrivateStateful] is not byte[] oldData) + { + throw new InvalidOperationException("Part does not exist."); + } + if (!expected.SequenceEqual(oldData)) + { + throw new ArgumentException("Expected content mismatch.", nameof(expected)); + } + State[XmssKeyPart.PrivateStateful] = data.ToArray(); + } + } + + /// + public void Load(XmssKeyPart part, Span destination) + { + lock (State) + { + ThrowIfInvalidPart(part); + + ObjectDisposedException.ThrowIf(IsDisposed, this); + + if (State[part] is not byte[] data) + { + throw new InvalidOperationException("Part does not exist."); + } + if (data.Length != destination.Length) + { + throw new ArgumentException("Part size mismatch.", nameof(destination)); + } + data.CopyTo(destination); + } + } + + /// + public void DeletePublicPart() + { + lock (State) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + + State[XmssKeyPart.Public] = null; + } + } + + /// + public void DeleteAll() + { + lock (State) + { + ObjectDisposedException.ThrowIf(IsDisposed, this); + + CryptographicOperations.ZeroMemory(State[XmssKeyPart.PrivateStateless]); + CryptographicOperations.ZeroMemory(State[XmssKeyPart.PrivateStateful]); + State[XmssKeyPart.PrivateStateless] = null; + State[XmssKeyPart.PrivateStateful] = null; + State[XmssKeyPart.Public] = null; + } + } + + bool IsDisposed; + + /// + public void Dispose() + { + lock (State) + { + if (!IsDisposed) + { + DeleteAll(); + IsDisposed = true; + } + } + } +}