Skip to content
Merged
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
2 changes: 1 addition & 1 deletion cas-core-lib
136 changes: 136 additions & 0 deletions cas-dotnet-sdk-tests/MLDSAWrapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using CasDotnetSdk.PQC;
using CasDotnetSdkTests.PQC;
using Xunit;

namespace CasDotnetSdkTests
{
public class MLDSAWrapperTests
{
private readonly MLDSAWrapper _wrapper;

public MLDSAWrapperTests()
{
this._wrapper = new MLDSAWrapper();
}

[Fact]
public void GenerateKeyPair()
{
var keyPair = this._wrapper.GenerateSigningAndVerificationKey();
Assert.NotNull(keyPair);
Assert.NotNull(keyPair.SigningKey);
Assert.NotNull(keyPair.VerificationKey);
// ML-DSA-65: 32-byte seed signing key, 1952-byte verification key.
Assert.Equal(32, keyPair.SigningKey.Length);
Assert.Equal(1952, keyPair.VerificationKey.Length);
}

[Fact]
public void SignAndVerify()
{
var keyPair = this._wrapper.GenerateSigningAndVerificationKey();
byte[] message = Encoding.UTF8.GetBytes("This is a test message.");
byte[] signature = this._wrapper.Sign(keyPair.SigningKey, message);
Assert.NotNull(signature);
Assert.Equal(3309, signature.Length);
Assert.True(this._wrapper.Verify(keyPair.VerificationKey, signature, message));

byte[] modifiedMessage = Encoding.UTF8.GetBytes("This is a modified test message.");
Assert.False(this._wrapper.Verify(keyPair.VerificationKey, signature, modifiedMessage));
}

[Fact]
public void SignIsDeterministic()
{
var keyPair = this._wrapper.GenerateSigningAndVerificationKey();
byte[] message = Encoding.UTF8.GetBytes("deterministic");
byte[] first = this._wrapper.Sign(keyPair.SigningKey, message);
byte[] second = this._wrapper.Sign(keyPair.SigningKey, message);
Assert.Equal(first, second);
}

// NIST ACVP ML-DSA-65 (FIPS 204) sigVer known-answer vectors.
// cas-lib verifies with an empty context, so only empty-context (external, pure) tests are included.
[Fact]
public void NistSignatureVerificationVectors()
{
var root = PqcVectorData.Load("nist_mldsa_65_sigver.json");
int count = 0;
var failures = new List<string>();
foreach (var group in root.GetProperty("testGroups").EnumerateArray())
{
foreach (var test in group.GetProperty("tests").EnumerateArray())
{
int tcId = test.GetProperty("tcId").GetInt32();
byte[] pk = PqcVectorData.Hex(test.GetProperty("pk").GetString()!);
byte[] msg = PqcVectorData.Hex(test.GetProperty("message").GetString()!);
byte[] sig = PqcVectorData.Hex(test.GetProperty("signature").GetString()!);
bool expected = test.GetProperty("testPassed").GetBoolean();

bool actual;
try
{
actual = this._wrapper.Verify(pk, sig, msg);
}
catch
{
// A malformed key/signature is a verification failure.
actual = false;
}
if (actual != expected)
{
failures.Add($"tcId={tcId} expected={expected} actual={actual}");
}
count++;
}
}
Assert.True(count > 0, "No NIST ML-DSA vectors were loaded.");
Assert.True(failures.Count == 0, $"{failures.Count}/{count} NIST ML-DSA vectors mismatched: {string.Join("; ", failures)}");
}

// Project Wycheproof ML-DSA-65 verification vectors.
// cas-lib verifies with an empty context, so tests that carry a non-empty ctx are skipped.
[Fact]
public void WycheproofSignatureVerificationVectors()
{
var root = PqcVectorData.Load("wycheproof_mldsa_65_verify.json");
int count = 0;
var failures = new List<string>();
foreach (var group in root.GetProperty("testGroups").EnumerateArray())
{
byte[] pk = PqcVectorData.Hex(group.GetProperty("publicKey").GetString()!);
foreach (var test in group.GetProperty("tests").EnumerateArray())
{
if (test.TryGetProperty("ctx", out var ctx) && ctx.GetString()!.Length > 0)
{
continue;
}
int tcId = test.GetProperty("tcId").GetInt32();
byte[] msg = PqcVectorData.Hex(test.GetProperty("msg").GetString()!);
byte[] sig = PqcVectorData.Hex(test.GetProperty("sig").GetString()!);
bool expected = test.GetProperty("result").GetString() == "valid";

bool actual;
try
{
actual = this._wrapper.Verify(pk, sig, msg);
}
catch
{
actual = false;
}
if (actual != expected)
{
failures.Add($"tcId={tcId} expected={expected} actual={actual}");
}
count++;
}
}
Assert.True(count > 0, "No Wycheproof ML-DSA vectors were loaded.");
Assert.True(failures.Count == 0, $"{failures.Count}/{count} Wycheproof ML-DSA vectors mismatched: {string.Join("; ", failures)}");
}
}
}
160 changes: 160 additions & 0 deletions cas-dotnet-sdk-tests/MLKEMWrapperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Collections.Generic;
using System.Linq;
using CasDotnetSdk.PQC;
using CasDotnetSdkTests.PQC;
using Xunit;

namespace CasDotnetSdkTests
{
public class MLKEMWrapperTests
{
private readonly MLKEMWrapper _wrapper;

public MLKEMWrapperTests()
{
this._wrapper = new MLKEMWrapper();
}

[Fact]
public void GenerateKeyPair()
{
var keyPair = this._wrapper.GenerateKeyPair();
Assert.NotNull(keyPair);
// ML-KEM-1024: 3168-byte decapsulation (secret) key, 1568-byte encapsulation (public) key.
Assert.Equal(3168, keyPair.SecretKey.Length);
Assert.Equal(1568, keyPair.PublicKey.Length);
}

[Fact]
public void EncapsulateAndDecapsulate()
{
var keyPair = this._wrapper.GenerateKeyPair();
var encapsulated = this._wrapper.Encapsulate(keyPair.PublicKey);
Assert.Equal(1568, encapsulated.Ciphertext.Length);
Assert.Equal(32, encapsulated.SharedSecret.Length);

byte[] receiverSecret = this._wrapper.Decapsulate(keyPair.SecretKey, encapsulated.Ciphertext);
Assert.Equal(encapsulated.SharedSecret, receiverSecret);
}

[Fact]
public void DecapsulateTamperedCiphertextDiffersOrThrows()
{
var keyPair = this._wrapper.GenerateKeyPair();
var encapsulated = this._wrapper.Encapsulate(keyPair.PublicKey);
byte[] tampered = (byte[])encapsulated.Ciphertext.Clone();
tampered[0] ^= 0xFF;

// ML-KEM uses implicit rejection: a tampered ciphertext yields a different
// (pseudorandom) shared secret rather than an error.
byte[] receiverSecret = this._wrapper.Decapsulate(keyPair.SecretKey, tampered);
Assert.NotEqual(encapsulated.SharedSecret, receiverSecret);
}

// NIST ACVP ML-KEM-1024 (FIPS 203) encapDecap decapsulation (VAL) vectors:
// decapsulating c with dk must always produce the expected shared secret k
// (the expected value already accounts for implicit rejection).
[Fact]
public void NistDecapsulationVectors()
{
var root = PqcVectorData.Load("nist_mlkem_1024_decaps.json");
int count = 0;
var failures = new List<string>();
foreach (var group in root.GetProperty("testGroups").EnumerateArray())
{
foreach (var test in group.GetProperty("tests").EnumerateArray())
{
int tcId = test.GetProperty("tcId").GetInt32();
byte[] dk = PqcVectorData.Hex(test.GetProperty("dk").GetString()!);
byte[] c = PqcVectorData.Hex(test.GetProperty("c").GetString()!);
byte[] expected = PqcVectorData.Hex(test.GetProperty("k").GetString()!);

byte[] actual = this._wrapper.Decapsulate(dk, c);
if (!actual.SequenceEqual(expected))
{
failures.Add($"tcId={tcId}");
}
count++;
}
}
Assert.True(count > 0, "No NIST ML-KEM vectors were loaded.");
Assert.True(failures.Count == 0, $"{failures.Count}/{count} NIST ML-KEM vectors mismatched: {string.Join(", ", failures)}");
}

// Project Wycheproof ML-KEM-1024 semi-expanded decapsulation vectors.
// valid -> decaps(dk, c) == K.
// invalid -> inputs are malformed. A wrong dk/ciphertext length is rejected
// (decaps throws); a corrupted but correctly-sized key is not validated
// by FIPS 203 decapsulation, so it must still return a 32-byte secret
// without crashing. Invalid tests carry no K to compare against.
[Fact]
public void WycheproofDecapsulationVectors()
{
const int DkLen = 3168;
const int CtLen = 1568;
var root = PqcVectorData.Load("wycheproof_mlkem_1024_decaps.json");
int count = 0;
var failures = new List<string>();
foreach (var group in root.GetProperty("testGroups").EnumerateArray())
{
foreach (var test in group.GetProperty("tests").EnumerateArray())
{
int tcId = test.GetProperty("tcId").GetInt32();
byte[] dk = PqcVectorData.Hex(test.GetProperty("dk").GetString()!);
byte[] c = PqcVectorData.Hex(test.GetProperty("c").GetString()!);
bool isValid = test.GetProperty("result").GetString() == "valid";

if (isValid)
{
byte[] expected = PqcVectorData.Hex(test.GetProperty("K").GetString()!);
byte[] actual = this._wrapper.Decapsulate(dk, c);
if (!actual.SequenceEqual(expected))
{
failures.Add($"tcId={tcId} (valid) wrong shared secret");
}
}
else if (dk.Length != DkLen || c.Length != CtLen)
{
// Malformed length must be rejected.
if (!ThrowsOnDecapsulate(dk, c))
{
failures.Add($"tcId={tcId} (bad length) was not rejected");
}
}
else
{
// Correctly-sized but corrupted key: must not crash, returns 32 bytes.
try
{
byte[] actual = this._wrapper.Decapsulate(dk, c);
if (actual.Length != 32)
{
failures.Add($"tcId={tcId} (corrupted key) returned {actual.Length} bytes");
}
}
catch
{
// Rejecting a corrupted key is also acceptable.
}
}
count++;
}
}
Assert.True(count > 0, "No Wycheproof ML-KEM vectors were loaded.");
Assert.True(failures.Count == 0, $"{failures.Count}/{count} Wycheproof ML-KEM vectors mismatched: {string.Join("; ", failures)}");
}

private bool ThrowsOnDecapsulate(byte[] dk, byte[] c)
{
try
{
this._wrapper.Decapsulate(dk, c);
return false;
}
catch
{
return true;
}
}
}
}
Loading
Loading