Skip to content

Commit

Permalink
Add memory state manager
Browse files Browse the repository at this point in the history
  • Loading branch information
dorssel committed Jan 3, 2025
1 parent 8bae681 commit 8845be6
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 15 deletions.
23 changes: 10 additions & 13 deletions Examples/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions UnitTests/UnitTests/MemoryStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void StoreStatefulPart(ReadOnlySpan<byte> expected, ReadOnlySpan<byte> 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))
{
Expand All @@ -68,7 +68,7 @@ public void Load(XmssKeyPart part, Span<byte> 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)
{
Expand Down
185 changes: 185 additions & 0 deletions UnitTests/UnitTests/XmssMemoryStateManagerTests.cs
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>(() =>
{
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<ArgumentException>(() =>
{
stateManager.StoreStatefulPart([1], [2, 3]);
});
}

[TestMethod]
public void StoreStoreStatefulPart_FileNotExists()
{
using var stateManager = new XmssMemoryStateManager();

Assert.ThrowsException<InvalidOperationException>(() =>
{
stateManager.StoreStatefulPart([1], [2]);
});
}

[TestMethod]
public void StoreStoreStatefulPart_FileSizeMismatch()
{
using var stateManager = new XmssMemoryStateManager();
stateManager.Store(XmssKeyPart.PrivateStateful, [1]);

Assert.ThrowsException<ArgumentException>(() =>
{
stateManager.StoreStatefulPart([1, 2], [3, 4]);
});
}

[TestMethod]
public void StoreStoreStatefulPart_FileContentMismatch()
{
using var stateManager = new XmssMemoryStateManager();
stateManager.Store(XmssKeyPart.PrivateStateful, [1]);

Assert.ThrowsException<ArgumentException>(() =>
{
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<ArgumentException>(() =>
{
stateManager.Load(XmssKeyPart.Public, read);
});
}

[TestMethod]
public void Load_PartNotExists()
{
using var stateManager = new XmssMemoryStateManager();

Assert.ThrowsException<InvalidOperationException>(() =>
{
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<ArgumentOutOfRangeException>(() =>
{
stateManager.Load(Enum.GetValues<XmssKeyPart>().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();
}
}
134 changes: 134 additions & 0 deletions Xmss/XmssMemoryStateManager.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
public sealed class XmssMemoryStateManager()
: IXmssStateManager, IDisposable
{
readonly Dictionary<XmssKeyPart, byte[]?> 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));
}
}

/// <inheritdoc/>
public void Store(XmssKeyPart part, ReadOnlySpan<byte> 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();
}
}

/// <inheritdoc/>
public void StoreStatefulPart(ReadOnlySpan<byte> expected, ReadOnlySpan<byte> 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();
}
}

/// <inheritdoc/>
public void Load(XmssKeyPart part, Span<byte> 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);
}
}

/// <inheritdoc/>
public void DeletePublicPart()
{
lock (State)
{
ObjectDisposedException.ThrowIf(IsDisposed, this);

State[XmssKeyPart.Public] = null;
}
}

/// <inheritdoc/>
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;

/// <inheritdoc/>
public void Dispose()
{
lock (State)
{
if (!IsDisposed)
{
DeleteAll();
IsDisposed = true;
}
}
}
}

0 comments on commit 8845be6

Please sign in to comment.