2
0
mirror of https://github.com/esiur/esiur-dotnet.git synced 2026-06-13 14:38:43 +00:00
Files
esiur-dotnet/Tests/Unit/AuthHandshakeTests.cs
2026-06-02 19:28:09 +03:00

191 lines
7.4 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Esiur.Security.Authority;
using Esiur.Security.Authority.Providers;
namespace Esiur.Tests.Unit;
/// <summary>
/// Drives a pair of <see cref="PasswordAuthenticationHandler"/> instances (initiator and
/// responder) through the SHA3 challenge/response handshake and asserts both the happy
/// path (matching session keys) and the security hardening (constant-time challenge
/// checks, nonce validation, fail-closed behaviour on malformed peer input).
/// </summary>
public class AuthHandshakeTests
{
// ---- test credential store -------------------------------------------------------
class TestAccount
{
public string Identity;
public byte[] RawPassword;
public byte[] Salt;
public byte[] Hash; // SHA3-256(RawPassword || Salt), exactly what the verifier stores
}
static readonly byte[] FixedSalt = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6 };
static TestAccount MakeAccount(string identity, string password)
{
var raw = Encoding.UTF8.GetBytes(password);
var hash = PasswordAuthenticationHandler.ComputeSha3(raw.Concat(FixedSalt).ToArray());
return new TestAccount { Identity = identity, RawPassword = raw, Salt = FixedSalt, Hash = hash };
}
class StubProvider : PasswordAuthenticationProvider
{
readonly Dictionary<string, TestAccount> _accounts;
readonly string _self;
public StubProvider(string self, params TestAccount[] accounts)
{
_self = self;
_accounts = accounts.ToDictionary(a => a.Identity, a => a);
}
public override IdentityPassword GetSelfIdentityAndCredential(string domain, string hostname)
=> new IdentityPassword(_self, _accounts[_self].RawPassword);
public override byte[] GetSelfCredential(string identity, string domain, string hostname)
=> _accounts.TryGetValue(identity, out var a) ? a.RawPassword : null;
public override PasswordHash GetHostedAccountCredential(string identity, string domain)
=> _accounts.TryGetValue(identity, out var a)
? new PasswordHash(a.Hash, a.Salt)
: new PasswordHash(null, null);
}
// ---- helpers ---------------------------------------------------------------------
static object[] DataOf(AuthenticationResult result)
=> ((List<object>)result.AuthenticationData)?.ToArray();
static byte[] PrivateNonce(PasswordAuthenticationHandler handler)
=> (byte[])typeof(PasswordAuthenticationHandler)
.GetField("_localNonce", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(handler);
static (PasswordAuthenticationHandler init, PasswordAuthenticationHandler resp) NewPair(string identity = "alice")
{
var account = MakeAccount(identity, "correct horse battery staple");
var initiator = new PasswordAuthenticationHandler(
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Initiator,
identity, null, "host", "domain", new StubProvider(identity, account));
var responder = new PasswordAuthenticationHandler(
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Responder,
null, null, "host", "domain", new StubProvider(identity, account));
return (initiator, responder);
}
// ---- happy path ------------------------------------------------------------------
[Fact]
public void InitializerIdentity_Handshake_Derives_Matching_SessionKeys()
{
var (init, resp) = NewPair();
var r1 = init.Process(null); // -> [initNonce, initIdentity]
Assert.Equal(AuthenticationRuling.InProgress, r1.Ruling);
var r2 = resp.Process(DataOf(r1)); // -> [respNonce, respSalt, respChallenge]
Assert.Equal(AuthenticationRuling.InProgress, r2.Ruling);
var r3 = init.Process(DataOf(r2)); // -> [initChallenge], Succeeded
Assert.Equal(AuthenticationRuling.Succeeded, r3.Ruling);
var r4 = resp.Process(DataOf(r3)); // Succeeded
Assert.Equal(AuthenticationRuling.Succeeded, r4.Ruling);
Assert.NotNull(r3.SessionKey);
Assert.Equal(64, r3.SessionKey.Length); // 512-bit derived key
Assert.Equal(r3.SessionKey, r4.SessionKey); // both ends agree
}
[Fact]
public void Wrong_Password_Fails()
{
var account = MakeAccount("alice", "the real password");
var init = new PasswordAuthenticationHandler(
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Initiator,
"alice", null, "host", "domain",
new StubProvider("alice", MakeAccount("alice", "a different password")));
var resp = new PasswordAuthenticationHandler(
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Responder,
null, null, "host", "domain", new StubProvider("alice", account));
var r1 = init.Process(null);
var r2 = resp.Process(DataOf(r1));
// Initiator validates the responder's challenge against its (wrong) password and bails.
var r3 = init.Process(DataOf(r2));
Assert.Equal(AuthenticationRuling.Failed, r3.Ruling);
}
// ---- security properties ---------------------------------------------------------
[Fact]
public void Tampered_Challenge_Fails()
{
var (init, resp) = NewPair();
var r1 = init.Process(null);
var r2 = resp.Process(DataOf(r1));
var r3 = init.Process(DataOf(r2)); // [initChallenge]
var tampered = DataOf(r3);
((byte[])tampered[0])[0] ^= 0xFF; // flip a bit in the challenge
var r4 = resp.Process(tampered);
Assert.Equal(AuthenticationRuling.Failed, r4.Ruling);
}
[Fact]
public void Reflected_Nonce_Is_Rejected()
{
// Replay defence: feeding the responder its own nonce must be rejected.
var (init, resp) = NewPair();
var respNonce = PrivateNonce(resp);
var forged = new object[] { respNonce, "alice" };
var result = resp.Process(forged);
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
}
[Fact]
public void Short_Nonce_Is_Rejected()
{
var (_, resp) = NewPair();
var result = resp.Process(new object[] { new byte[5], "alice" });
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
}
[Theory]
[InlineData(0)] // empty
[InlineData(1)] // too few elements
public void Truncated_Input_Fails_Without_Throwing(int count)
{
var (_, resp) = NewPair();
var result = resp.Process(Enumerable.Range(0, count).Select(_ => (object)new byte[20]).ToArray());
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
}
[Fact]
public void Null_Input_Fails_Without_Throwing()
{
var (_, resp) = NewPair();
var result = resp.Process(null);
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
}
[Fact]
public void WrongType_Material_Fails_Closed()
{
// A peer sending a string where a nonce (byte[]) is expected must fail, not throw.
var (_, resp) = NewPair();
var result = resp.Process(new object[] { "not-a-nonce", "alice" });
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
}
}