mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
PPAP integration
This commit is contained in:
@@ -0,0 +1,783 @@
|
||||
using System.Buffers.Binary;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Authority.Providers.Ppap;
|
||||
|
||||
namespace Esiur.Tests.Unit;
|
||||
|
||||
public class PpapAuthenticationTests
|
||||
{
|
||||
const string Domain = "example.test";
|
||||
const string InitiatorIdentity = "alice";
|
||||
const string ResponderIdentity = "server";
|
||||
const string InitiatorPassword = "correct horse battery staple";
|
||||
const string ResponderPassword = "server paper password";
|
||||
|
||||
static readonly PpapKdfProfile TestKdf = new(
|
||||
PpapKdfProfile.Argon2Version13,
|
||||
memoryKiB: 8 * 1024,
|
||||
iterations: 1,
|
||||
parallelism: 1);
|
||||
|
||||
[Theory]
|
||||
[InlineData(AuthenticationMode.InitializerIdentity)]
|
||||
[InlineData(AuthenticationMode.ResponderIdentity)]
|
||||
[InlineData(AuthenticationMode.DualIdentity)]
|
||||
public void PasswordHandshake_AllIdentityModesDeriveMatchingKeys(
|
||||
AuthenticationMode mode)
|
||||
{
|
||||
using var pair = CreatePair(mode);
|
||||
|
||||
var result = CompleteHandshake(pair.Initiator, pair.Responder);
|
||||
|
||||
var authenticatesInitiator = mode is AuthenticationMode.InitializerIdentity
|
||||
or AuthenticationMode.DualIdentity;
|
||||
var authenticatesResponder = mode is AuthenticationMode.ResponderIdentity
|
||||
or AuthenticationMode.DualIdentity;
|
||||
|
||||
Assert.Equal(PpapProtocol.SessionKeyLength, result.Initiator.SessionKey.Length);
|
||||
Assert.Equal(result.Initiator.SessionKey, result.Responder.SessionKey);
|
||||
Assert.Equal(authenticatesInitiator ? InitiatorIdentity : null,
|
||||
result.Initiator.LocalIdentity);
|
||||
Assert.Equal(authenticatesResponder ? ResponderIdentity : null,
|
||||
result.Initiator.RemoteIdentity);
|
||||
Assert.Equal(authenticatesResponder ? ResponderIdentity : null,
|
||||
result.Responder.LocalIdentity);
|
||||
Assert.Equal(authenticatesInitiator ? InitiatorIdentity : null,
|
||||
result.Responder.RemoteIdentity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrongPassword_FailsWithoutChangingRegistration()
|
||||
{
|
||||
using var pair = CreatePair(
|
||||
AuthenticationMode.InitializerIdentity,
|
||||
initiatorPassword: "not the registered password");
|
||||
var before = Snapshot(pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
|
||||
var first = pair.Initiator.Process(null!);
|
||||
var second = pair.Responder.Process(Wire(first, PpapMessageType.ClientHello));
|
||||
var third = pair.Initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
||||
var fourth = pair.Responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
||||
var failed = pair.Initiator.Process(Wire(fourth, PpapMessageType.ResponderProof));
|
||||
|
||||
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
||||
Assert.Null(failed.SessionKey);
|
||||
AssertSnapshot(before, pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TamperedResponderFinished_FailsClosed()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
||||
|
||||
var first = pair.Initiator.Process(null!);
|
||||
var second = pair.Responder.Process(Wire(first, PpapMessageType.ClientHello));
|
||||
var third = pair.Initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
||||
var fourth = pair.Responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
||||
var tampered = TamperLastByte(Wire(fourth, PpapMessageType.ResponderProof));
|
||||
|
||||
var failed = pair.Initiator.Process(tampered);
|
||||
|
||||
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
||||
Assert.Null(failed.SessionKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TamperedInitiatorFinished_FailsClosed()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
||||
|
||||
var first = pair.Initiator.Process(null!);
|
||||
var second = pair.Responder.Process(Wire(first, PpapMessageType.ClientHello));
|
||||
var third = pair.Initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
||||
var fourth = pair.Responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
||||
var fifth = pair.Initiator.Process(Wire(fourth, PpapMessageType.ResponderProof));
|
||||
var tampered = TamperLastByte(Wire(fifth, PpapMessageType.InitiatorFinished));
|
||||
|
||||
var failed = pair.Responder.Process(tampered);
|
||||
|
||||
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
||||
Assert.Null(failed.SessionKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DualIdentityProofs_DoNotExposeRegistrationDescriptors()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
||||
var initiatorRegistration = pair.ResponderStore.Get(
|
||||
Domain,
|
||||
InitiatorIdentity);
|
||||
var responderRegistration = pair.InitiatorStore.Get(
|
||||
Domain,
|
||||
ResponderIdentity);
|
||||
|
||||
var first = pair.Initiator.Process(null!);
|
||||
var second = pair.Responder.Process(
|
||||
Wire(first, PpapMessageType.ClientHello));
|
||||
var third = pair.Initiator.Process(
|
||||
Wire(second, PpapMessageType.ServerHello));
|
||||
var initiatorProof = Wire(third, PpapMessageType.InitiatorProof);
|
||||
var fourth = pair.Responder.Process(initiatorProof);
|
||||
var responderProof = Wire(fourth, PpapMessageType.ResponderProof);
|
||||
|
||||
AssertDescriptorNotVisible(initiatorProof, responderRegistration);
|
||||
AssertDescriptorNotVisible(responderProof, initiatorRegistration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TamperedEncryptedResponderDescriptor_FailsClosed()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
||||
var first = pair.Initiator.Process(null!);
|
||||
var second = pair.Responder.Process(
|
||||
Wire(first, PpapMessageType.ClientHello));
|
||||
var third = pair.Initiator.Process(
|
||||
Wire(second, PpapMessageType.ServerHello));
|
||||
var tampered = TamperField(
|
||||
Wire(third, PpapMessageType.InitiatorProof),
|
||||
fieldIndex: 2);
|
||||
|
||||
var failed = pair.Responder.Process(tampered);
|
||||
|
||||
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
||||
Assert.Null(failed.SessionKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TamperedEncryptedInitiatorDescriptor_FailsClosed()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
||||
var first = pair.Initiator.Process(null!);
|
||||
var second = pair.Responder.Process(
|
||||
Wire(first, PpapMessageType.ClientHello));
|
||||
var third = pair.Initiator.Process(
|
||||
Wire(second, PpapMessageType.ServerHello));
|
||||
var fourth = pair.Responder.Process(
|
||||
Wire(third, PpapMessageType.InitiatorProof));
|
||||
var tampered = TamperField(
|
||||
Wire(fourth, PpapMessageType.ResponderProof),
|
||||
fieldIndex: 1);
|
||||
|
||||
var failed = pair.Initiator.Process(tampered);
|
||||
|
||||
Assert.Equal(AuthenticationRuling.Failed, failed.Ruling);
|
||||
Assert.Null(failed.SessionKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProtectedDescriptor_DiffersAcrossFreshSessionsForSameRegistration()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
||||
var firstProof = CreateInitiatorProof(pair.Initiator, pair.Responder);
|
||||
var initiatorProvider = Assert.IsType<PpapAuthenticationProvider>(
|
||||
pair.Initiator.Provider);
|
||||
var responderProvider = Assert.IsType<PpapAuthenticationProvider>(
|
||||
pair.Responder.Provider);
|
||||
using var secondInitiator = CreateHandler(
|
||||
initiatorProvider,
|
||||
AuthenticationDirection.Initiator,
|
||||
AuthenticationMode.DualIdentity);
|
||||
using var secondResponder = CreateHandler(
|
||||
responderProvider,
|
||||
AuthenticationDirection.Responder,
|
||||
AuthenticationMode.DualIdentity);
|
||||
var secondProof = CreateInitiatorProof(secondInitiator, secondResponder);
|
||||
|
||||
var firstDescriptor = ExtractField(firstProof, fieldIndex: 2);
|
||||
var secondDescriptor = ExtractField(secondProof, fieldIndex: 2);
|
||||
|
||||
Assert.Equal(144, firstDescriptor.Length);
|
||||
Assert.Equal(PpapProtocol.ProtectedDescriptorLength, firstDescriptor.Length);
|
||||
Assert.Equal(144, secondDescriptor.Length);
|
||||
Assert.False(firstDescriptor.SequenceEqual(secondDescriptor));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitializerPassword_RotationChangesVersionNonceAndKey()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
||||
CompleteHandshake(pair.Initiator, pair.Responder);
|
||||
var before = Snapshot(pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
|
||||
CompleteInitializerRotation(pair.Initiator, pair.Responder);
|
||||
|
||||
var after = pair.ResponderStore.Get(Domain, InitiatorIdentity);
|
||||
Assert.Equal(before.Version + 1, after.Version);
|
||||
Assert.False(before.Nonce.SequenceEqual(after.Nonce));
|
||||
Assert.False(before.EncapsulationKey.SequenceEqual(after.EncapsulationKey));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResponderPassword_RotationChangesVerifierRecord()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.ResponderIdentity);
|
||||
CompleteHandshake(pair.Initiator, pair.Responder);
|
||||
var before = Snapshot(pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
||||
|
||||
CompleteResponderRotation(pair.Initiator, pair.Responder);
|
||||
|
||||
AssertRotated(
|
||||
before,
|
||||
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DualPassword_RotationChangesBothVerifierRecords()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.DualIdentity);
|
||||
CompleteHandshake(pair.Initiator, pair.Responder);
|
||||
var initiatorBefore = Snapshot(
|
||||
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
var responderBefore = Snapshot(
|
||||
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
||||
|
||||
CompleteDualRotation(pair.Initiator, pair.Responder);
|
||||
|
||||
AssertRotated(
|
||||
initiatorBefore,
|
||||
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
AssertRotated(
|
||||
responderBefore,
|
||||
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TamperedRotationProof_FailsWithoutPersistingOffer()
|
||||
{
|
||||
using var pair = CreatePair(AuthenticationMode.InitializerIdentity);
|
||||
CompleteHandshake(pair.Initiator, pair.Responder);
|
||||
var before = Snapshot(pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
|
||||
var offer = pair.Initiator.BeginKeyRotation();
|
||||
var challenge = pair.Responder.ProcessKeyRotation(
|
||||
RotationWire(offer, PpapMessageType.RotationOffer));
|
||||
var proof = pair.Initiator.ProcessKeyRotation(
|
||||
RotationWire(challenge, PpapMessageType.RotationChallenge));
|
||||
var tampered = TamperLastByte(
|
||||
RotationWire(proof, PpapMessageType.RotationProof));
|
||||
|
||||
var failed = pair.Responder.ProcessKeyRotation(tampered);
|
||||
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Failed, failed.Ruling);
|
||||
AssertSnapshot(before, pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticDualIdentity_UsesEncryptedNoOpCompletionWithoutChangingRecords()
|
||||
{
|
||||
using var pair = CreateStaticPair();
|
||||
var initiatorBefore = Snapshot(
|
||||
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
var responderBefore = Snapshot(
|
||||
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
||||
CompleteHandshake(pair.Initiator, pair.Responder);
|
||||
|
||||
var done = pair.Initiator.BeginKeyRotation();
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
||||
var completed = pair.Responder.ProcessKeyRotation(
|
||||
RotationWire(done, PpapMessageType.RotationDone));
|
||||
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
||||
Assert.Null(completed.Data);
|
||||
AssertSnapshot(
|
||||
initiatorBefore,
|
||||
pair.ResponderStore.Get(Domain, InitiatorIdentity));
|
||||
AssertSnapshot(
|
||||
responderBefore,
|
||||
pair.InitiatorStore.Get(Domain, ResponderIdentity));
|
||||
}
|
||||
|
||||
static HandshakeResult CompleteHandshake(
|
||||
PpapAuthenticationHandler initiator,
|
||||
PpapAuthenticationHandler responder)
|
||||
{
|
||||
var first = initiator.Process(null!);
|
||||
Assert.Equal(AuthenticationRuling.InProgress, first.Ruling);
|
||||
|
||||
var second = responder.Process(Wire(first, PpapMessageType.ClientHello));
|
||||
Assert.Equal(AuthenticationRuling.InProgress, second.Ruling);
|
||||
|
||||
var third = initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
||||
Assert.Equal(AuthenticationRuling.InProgress, third.Ruling);
|
||||
|
||||
var fourth = responder.Process(Wire(third, PpapMessageType.InitiatorProof));
|
||||
Assert.Equal(AuthenticationRuling.InProgress, fourth.Ruling);
|
||||
|
||||
var fifth = initiator.Process(Wire(fourth, PpapMessageType.ResponderProof));
|
||||
Assert.Equal(AuthenticationRuling.Succeeded, fifth.Ruling);
|
||||
|
||||
var sixth = responder.Process(Wire(fifth, PpapMessageType.InitiatorFinished));
|
||||
Assert.Equal(AuthenticationRuling.Succeeded, sixth.Ruling);
|
||||
|
||||
return new HandshakeResult(fifth, sixth);
|
||||
}
|
||||
|
||||
static byte[] CreateInitiatorProof(
|
||||
PpapAuthenticationHandler initiator,
|
||||
PpapAuthenticationHandler responder)
|
||||
{
|
||||
var first = initiator.Process(null!);
|
||||
var second = responder.Process(Wire(first, PpapMessageType.ClientHello));
|
||||
var third = initiator.Process(Wire(second, PpapMessageType.ServerHello));
|
||||
return Wire(third, PpapMessageType.InitiatorProof);
|
||||
}
|
||||
|
||||
static PpapAuthenticationHandler CreateHandler(
|
||||
PpapAuthenticationProvider provider,
|
||||
AuthenticationDirection direction,
|
||||
AuthenticationMode mode)
|
||||
=> Assert.IsType<PpapAuthenticationHandler>(
|
||||
provider.CreateAuthenticationHandler(new AuthenticationContext
|
||||
{
|
||||
Direction = direction,
|
||||
Mode = mode,
|
||||
Domain = Domain,
|
||||
InitiatorIdentity = InitiatorIdentity,
|
||||
ResponderIdentity = ResponderIdentity,
|
||||
HostName = ResponderIdentity,
|
||||
}));
|
||||
|
||||
static void CompleteInitializerRotation(
|
||||
PpapAuthenticationHandler initiator,
|
||||
PpapAuthenticationHandler responder)
|
||||
{
|
||||
Assert.True(initiator.RequiresKeyRotation);
|
||||
Assert.True(responder.RequiresKeyRotation);
|
||||
|
||||
var offer = initiator.BeginKeyRotation();
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, offer.Ruling);
|
||||
|
||||
var challenge = responder.ProcessKeyRotation(
|
||||
RotationWire(offer, PpapMessageType.RotationOffer));
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, challenge.Ruling);
|
||||
|
||||
var proof = initiator.ProcessKeyRotation(
|
||||
RotationWire(challenge, PpapMessageType.RotationChallenge));
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, proof.Ruling);
|
||||
|
||||
var commit = responder.ProcessKeyRotation(
|
||||
RotationWire(proof, PpapMessageType.RotationProof));
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, commit.Ruling);
|
||||
|
||||
var done = initiator.ProcessKeyRotation(
|
||||
RotationWire(commit, PpapMessageType.RotationCommit));
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
||||
|
||||
var completed = responder.ProcessKeyRotation(
|
||||
RotationWire(done, PpapMessageType.RotationDone));
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
||||
Assert.Null(completed.Data);
|
||||
}
|
||||
|
||||
static void CompleteResponderRotation(
|
||||
PpapAuthenticationHandler initiator,
|
||||
PpapAuthenticationHandler responder)
|
||||
{
|
||||
var start = initiator.BeginKeyRotation();
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.InProgress, start.Ruling);
|
||||
|
||||
var offer = responder.ProcessKeyRotation(
|
||||
RotationWire(start, PpapMessageType.RotationStart));
|
||||
var challenge = initiator.ProcessKeyRotation(
|
||||
RotationWire(offer, PpapMessageType.RotationOffer));
|
||||
var proof = responder.ProcessKeyRotation(
|
||||
RotationWire(challenge, PpapMessageType.RotationChallenge));
|
||||
var commit = initiator.ProcessKeyRotation(
|
||||
RotationWire(proof, PpapMessageType.RotationProof));
|
||||
var acknowledgement = responder.ProcessKeyRotation(
|
||||
RotationWire(commit, PpapMessageType.RotationCommit));
|
||||
var done = initiator.ProcessKeyRotation(
|
||||
RotationWire(acknowledgement, PpapMessageType.RotationCommitAck));
|
||||
var completed = responder.ProcessKeyRotation(
|
||||
RotationWire(done, PpapMessageType.RotationDone));
|
||||
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
||||
}
|
||||
|
||||
static void CompleteDualRotation(
|
||||
PpapAuthenticationHandler initiator,
|
||||
PpapAuthenticationHandler responder)
|
||||
{
|
||||
var initiatorOffer = initiator.BeginKeyRotation();
|
||||
var initiatorChallenge = responder.ProcessKeyRotation(
|
||||
RotationWire(initiatorOffer, PpapMessageType.RotationOffer));
|
||||
var initiatorProof = initiator.ProcessKeyRotation(
|
||||
RotationWire(initiatorChallenge, PpapMessageType.RotationChallenge));
|
||||
var initiatorCommit = responder.ProcessKeyRotation(
|
||||
RotationWire(initiatorProof, PpapMessageType.RotationProof));
|
||||
var responderStart = initiator.ProcessKeyRotation(
|
||||
RotationWire(initiatorCommit, PpapMessageType.RotationCommit));
|
||||
|
||||
var responderOffer = responder.ProcessKeyRotation(
|
||||
RotationWire(responderStart, PpapMessageType.RotationStart));
|
||||
var responderChallenge = initiator.ProcessKeyRotation(
|
||||
RotationWire(responderOffer, PpapMessageType.RotationOffer));
|
||||
var responderProof = responder.ProcessKeyRotation(
|
||||
RotationWire(responderChallenge, PpapMessageType.RotationChallenge));
|
||||
var responderCommit = initiator.ProcessKeyRotation(
|
||||
RotationWire(responderProof, PpapMessageType.RotationProof));
|
||||
var acknowledgement = responder.ProcessKeyRotation(
|
||||
RotationWire(responderCommit, PpapMessageType.RotationCommit));
|
||||
var done = initiator.ProcessKeyRotation(
|
||||
RotationWire(acknowledgement, PpapMessageType.RotationCommitAck));
|
||||
var completed = responder.ProcessKeyRotation(
|
||||
RotationWire(done, PpapMessageType.RotationDone));
|
||||
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, done.Ruling);
|
||||
Assert.Equal(AuthenticationKeyRotationRuling.Succeeded, completed.Ruling);
|
||||
}
|
||||
|
||||
static byte[] Wire(AuthenticationResult result, PpapMessageType expectedType)
|
||||
{
|
||||
var wire = Assert.IsType<byte[]>(result.AuthenticationData);
|
||||
Assert.True(wire.Length >= 10);
|
||||
Assert.Equal(expectedType, (PpapMessageType)wire[5]);
|
||||
return wire;
|
||||
}
|
||||
|
||||
static byte[] RotationWire(
|
||||
AuthenticationKeyRotationResult result,
|
||||
PpapMessageType expectedType)
|
||||
{
|
||||
var wire = Assert.IsType<byte[]>(result.Data);
|
||||
Assert.True(wire.Length >= 10);
|
||||
Assert.Equal(expectedType, (PpapMessageType)wire[5]);
|
||||
return wire;
|
||||
}
|
||||
|
||||
static byte[] TamperLastByte(byte[] wire)
|
||||
{
|
||||
var tampered = (byte[])wire.Clone();
|
||||
tampered[^1] ^= 0x80;
|
||||
return tampered;
|
||||
}
|
||||
|
||||
static byte[] TamperField(byte[] wire, int fieldIndex)
|
||||
{
|
||||
var tampered = (byte[])wire.Clone();
|
||||
var offset = 10;
|
||||
for (var index = 0; index <= fieldIndex; index++)
|
||||
{
|
||||
Assert.True(tampered.Length - offset >= sizeof(int));
|
||||
var length = BinaryPrimitives.ReadInt32BigEndian(
|
||||
tampered.AsSpan(offset, sizeof(int)));
|
||||
offset += sizeof(int);
|
||||
Assert.InRange(length, 1, tampered.Length - offset);
|
||||
|
||||
if (index == fieldIndex)
|
||||
{
|
||||
tampered[offset + (length / 2)] ^= 0x80;
|
||||
return tampered;
|
||||
}
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The requested PPAP field was not found.");
|
||||
}
|
||||
|
||||
static byte[] ExtractField(byte[] wire, int fieldIndex)
|
||||
{
|
||||
var offset = 10;
|
||||
for (var index = 0; index <= fieldIndex; index++)
|
||||
{
|
||||
Assert.True(wire.Length - offset >= sizeof(int));
|
||||
var length = BinaryPrimitives.ReadInt32BigEndian(
|
||||
wire.AsSpan(offset, sizeof(int)));
|
||||
offset += sizeof(int);
|
||||
Assert.InRange(length, 1, wire.Length - offset);
|
||||
|
||||
if (index == fieldIndex)
|
||||
return wire.AsSpan(offset, length).ToArray();
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The requested PPAP field was not found.");
|
||||
}
|
||||
|
||||
static void AssertDescriptorNotVisible(
|
||||
byte[] wire,
|
||||
PpapRegistrationRecord registration)
|
||||
{
|
||||
Assert.False(ContainsSubsequence(wire, registration.Nonce),
|
||||
"The PPAP proof contains the plaintext registration nonce.");
|
||||
Assert.False(ContainsSubsequence(
|
||||
wire,
|
||||
EncodeDescriptorVersionPrefix(registration)),
|
||||
"The PPAP proof contains the plaintext registration version descriptor.");
|
||||
Assert.False(ContainsSubsequence(
|
||||
wire,
|
||||
EncodeDescriptorProfile(registration.KdfProfile)),
|
||||
"The PPAP proof contains the plaintext registration KDF descriptor.");
|
||||
}
|
||||
|
||||
static byte[] EncodeDescriptorVersionPrefix(PpapRegistrationRecord registration)
|
||||
{
|
||||
var encoded = new byte[1 + sizeof(long) + sizeof(int)];
|
||||
encoded[0] = (byte)registration.Kind;
|
||||
BinaryPrimitives.WriteInt64BigEndian(
|
||||
encoded.AsSpan(1, sizeof(long)),
|
||||
registration.Version);
|
||||
BinaryPrimitives.WriteInt32BigEndian(
|
||||
encoded.AsSpan(1 + sizeof(long), sizeof(int)),
|
||||
registration.Nonce.Length);
|
||||
return encoded;
|
||||
}
|
||||
|
||||
static byte[] EncodeDescriptorProfile(PpapKdfProfile profile)
|
||||
{
|
||||
Assert.NotNull(profile);
|
||||
var encoded = new byte[1 + (4 * sizeof(int))];
|
||||
encoded[0] = 1;
|
||||
BinaryPrimitives.WriteInt32BigEndian(
|
||||
encoded.AsSpan(1, sizeof(int)),
|
||||
profile.Version);
|
||||
BinaryPrimitives.WriteInt32BigEndian(
|
||||
encoded.AsSpan(1 + sizeof(int), sizeof(int)),
|
||||
profile.MemoryKiB);
|
||||
BinaryPrimitives.WriteInt32BigEndian(
|
||||
encoded.AsSpan(1 + (2 * sizeof(int)), sizeof(int)),
|
||||
profile.Iterations);
|
||||
BinaryPrimitives.WriteInt32BigEndian(
|
||||
encoded.AsSpan(1 + (3 * sizeof(int)), sizeof(int)),
|
||||
profile.Parallelism);
|
||||
return encoded;
|
||||
}
|
||||
|
||||
static bool ContainsSubsequence(byte[] value, byte[] candidate)
|
||||
{
|
||||
if (candidate.Length == 0 || candidate.Length > value.Length)
|
||||
return false;
|
||||
|
||||
for (var offset = 0; offset <= value.Length - candidate.Length; offset++)
|
||||
{
|
||||
if (value.AsSpan(offset, candidate.Length).SequenceEqual(candidate))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static RegistrationSnapshot Snapshot(PpapRegistrationRecord record)
|
||||
=> new(record.Version, record.Nonce, record.EncapsulationKey);
|
||||
|
||||
static void AssertSnapshot(RegistrationSnapshot expected,
|
||||
PpapRegistrationRecord actual)
|
||||
{
|
||||
Assert.Equal(expected.Version, actual.Version);
|
||||
Assert.Equal(expected.Nonce, actual.Nonce);
|
||||
Assert.Equal(expected.EncapsulationKey, actual.EncapsulationKey);
|
||||
}
|
||||
|
||||
static void AssertRotated(RegistrationSnapshot before,
|
||||
PpapRegistrationRecord after)
|
||||
{
|
||||
Assert.Equal(before.Version + 1, after.Version);
|
||||
Assert.False(before.Nonce.SequenceEqual(after.Nonce));
|
||||
Assert.False(before.EncapsulationKey.SequenceEqual(after.EncapsulationKey));
|
||||
}
|
||||
|
||||
static PpapPair CreatePair(
|
||||
AuthenticationMode mode,
|
||||
string initiatorPassword = InitiatorPassword,
|
||||
string responderPassword = ResponderPassword)
|
||||
{
|
||||
var authenticatesInitiator = mode is AuthenticationMode.InitializerIdentity
|
||||
or AuthenticationMode.DualIdentity;
|
||||
var authenticatesResponder = mode is AuthenticationMode.ResponderIdentity
|
||||
or AuthenticationMode.DualIdentity;
|
||||
|
||||
var initiatorStore = new InMemoryPpapRegistrationStore();
|
||||
var responderStore = new InMemoryPpapRegistrationStore();
|
||||
PpapLocalIdentity? initiatorLocal = null;
|
||||
PpapLocalIdentity? responderLocal = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (authenticatesInitiator)
|
||||
{
|
||||
initiatorLocal = PpapLocalIdentity.FromPassword(
|
||||
InitiatorIdentity, initiatorPassword, TestKdf);
|
||||
using var registered = PpapLocalIdentity.FromPassword(
|
||||
InitiatorIdentity, InitiatorPassword, TestKdf);
|
||||
Assert.True(responderStore.TryAdd(
|
||||
Domain,
|
||||
PpapRegistrationRecord.FromLocalIdentity(Domain, registered)));
|
||||
}
|
||||
|
||||
if (authenticatesResponder)
|
||||
{
|
||||
responderLocal = PpapLocalIdentity.FromPassword(
|
||||
ResponderIdentity, responderPassword, TestKdf);
|
||||
using var registered = PpapLocalIdentity.FromPassword(
|
||||
ResponderIdentity, ResponderPassword, TestKdf);
|
||||
Assert.True(initiatorStore.TryAdd(
|
||||
Domain,
|
||||
PpapRegistrationRecord.FromLocalIdentity(Domain, registered)));
|
||||
}
|
||||
|
||||
var initiatorProvider = new PpapAuthenticationProvider(
|
||||
initiatorLocal!, initiatorStore);
|
||||
var responderProvider = new PpapAuthenticationProvider(
|
||||
responderLocal!, responderStore);
|
||||
var initiator = Assert.IsType<PpapAuthenticationHandler>(
|
||||
initiatorProvider.CreateAuthenticationHandler(new AuthenticationContext
|
||||
{
|
||||
Direction = AuthenticationDirection.Initiator,
|
||||
Mode = mode,
|
||||
Domain = Domain,
|
||||
InitiatorIdentity = authenticatesInitiator ? InitiatorIdentity : null,
|
||||
ResponderIdentity = authenticatesResponder ? ResponderIdentity : null,
|
||||
HostName = ResponderIdentity,
|
||||
}));
|
||||
var responder = Assert.IsType<PpapAuthenticationHandler>(
|
||||
responderProvider.CreateAuthenticationHandler(new AuthenticationContext
|
||||
{
|
||||
Direction = AuthenticationDirection.Responder,
|
||||
Mode = mode,
|
||||
Domain = Domain,
|
||||
InitiatorIdentity = authenticatesInitiator ? InitiatorIdentity : null,
|
||||
ResponderIdentity = authenticatesResponder ? ResponderIdentity : null,
|
||||
HostName = ResponderIdentity,
|
||||
}));
|
||||
|
||||
return new PpapPair(
|
||||
initiatorLocal,
|
||||
responderLocal,
|
||||
initiatorStore,
|
||||
responderStore,
|
||||
initiator,
|
||||
responder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
initiatorLocal?.Dispose();
|
||||
responderLocal?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static PpapPair CreateStaticPair()
|
||||
{
|
||||
PpapCryptography.GenerateKeyPair(
|
||||
out var initiatorPrivateKey,
|
||||
out var initiatorPublicKey);
|
||||
PpapCryptography.GenerateKeyPair(
|
||||
out var responderPrivateKey,
|
||||
out var responderPublicKey);
|
||||
PpapLocalIdentity? initiatorLocal = null;
|
||||
PpapLocalIdentity? responderLocal = null;
|
||||
|
||||
try
|
||||
{
|
||||
initiatorLocal = PpapLocalIdentity.FromStaticKey(
|
||||
InitiatorIdentity,
|
||||
initiatorPrivateKey);
|
||||
responderLocal = PpapLocalIdentity.FromStaticKey(
|
||||
ResponderIdentity,
|
||||
responderPrivateKey);
|
||||
var initiatorStore = new InMemoryPpapRegistrationStore();
|
||||
var responderStore = new InMemoryPpapRegistrationStore();
|
||||
Assert.True(responderStore.TryAdd(
|
||||
Domain,
|
||||
PpapRegistrationRecord.FromLocalIdentity(Domain, initiatorLocal)));
|
||||
Assert.True(initiatorStore.TryAdd(
|
||||
Domain,
|
||||
PpapRegistrationRecord.FromLocalIdentity(Domain, responderLocal)));
|
||||
|
||||
var initiatorProvider = new PpapAuthenticationProvider(
|
||||
initiatorLocal!,
|
||||
initiatorStore);
|
||||
var responderProvider = new PpapAuthenticationProvider(
|
||||
responderLocal!,
|
||||
responderStore);
|
||||
var initiator = Assert.IsType<PpapAuthenticationHandler>(
|
||||
initiatorProvider.CreateAuthenticationHandler(new AuthenticationContext
|
||||
{
|
||||
Direction = AuthenticationDirection.Initiator,
|
||||
Mode = AuthenticationMode.DualIdentity,
|
||||
Domain = Domain,
|
||||
InitiatorIdentity = InitiatorIdentity,
|
||||
ResponderIdentity = ResponderIdentity,
|
||||
HostName = ResponderIdentity,
|
||||
}));
|
||||
var responder = Assert.IsType<PpapAuthenticationHandler>(
|
||||
responderProvider.CreateAuthenticationHandler(new AuthenticationContext
|
||||
{
|
||||
Direction = AuthenticationDirection.Responder,
|
||||
Mode = AuthenticationMode.DualIdentity,
|
||||
Domain = Domain,
|
||||
InitiatorIdentity = InitiatorIdentity,
|
||||
ResponderIdentity = ResponderIdentity,
|
||||
HostName = ResponderIdentity,
|
||||
}));
|
||||
|
||||
return new PpapPair(
|
||||
initiatorLocal,
|
||||
responderLocal,
|
||||
initiatorStore,
|
||||
responderStore,
|
||||
initiator,
|
||||
responder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
initiatorLocal?.Dispose();
|
||||
responderLocal?.Dispose();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
PpapCryptography.Clear(initiatorPrivateKey);
|
||||
PpapCryptography.Clear(initiatorPublicKey);
|
||||
PpapCryptography.Clear(responderPrivateKey);
|
||||
PpapCryptography.Clear(responderPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
readonly record struct RegistrationSnapshot(
|
||||
long Version,
|
||||
byte[] Nonce,
|
||||
byte[] EncapsulationKey);
|
||||
|
||||
readonly record struct HandshakeResult(
|
||||
AuthenticationResult Initiator,
|
||||
AuthenticationResult Responder);
|
||||
|
||||
sealed class PpapPair : IDisposable
|
||||
{
|
||||
readonly PpapLocalIdentity? _initiatorLocal;
|
||||
readonly PpapLocalIdentity? _responderLocal;
|
||||
|
||||
public InMemoryPpapRegistrationStore InitiatorStore { get; }
|
||||
public InMemoryPpapRegistrationStore ResponderStore { get; }
|
||||
public PpapAuthenticationHandler Initiator { get; }
|
||||
public PpapAuthenticationHandler Responder { get; }
|
||||
|
||||
public PpapPair(
|
||||
PpapLocalIdentity? initiatorLocal,
|
||||
PpapLocalIdentity? responderLocal,
|
||||
InMemoryPpapRegistrationStore initiatorStore,
|
||||
InMemoryPpapRegistrationStore responderStore,
|
||||
PpapAuthenticationHandler initiator,
|
||||
PpapAuthenticationHandler responder)
|
||||
{
|
||||
_initiatorLocal = initiatorLocal;
|
||||
_responderLocal = responderLocal;
|
||||
InitiatorStore = initiatorStore;
|
||||
ResponderStore = responderStore;
|
||||
Initiator = initiator;
|
||||
Responder = responder;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Initiator.Dispose();
|
||||
Responder.Dispose();
|
||||
_initiatorLocal?.Dispose();
|
||||
_responderLocal?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user